chore: sync the frontend template from 4dc033c to 8157ad4

The product was materialized from the template at `4dc033c` and has stayed
on it through 43 template commits, so it was missing all three rounds of
adapter remediation — including files it never had, such as the shared
`abortable-operation` primitive and the `exact-snapshot` decoder that
later fixes are written against. Taking only the newest round was not
possible for that reason: the delta is coherent only as a whole.

The product had not touched `src/adapters` at all since materialization,
so the 140-file delta applied with a three-way merge and no conflicts.
`package.json` was the single overlap and merged cleanly: the product owns
`name`, the template contributed `check:adapter-inventory`,
`check:remediation-ledger` and the image-resolve-signal type fixture.
All 24 product-owned files — README, index.html, CI workflow, i18n
catalog, home page, generated schemas, evidence scripts, component and
visual snapshots — are byte-identical to `main`.

`template.lock.json` now pins the synced revision and tree.

Verified in this repository, not inherited from the template: six type
projects, lint, nine gates (adapter inventory, remediation ledger,
registries, diagnostics, realtime boundaries, architecture, browser
file/storage boundaries, optional recipes, documentation), the production
build, and 2,054 of 2,073 tests. The 19 failures are all in
`tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing
sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template
records; four suites that failed once under parallel load pass in
isolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-15 12:04:58 +09:00
co-authored by Claude Opus 5
parent 002ba3624e
commit 4bff9ca151
142 changed files with 23010 additions and 1544 deletions
+31
View File
@@ -0,0 +1,31 @@
import { lstat, mkdir, readdir, symlink } from "node:fs/promises";
import path from "node:path";
/**
* Links the repository's installed dependencies into a throwaway fixture root.
*
* The obvious form — one directory symlink at `<fixture>/node_modules` — is
* destructive. A fixture runs `pnpm` inside itself, pnpm does not recognise the
* modules directory it finds there, and its purge follows the symlink and
* deletes the *repository's* real dependencies mid-run. With `CI=true` that
* happens without a prompt, so a test suite silently uninstalls the workspace
* it is running in.
*
* `node_modules` is therefore a real directory here, and every entry inside it
* is an individual symlink. Package resolution is unchanged, but a recursive
* delete unlinks the fixture's own symlinks instead of walking through one link
* into the shared tree.
*/
export async function linkFixtureNodeModules(
fixtureRoot: string,
sourceRoot: string = process.cwd(),
): Promise<void> {
const source = path.join(sourceRoot, "node_modules");
const target = path.join(fixtureRoot, "node_modules");
await mkdir(target, { recursive: true });
for (const entry of await readdir(source)) {
const from = path.join(source, entry);
const stats = await lstat(from);
await symlink(from, path.join(target, entry), stats.isDirectory() ? "dir" : "file");
}
}
+2 -2
View File
@@ -5,7 +5,6 @@ import {
readFile,
readdir,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import path from "node:path";
@@ -14,6 +13,7 @@ import {
loadCiGateContract,
parseCiGateContract,
} from "../contracts/ci-gates.ts";
import { linkFixtureNodeModules } from "./fixture-node-modules.ts";
import { generateCiWorkflow } from "../generate-ci-workflow.ts";
export const REMOVAL_FIXTURE_COPY_TARGETS = Object.freeze([
@@ -42,7 +42,7 @@ export async function prepareRemovalFixture(
for (const target of copyTargets) {
await cp(target, path.join(root, target), { recursive: true });
}
await symlink(path.resolve("node_modules"), path.join(root, "node_modules"), "dir");
await linkFixtureNodeModules(root);
}
export function runRemovalFixturePnpm(
+27 -11
View File
@@ -1,3 +1,10 @@
import { createHash } from "node:crypto";
import {
canonicalStaticManifestBytes,
decodeStaticAssetManifest,
} from "../../src/contracts/service-worker-static-manifest.ts";
import type {
InstalledServiceWorkerSelection,
ServiceWorkerHandlerId,
@@ -79,19 +86,28 @@ export function resolveServiceWorkerBuildInput(input: Readonly<{
});
}
/**
* SW-05. The build gate no longer type-casts the manifest. It decodes every row
* through the shared runtime-neutral codec and recomputes the set digest from
* the same canonical bytes the generator hashed, so a tampered row, a reordered
* set or a stale digest fails admission instead of shipping.
*/
function parseAssets(value: unknown): StaticAssetManifestV1 {
const candidate = record(value);
if (
candidate?.schemaVersion !== 1 ||
typeof candidate.buildId !== "string" ||
typeof candidate.releaseId !== "string" ||
typeof candidate.setDigest !== "string" ||
!DIGEST.test(candidate.setDigest) ||
!Array.isArray(candidate.assets)
) {
throw new TypeError("Generated Service Worker asset manifest is invalid.");
const decoded = decodeStaticAssetManifest(value);
if (!decoded.ok) {
throw new TypeError(
`Generated Service Worker asset manifest is invalid: ${decoded.error.reason}`,
);
}
return candidate as unknown as StaticAssetManifestV1;
const expected = `sha256:${createHash("sha256")
.update(canonicalStaticManifestBytes(decoded.manifest.assets))
.digest("hex")}`;
if (expected !== decoded.manifest.setDigest) {
throw new TypeError(
"Generated Service Worker asset manifest set digest does not match its assets.",
);
}
return decoded.manifest as unknown as StaticAssetManifestV1;
}
function record(value: unknown): Record<string, unknown> | null {