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:
co-authored by
Claude Opus 5
parent
002ba3624e
commit
4bff9ca151
@@ -1,4 +1,11 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import {
|
||||
CACHEABLE_ASSET_CONTENT_TYPES,
|
||||
canonicalStaticManifestBytes,
|
||||
decodeStaticAssetManifest,
|
||||
isCanonicalStaticAssetUrl,
|
||||
} from "../src/contracts/service-worker-static-manifest.ts";
|
||||
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -18,15 +25,9 @@ import {
|
||||
|
||||
const OUTPUT = ".generated/frontend-runtime/service-worker-assets.ts";
|
||||
|
||||
const CACHEABLE_EXTENSIONS: Readonly<Record<string, string>> = Object.freeze({
|
||||
".js": "text/javascript",
|
||||
".mjs": "text/javascript",
|
||||
".css": "text/css",
|
||||
".woff2": "font/woff2",
|
||||
".svg": "image/svg+xml",
|
||||
".png": "image/png",
|
||||
".webp": "image/webp",
|
||||
});
|
||||
// SW-RR-03. The generator and the shared decoder read the same table, so a
|
||||
// manifest this script produces can never be one the runtime contract refuses.
|
||||
const CACHEABLE_EXTENSIONS = CACHEABLE_ASSET_CONTENT_TYPES;
|
||||
|
||||
const EXCLUDED_FILES: ReadonlySet<string> = new Set([
|
||||
"index.html",
|
||||
@@ -58,8 +59,17 @@ export async function collectStaticAssets(
|
||||
if (bytes.byteLength > SERVICE_WORKER_BOUNDS.singleAssetBytes) {
|
||||
throw new Error(`Static asset exceeds its byte bound: ${relative}`);
|
||||
}
|
||||
// SW-02. The URL is checked against the same predicate the runtime decoder
|
||||
// applies. Emitting a path the decoder will refuse turned a correct build
|
||||
// into a runtime contract failure discovered only at install time.
|
||||
const url = `/${relative.split(path.sep).join("/")}`;
|
||||
if (!isCanonicalStaticAssetUrl(url)) {
|
||||
throw new Error(
|
||||
`Static asset path is not canonical for the service worker manifest: ${relative}`,
|
||||
);
|
||||
}
|
||||
assets.push({
|
||||
url: `/${relative.split(path.sep).join("/")}`,
|
||||
url,
|
||||
sha256: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
|
||||
bytes: bytes.byteLength,
|
||||
contentType,
|
||||
@@ -74,32 +84,31 @@ export async function collectStaticAssets(
|
||||
throw new Error("Static asset set exceeds its byte bound.");
|
||||
}
|
||||
|
||||
// The set digest is a length-prefixed hash over the sorted asset identities,
|
||||
// so a reordered directory listing cannot change it.
|
||||
const hash = createHash("sha256");
|
||||
hash.update("CA_STATIC_ASSET_SET_V1\0");
|
||||
for (const asset of assets) {
|
||||
hash.update(lengthPrefixed(asset.url));
|
||||
hash.update(lengthPrefixed(asset.sha256));
|
||||
hash.update(lengthPrefixed(String(asset.bytes)));
|
||||
hash.update(lengthPrefixed(asset.contentType));
|
||||
}
|
||||
// SW-05. The canonical byte serialization lives in the shared runtime-neutral
|
||||
// codec so the worker can recompute the identical digest with WebCrypto.
|
||||
const setDigest: `sha256:${string}` = `sha256:${createHash("sha256")
|
||||
.update(canonicalStaticManifestBytes(assets))
|
||||
.digest("hex")}`;
|
||||
|
||||
return {
|
||||
const manifest: StaticAssetManifestV1 = {
|
||||
schemaVersion: 1,
|
||||
buildId,
|
||||
releaseId,
|
||||
setDigest: `sha256:${hash.digest("hex")}`,
|
||||
setDigest,
|
||||
assets,
|
||||
};
|
||||
// SW-02. Every manifest this generator returns has already passed the exact
|
||||
// decoder the runtime will apply to it, so the build stops here rather than
|
||||
// at install time.
|
||||
const decoded = decodeStaticAssetManifest(manifest);
|
||||
if (!decoded.ok) {
|
||||
throw new Error(
|
||||
`Generated service worker manifest is not decodable: ${decoded.error.reason}`,
|
||||
);
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
function lengthPrefixed(value: string): Buffer {
|
||||
const bytes = Buffer.from(value, "utf8");
|
||||
const prefix = Buffer.alloc(4);
|
||||
prefix.writeUInt32BE(bytes.byteLength, 0);
|
||||
return Buffer.concat([prefix, bytes]);
|
||||
}
|
||||
|
||||
async function walk(root: string, current: string): Promise<string[]> {
|
||||
const entries = await readdir(current, { withFileTypes: true });
|
||||
|
||||
Reference in New Issue
Block a user