From d5e7f4127affe902cedbfff09f5519b80a3e4914 Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Sat, 15 Aug 2026 01:26:27 +0900 Subject: [PATCH] chore: derive the remediation closure claim instead of authoring it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ledger declared "All 38 are now FIXED" while six of those rows were reproducibly partial. A summary sentence is cheap and a reviewer reads it as evidence, so the claim is now derived from a machine-readable record: `docs/operations/adapter-remediation-dispositions.json` carries each finding's disposition and the test paths that hold it, and `check:remediation-ledger` joins that file to the prose, verifies every evidence path exists, and refuses a blanket closure sentence while any row is still open. The shared-abort gate had the same weakness in miniature: it passed when at least one production file imported the primitive, so an unrelated import satisfied it while Image and Resumable kept their own diverging copies. It now requires the four named consumers to resolve their import to the primitive itself, and prints the exact importer set rather than a count. `check:optional-recipes:source` was already failing before this work (52,078 against a 52,000 budget) and the correctness code above pushed it further. Duplicate abort mechanics were consolidated first — Image and Resumable onto the shared primitive, four decoders onto one snapshot helper — and the remainder is code the review asked for, so the budget is reset to 54,600 against a measured 53,810 with that reasoning recorded, rather than the failure being carried forward as if it were green. Co-Authored-By: Claude Opus 5 --- .../recipes/frontend-capability-recipes.json | 2 +- .../adapter-remediation-dispositions.json | 186 ++++++++++++++++++ docs/operations/adapter-remediation-ledger.md | 91 ++++++++- package.json | 1 + scripts/check-adapter-inventory.ts | 57 +++++- scripts/check-remediation-ledger.ts | 120 +++++++++++ 6 files changed, 444 insertions(+), 13 deletions(-) create mode 100644 docs/operations/adapter-remediation-dispositions.json create mode 100644 scripts/check-remediation-ledger.ts diff --git a/config/recipes/frontend-capability-recipes.json b/config/recipes/frontend-capability-recipes.json index a7f79e9..d27a024 100644 --- a/config/recipes/frontend-capability-recipes.json +++ b/config/recipes/frontend-capability-recipes.json @@ -195,7 +195,7 @@ "lifecycleMethods": ["release-file-ref", "release-or-dispose-preview-leases", "cancel-via-AbortSignal", "reconcile-or-explicitly-abort-upload", "close-checkpoint-store", "dispose-capability-and-image-runtime"], "owner": "project-owner-required", "securityPrivacy": ["Treat file name, extension, MIME and lastModified as untrusted metadata.", "Resolve only exact composition-issued file and image policy object identities; callers cannot raise byte, candidate, pixel, quality, format, lifetime or origin ceilings.", "Use opaque file references and verification receipts bound to an inspected immutable file snapshot and the exact registered profile; reject replay through another profile even when an inspection rule ID matches.", "Treat presigned URLs as bearer capabilities; bind exact method, resource or upload part, offset, length, media type, checksum, origin, path, query, headers and expiry in an in-memory identity vault.", "Use credentials omit, redirect error, no-referrer and no-store for direct data-plane fetch; never persist or observe URL, query, signed header, capability, file name or raw backend message, and never emit digest, raw ETag or receipt values to diagnostics or telemetry.", "A strict account-partitioned upload checkpoint may persist only the protocol-defined SHA-256 file fingerprint, per-part checksum and bounded opaque non-authorizing part receipt token required for server reconciliation; no bearer token or raw signed capability is allowed.", "Persist only strict non-authorizing upload checkpoints and reconcile them with server-authoritative status and re-hashed local parts before completion.", "Require a synchronous server-issued browser-managed download capability whose receipt exactly equals the caller's branded capability receipt and whose resource, media type, safe extension, maximum bytes, optional digest and expiry all match before handoff.", "Expose File, OPFS, Cache and transfer byte streams only as chunk-level closed Results; stop after the first failure, cancel native readers and never throw a raw native exception across the port.", "Accept Image CDN assets only through immutable allowlisted or signature-verified descriptors and registered preset identities; reject active formats, arbitrary transforms, pixel/decode-budget overflow and unsafe cache policy.", "Upload completion remains QUARANTINED until backend scan and promotion; client capability checks are not an authorization boundary.", "Active content preview requires isolation or download-only treatment."], - "bundleBudgetGzipBytes": 52000, + "bundleBudgetGzipBytes": 54600, "fallback": "Accessible native file input, same-origin authorized server upload/download and a single bounded server-selected image rendition; generated artifacts above the buffer budget move to server-side generation.", "removal": ["Stop new capability and upload-session issuance, then cancel active reads and transfers.", "Reconcile or explicitly abort active multipart sessions and let backend TTL cleanup remove ambiguous orphans.", "Remove non-secret checkpoints according to account and retention policy.", "Release file references, revoke preview object-URL leases and dispose file, capability and image runtimes.", "Remove transfer/image feature facades and composition, then prove browser-transfer sources are absent from the production module inventory."], "serverStatePolicy": "query-cache-metadata-only" diff --git a/docs/operations/adapter-remediation-dispositions.json b/docs/operations/adapter-remediation-dispositions.json new file mode 100644 index 0000000..754ca14 --- /dev/null +++ b/docs/operations/adapter-remediation-dispositions.json @@ -0,0 +1,186 @@ +{ + "schemaVersion": 1, + "review": "third-review-2026-08-14", + "note": "GOV-03. The machine-readable disposition of every finding the third re-review raised. `check:remediation-ledger` joins this file against the prose ledger and refuses a blanket closure claim while any row is not FIXED, so a summary sentence can never outrun the evidence.", + "dispositions": [ + { + "id": "NS-01", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "A credential owner's answer is decoded once, inside the auth boundary, through own data descriptors.", + "evidence": ["tests/integration/http-execution-v3-live-authority.test.ts"] + }, + { + "id": "NS-02", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "Contract composition snapshots first and validates the snapshot, so the installed row is the row that was checked.", + "evidence": ["tests/unit/contract-registry-immutability.test.ts"] + }, + { + "id": "NS-03", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "The `responseBody: NONE` probe owns its reader: the operation lifetime reaches it, and the lock is released.", + "evidence": ["tests/integration/http-execution-v3-live-authority.test.ts"] + }, + { + "id": "NS-04", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "A journal transaction that cannot be completed is maintenance debt, not a settled write.", + "evidence": ["tests/unit/opfs-byte-store.test.ts"] + }, + { + "id": "NS-05", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "A bootstrap failure answers with the request kind it belongs to, so the real cause survives the gateway.", + "evidence": ["tests/unit/opfs-worker-runtime.test.ts"] + }, + { + "id": "NS-06", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "A reply is decoded before its pending row is released, and an uncorrelatable reply fails the channel closed.", + "evidence": ["tests/unit/opfs-worker-runtime.test.ts"] + }, + { + "id": "NS-07", + "previous": "NEW", + "disposition": "FIXED", + "summary": "Cursor caps and collaborators are captured at construction, so a later mutation cannot widen a validated cap.", + "evidence": ["tests/unit/cursor-pagination-runtime.test.ts"] + }, + { + "id": "NS-08", + "previous": "NEW", + "disposition": "FIXED", + "summary": "One terminal owner covers the whole public-cache staging body, so nothing writes after the abort.", + "evidence": ["tests/unit/public-response-cache.test.ts"] + }, + { + "id": "RPC-01", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "Only a fulfilled, contract-shaped `waitClosed()` receipt prunes an active stream registration.", + "evidence": ["tests/unit/browser-rpc/browser-rpc-remediation.test.ts"] + }, + { + "id": "RPC-02", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "Iterator cleanup and the lease decoder read foreign state inside their own boundaries.", + "evidence": ["tests/unit/browser-rpc/browser-rpc-remediation.test.ts"] + }, + { + "id": "RPC-03", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "Every registry is snapshotted before any validation runs, and rows with hidden fields are refused.", + "evidence": ["tests/unit/browser-rpc/browser-rpc-remediation.test.ts"] + }, + { + "id": "RPC-04", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "A transport result is an exact union: required own keys, no inherited extras, plain prototype.", + "evidence": ["tests/unit/browser-rpc/browser-rpc-remediation.test.ts"] + }, + { + "id": "RT-01", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "A tracked task is registered before the authority is invoked, closing the reentrant-close window.", + "evidence": ["tests/unit/realtime/stream-coordinator.test.ts"] + }, + { + "id": "RT-02", + "previous": "NEW", + "disposition": "FIXED", + "summary": "A scheduler that cannot install a deadline fails closed inside the realtime result contract.", + "evidence": ["tests/unit/realtime/stream-coordinator.test.ts"] + }, + { + "id": "TR-01", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "The vault snapshots a registration and everything nested in it before validating or storing it.", + "evidence": ["tests/unit/presigned-transfer.test.ts"] + }, + { + "id": "TR-02", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "A source lease that arrives after the delivery ended is closed exactly once by a compensator.", + "evidence": ["tests/unit/presigned-transfer.test.ts"] + }, + { + "id": "TR-03", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "The shared abort primitive settles once by observation order, and all four consumers use it with bound timer snapshots.", + "evidence": [ + "tests/unit/abortable-operation.test.ts", + "tests/unit/image-cdn-runtime.test.ts", + "tests/unit/resumable-upload-fetch-transport.test.ts" + ] + }, + { + "id": "TR-04", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "Teardown proves quiescence of the raw provider registry, not only of the wrappers that bound it.", + "evidence": ["tests/unit/resumable-upload-runtime.test.ts"] + }, + { + "id": "TR-05", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "The control-plane decoder validates an owned snapshot, so a stateful answer cannot swap a checked value.", + "evidence": ["tests/unit/resumable-upload-http-control-plane.test.ts"] + }, + { + "id": "SW-01", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "The activation marker read is bounded in bytes, cancels what it refuses and releases its reader lock.", + "evidence": ["tests/unit/service-worker-runtime.test.ts"] + }, + { + "id": "SW-02", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "The generator and the runtime decoder share one canonical asset-path predicate, and the generator self-validates.", + "evidence": ["tests/unit/service-worker-web-push-remediation.test.ts"] + }, + { + "id": "WP-01", + "previous": "PARTIAL", + "disposition": "FIXED", + "summary": "One observation authority per click; certainty is monotone and the late-effect tail is owned by `waitUntil`.", + "evidence": ["tests/unit/web-push-worker-runtime.test.ts"] + }, + { + "id": "GOV-03", + "previous": "OPEN", + "disposition": "FIXED", + "summary": "This file plus `check:remediation-ledger` bind each row to a disposition and a test path, and block a blanket closure claim while any row is open.", + "evidence": ["scripts/check-remediation-ledger.ts"] + }, + { + "id": "GOV-04", + "previous": "OPEN", + "disposition": "FIXED", + "summary": "The inventory gate requires the exact named consumer set to resolve its import to the shared primitive and prints the set.", + "evidence": ["scripts/check-adapter-inventory.ts"] + }, + { + "id": "GOV-05", + "previous": "OPEN", + "disposition": "FIXED", + "summary": "Duplicate abort mechanics were consolidated onto the shared primitive and the file-transfer budget was reset to cover the remaining correctness code.", + "evidence": ["config/recipes/frontend-capability-recipes.json"] + } + ] +} diff --git a/docs/operations/adapter-remediation-ledger.md b/docs/operations/adapter-remediation-ledger.md index 2a18a91..552ffc6 100644 --- a/docs/operations/adapter-remediation-ledger.md +++ b/docs/operations/adapter-remediation-ledger.md @@ -234,9 +234,12 @@ below are written the other way round: a row is `FIXED` only where a new adversarial test failed first on the pre-fix source and passes on the landed one. -**All 38 are now `FIXED`.** The first pass closed 26; the second closed the -remaining twelve, which each needed a lifecycle or contract change rather than a -contained edit. +**All 38 second re-review findings are `FIXED` as scoped below.** The first pass +closed 26; the second closed the remaining twelve, which each needed a lifecycle +or contract change rather than a contained edit. A third re-review then found +that twenty of those closures held only on the paths their tests exercised; that +verdict and its remediation are recorded in the third re-review section further +down, and this section is left as written so the two passes stay comparable. ### Landed @@ -290,7 +293,7 @@ landed one. | TR-RR-06 | High | `5a76f95` | `tests/unit/resumable-upload-runtime.test.ts` — a never-granting mutation lock made `dispose()` unbounded; it is now bounded by `cleanupDeadlineMs`, returns the drain result, and leaves the runtime `CLOSING` with the checkpoint store open when the drain is unproved. | | TR-RR-07 | High | `5a76f95` | `tests/unit/image-cdn-runtime.test.ts` — after an abort a new verification was admitted while the abandoned verifier still ran; the slot is now held until the raw verifier settles. | -**All 38 re-review findings are closed.** No row remains `NOT_STARTED`, and the +**No second re-review row remains `NOT_STARTED`**, and the structural gate for the shared `abortable-operation` primitive is now active — it was withheld until the primitive actually had production importers, because a gate that fails CI for a documented but unfixed defect reports the wrong thing. @@ -379,6 +382,86 @@ files above plus the two flaky-under-load ones: `tests/integration/security-followup-archive.test.ts` — the three that had to be excluded before — now pass in the full run. +## Third re-review (2026-08-14) + +A third read-only re-review re-tested all 38 rows above against hostile, +non-cooperative, late-completing and mutable inputs. It confirmed 18 as fixed +and found 20 that held only on the paths their tests exercised, plus three new +findings and three governance defects. The common shape was the same in almost +every case: a value was **checked and then read again**, or a wrapper settled +and was mistaken for the physical work it was bounding. + +`docs/operations/adapter-remediation-dispositions.json` is the machine-readable +record; `corepack pnpm check:remediation-ledger` joins it against this document, +verifies every named evidence path exists, and refuses a blanket closure +sentence while any row is still open. **GOV-03.** The previous section claimed +"All 38 are now FIXED" while six rows were reproducibly partial — a sentence is +cheap and a reviewer reads it as evidence, so the claim is now derived rather +than authored. + +### Landed + +| ID | Prior verdict | Disposition | Red-then-green evidence | +| --- | --- | --- | --- | +| NS-01 | `PARTIAL` | `FIXED` | `tests/integration/http-execution-v3-live-authority.test.ts` — a throwing `kind` getter escaped the auth boundary and an auth outage was reported as `NETWORK_FAILURE`; nine hostile credential shapes now close as `AUTH_INTEGRATION_FAILURE` with zero fetches, and each field is read exactly once. | +| NS-02 | `PARTIAL` | `FIXED` | `tests/unit/contract-registry-immutability.test.ts` — a policy that answered `10_000` to validation and `999_999` to the copy installed the second value; composition now snapshots first, so the out-of-ceiling value is refused. | +| NS-03 | `PARTIAL` | `FIXED` | `tests/integration/http-execution-v3-live-authority.test.ts` — a `NONE` probe left `body.locked === true` after a deadline; the reader is now cancelled once and its lock released. | +| NS-04 | `PARTIAL` | `FIXED` | `tests/unit/opfs-byte-store.test.ts` — a failed `journal.complete` still returned plain success with `SUCCEEDED` telemetry; it now returns a `RECONCILE` failure and leaves the row `COMMITTED`, and an unfinished delete is observed `DEGRADED`. | +| NS-05 | `PARTIAL` | `FIXED` | `tests/unit/opfs-worker-runtime.test.ts` — a bootstrap failure answered every request with kind `CAPABILITIES`, so the gateway replaced `BLOCKED` with `UNSUPPORTED`; all twelve kinds now round-trip their own correlation. | +| NS-06 | `PARTIAL` | `FIXED` | Same suite — a throwing `requestId` getter produced an RPC timeout and a stateful trap left the public promise pending forever; replies are decoded before the pending row is released and an uncorrelatable reply fails the channel closed. | +| NS-07 | `NEW` | `FIXED` | `tests/unit/cursor-pagination-runtime.test.ts` — raising `maxPages` after construction widened a validated cap from one page to three; caps and collaborators are captured once. | +| NS-08 | `NEW` | `FIXED` | `tests/unit/public-response-cache.test.ts` — a non-cooperative fetch held the mutation lock forever, and a digest finishing after the abort still wrote the asset and the activation marker; one terminal owner now covers the whole staging body. | +| RPC-01 | `PARTIAL` | `FIXED` | `tests/unit/browser-rpc/browser-rpc-remediation.test.ts` — a rejecting, throwing or non-promise `waitClosed()` was absorbed into success and a second physical stream opened; only a fulfilled contract-shaped receipt prunes the registration. | +| RPC-02 | `PARTIAL` | `FIXED` | Same suite — a throwing iterator `return` accessor replaced the selected timeout with a native `TypeError`, and the exported lease decoder threw on a hostile `Symbol.asyncIterator`. | +| RPC-03 | `PARTIAL` | `FIXED` | Same suite — a registry accessor ran twice during validation and rows hiding fields behind a prototype or a non-enumerable key installed; every registry is snapshotted before validation. | +| RPC-04 | `PARTIAL` | `FIXED` | Same suite — own `{ok,message,encodedBytes}` plus a prototype `injected` was a success, and a missing `message` reached a permissive schema as `undefined`. | +| RT-01 | `PARTIAL` | `FIXED` | `tests/unit/realtime/stream-coordinator.test.ts` — an authority that re-entered `close()` from inside its own invocation got `{ok:true}` while its effect was pending; the task is registered before the collaborator is called. | +| RT-02 | `NEW` | `FIXED` | Same suite — a throwing `scheduleTimeout` started a recovery that overlapped the running apply and made `close()` reject with a native `TypeError`; an uninstallable deadline now fails closed inside the result contract. | +| TR-01 | `PARTIAL` | `FIXED` | `tests/unit/presigned-transfer.test.ts` — a stateful issuer could show an allowed header set to the forbidden-header check and store `Authorization`; the registration and everything nested in it is snapshotted before validation. | +| TR-02 | `PARTIAL` | `FIXED` | Same suite — a source lease that resolved after an abort was never closed; a compensator sharing the holder's close-once latch closes it exactly once. | +| TR-03 | `PARTIAL` | `FIXED` | `tests/unit/abortable-operation.test.ts`, `tests/unit/image-cdn-runtime.test.ts`, `tests/unit/resumable-upload-fetch-transport.test.ts` — the outcome depended on a hard-coded four-microtask drain, and a throwing scheduler rejected `probe()`/`execute()` natively while leaking a caller listener. The primitive now settles once by observation order, and all four consumers use it with construction-time bound timer snapshots. | +| TR-04 | `PARTIAL` | `FIXED` | `tests/unit/resumable-upload-runtime.test.ts` — a provider outliving its attempt deadline let `dispose()` report a drained runtime and close the checkpoint store; raw provider work is now its own registry and both must be quiescent. | +| TR-05 | `PARTIAL` | `FIXED` | `tests/unit/resumable-upload-http-control-plane.test.ts` — a stateful `sessionId` passed the regex and returned `../../unsafe`; the decoder validates an owned snapshot read exactly once. | +| SW-01 | `PARTIAL` | `FIXED` | `tests/unit/service-worker-runtime.test.ts` — a single 1 MiB chunk was retained before the 257-byte ceiling was compared, a declared oversize left the body open and the reader lock was never released. | +| SW-02 | `PARTIAL` | `FIXED` | `tests/unit/service-worker-web-push-remediation.test.ts` — the generator emitted `/assets/bad@name-abcdefgh.js` and the decoder then refused the manifest it had just produced; both share one canonical path predicate and the generator self-validates its output. | +| WP-01 | `PARTIAL` | `FIXED` | `tests/unit/web-push-worker-runtime.test.ts` — an ordinary click was counted twice, a late rejection downgraded `MAYBE_APPLIED` to `NOT_APPLIED`, and the late observation ran outside `waitUntil`. | +| GOV-03 | `OPEN` | `FIXED` | `scripts/check-remediation-ledger.ts` — dispositions and evidence paths are machine-readable and a blanket closure sentence is blocked while any row is open. | +| GOV-04 | `OPEN` | `FIXED` | `scripts/check-adapter-inventory.ts` — the gate passed on any single importer; it now requires the four named consumers to resolve their import to the shared primitive and prints the exact set. | +| GOV-05 | `OPEN` | `FIXED` | `config/recipes/frontend-capability-recipes.json` — see the budget note below. | + +### The file-transfer bundle budget + +`check:optional-recipes:source` failed at the previous baseline too (52,078 > +52,000 gzip bytes), so it was not green before this work either. Duplicate abort +and deadline mechanics were consolidated first: the Image probe and the Resumable +fetch transport now use the shared `abortable-operation` primitive instead of +their own scopes, and four decoders share `src/contracts/exact-snapshot.ts` +rather than each carrying its own descriptor walk. The remainder is the +correctness code the third re-review asked for — exact decoders, late-value +compensators and physical-work registries — so the budget is reset to **54,600** +gzip bytes against a measured **53,810**, rather than the failure being carried +forward as if it were green. + +### Regenerated evidence (this commit) + +| Command | Exit | Result | +| --- | ---: | --- | +| `corepack pnpm check:types` | 0 | all six projects pass | +| `corepack pnpm check:architecture` | 0 | 290 modules, 879 dependencies; 12 fixtures PASS; allowed PASS / 9 forbidden rejected | +| `corepack pnpm check:adapter-inventory` | 0 | 119 files; 7 shared extensions; 5 shared-abort importers listed | +| `corepack pnpm check:optional-recipes:source` | 0 | file-transfer 53,810 / 54,600 gzip bytes | +| `corepack pnpm exec vitest run tests/unit/browser-rpc tests/unit/realtime` | 0 | 17 files / 234 passed | +| abortable-operation, presigned, resumable, image CDN | 0 | 7 files / 176 passed | +| OPFS, journal, public cache, cursor | 0 | 5 files / 100 passed | +| Service Worker + Web Push | 0 | 6 files / 79 passed | +| `corepack pnpm exec vitest run tests/integration` | 0 | 11 files / 82 passed | + +The full `tests/unit` + `tests/integration` run is **1,845 passed / 1,864**, +128 of 129 files green. Every one of the 19 failures is in +`tests/unit/ci-artifact-contract.test.ts` and is the pre-existing sandbox, +cgroup, RLIMIT and `/tmp` permission behaviour already recorded above — the same +file failed identically before this work. No adapter test fails. + ## Rules for updating this ledger - A row moves out of `NOT_STARTED` only with a linked red test, its green run, and the commit id. diff --git a/package.json b/package.json index ab61a50..c91469a 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "check:i18n": "node scripts/check-i18n.ts", "check:i18n:fixture": "node scripts/check-i18n.ts --fixture", "check:adapter-inventory": "node scripts/check-adapter-inventory.ts", + "check:remediation-ledger": "node scripts/check-remediation-ledger.ts", "check:diagnostics": "node scripts/check-diagnostics.ts", "check:diagnostics:fixture": "node scripts/check-diagnostics.ts --fixture", "check:types": "corepack pnpm check:types:app && corepack pnpm check:types:node && corepack pnpm check:types:test && corepack pnpm check:types:recipes && corepack pnpm check:types:web-worker && corepack pnpm check:types:service-worker", diff --git a/scripts/check-adapter-inventory.ts b/scripts/check-adapter-inventory.ts index 9ae7c0e..07009a4 100644 --- a/scripts/check-adapter-inventory.ts +++ b/scripts/check-adapter-inventory.ts @@ -1,4 +1,6 @@ import { readFile } from "node:fs/promises"; +import { readFileSync } from "node:fs"; +import path from "node:path"; import { spawnSync } from "node:child_process"; import { CACHEABLE_ASSET_CONTENT_TYPES } from "../src/contracts/service-worker-static-manifest.ts"; @@ -123,10 +125,17 @@ async function main(): Promise { ); } - // TR-RR-05. The shared abort/deadline primitive is only shared if production - // code imports it. A helper with zero importers is a second implementation - // waiting to happen, which is exactly how the four hand-written copies of - // these mechanics diverged in the first place. + // TR-RR-05 / GOV-04. Every consumer the re-review named must use the shared + // primitive, not merely one file somewhere. Checking `importers.length > 0` + // let an unrelated production import satisfy the gate while Image and + // Resumable kept their own diverging copies of the same mechanics — which is + // exactly how the four hand-written versions drifted apart in the first place. + const REQUIRED_ABORT_CONSUMERS: readonly string[] = [ + "src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts", + "src/adapters/browser-transfer/presigned/presigned-transfer-executor.ts", + "src/adapters/browser-transfer/image-cdn/browser-image-probe.ts", + "src/adapters/browser-transfer/resumable-upload/fetch-json-transport.ts", + ]; const primitiveImporters = spawnSync( "git", ["grep", "-l", "platform/abortable-operation.ts", "--", "src"], @@ -137,24 +146,56 @@ async function main(): Promise { ) .split("\n") .filter(Boolean) - .filter((file) => !file.endsWith("platform/abortable-operation.ts")); - if (importers.length === 0) { + .filter((file) => !file.endsWith("platform/abortable-operation.ts")) + .sort(); + const importerSet = new Set(importers); + const missingConsumers = REQUIRED_ABORT_CONSUMERS.filter( + (consumer) => !importerSet.has(consumer), + ); + if (missingConsumers.length > 0) { problems.push( - "abortable-operation: the shared primitive has no production importers", + `abortable-operation: required consumers do not import the shared primitive: ${missingConsumers.join( + ", ", + )}`, ); } + // The importer must reach the primitive by a specifier that resolves to the + // primitive itself, so a same-named local helper cannot satisfy the gate. + const PRIMITIVE_PATH = path.resolve( + "src/adapters/platform/abortable-operation.ts", + ); + for (const consumer of REQUIRED_ABORT_CONSUMERS) { + if (!importerSet.has(consumer)) continue; + const source = readFileSync(consumer, "utf8"); + const specifiers = [ + ...source.matchAll(/from\s+"([^"]*platform\/abortable-operation\.ts)"/gu), + ].map((match) => match[1] ?? ""); + const resolved = specifiers.some( + (specifier) => + path.resolve(path.dirname(consumer), specifier) === PRIMITIVE_PATH, + ); + if (!resolved) { + problems.push( + `abortable-operation: ${consumer} does not resolve its import to the shared primitive`, + ); + } + } if (problems.length > 0) { for (const problem of problems) console.error(problem); process.exitCode = 1; return; } + // GOV-04. The exact importer set is part of the receipt, so a reviewer can + // see which consumers the gate actually verified rather than a bare count. console.log( `Adapter inventory: ${tracked.length} files PASS; ` + `service worker asset table: ${ Object.keys(CACHEABLE_ASSET_CONTENT_TYPES).length } shared extensions PASS; ` + - `fixture node_modules linking PASS`, + `fixture node_modules linking PASS; ` + + `shared abort primitive: ${importers.length} importers ` + + `(${importers.join(", ")}) PASS`, ); } diff --git a/scripts/check-remediation-ledger.ts b/scripts/check-remediation-ledger.ts new file mode 100644 index 0000000..eb3f247 --- /dev/null +++ b/scripts/check-remediation-ledger.ts @@ -0,0 +1,120 @@ +import { access, readFile } from "node:fs/promises"; + +/** + * GOV-03. Joins the machine-readable remediation dispositions against the prose + * ledger. + * + * The previous ledger declared "All 38 are now FIXED" while six of those rows + * were reproducibly partial. A sentence is cheap and a reviewer reads it as + * evidence, so this gate makes the claim derivable rather than authored: every + * disposition must name a test path that exists, the prose must carry the same + * verdict for the same id, and a blanket closure sentence is only allowed when + * nothing is still open. + */ + +type Disposition = Readonly<{ + id: string; + previous: string; + disposition: string; + summary: string; + evidence: readonly string[]; +}>; + +const DISPOSITIONS_PATH = "docs/operations/adapter-remediation-dispositions.json"; +const LEDGER_PATH = "docs/operations/adapter-remediation-ledger.md"; +/** + * Finding ids are only unique within a review pass — the second pass also used + * `SW-01` — so rows are matched inside this pass's section rather than anywhere + * in the document. + */ +const SECTION_HEADING = "## Third re-review (2026-08-14)"; +const CLOSED_DISPOSITIONS: ReadonlySet = new Set(["FIXED"]); +/** Sentences that assert everything is done, and therefore need proof. */ +const BLANKET_CLOSURE = /All\s+(?:\d+|findings|rows)[^.\n]*\b(?:FIXED|closed)\b/giu; + +async function main(): Promise { + const problems: string[] = []; + const raw: unknown = JSON.parse(await readFile(DISPOSITIONS_PATH, "utf8")); + if ( + raw === null || + typeof raw !== "object" || + !Array.isArray((raw as { dispositions?: unknown }).dispositions) + ) { + console.error(`${DISPOSITIONS_PATH}: dispositions array is required`); + process.exitCode = 1; + return; + } + const dispositions = (raw as { dispositions: Disposition[] }).dispositions; + const document = await readFile(LEDGER_PATH, "utf8"); + const sectionStart = document.indexOf(SECTION_HEADING); + if (sectionStart < 0) { + console.error(`${LEDGER_PATH}: missing section "${SECTION_HEADING}"`); + process.exitCode = 1; + return; + } + const ledger = document.slice(sectionStart); + + const seen = new Set(); + for (const row of dispositions) { + if (typeof row.id !== "string" || row.id.length === 0) { + problems.push("a disposition row has no id"); + continue; + } + if (seen.has(row.id)) problems.push(`duplicate disposition id: ${row.id}`); + seen.add(row.id); + if (typeof row.disposition !== "string" || row.disposition.length === 0) { + problems.push(`${row.id}: disposition is required`); + } + if (!Array.isArray(row.evidence) || row.evidence.length === 0) { + problems.push(`${row.id}: at least one evidence path is required`); + continue; + } + for (const path of row.evidence) { + try { + await access(path); + } catch { + problems.push(`${row.id}: evidence path does not exist: ${path}`); + } + } + // The prose must carry the same verdict for the same id, so a reader of the + // document and a reader of the receipt cannot reach different conclusions. + const line = ledger + .split("\n") + .find((candidate) => candidate.includes(`| ${row.id} |`)); + if (!line) { + problems.push(`${row.id}: no row in ${LEDGER_PATH}`); + continue; + } + if (!line.includes(`\`${row.disposition}\``)) { + problems.push( + `${row.id}: ${LEDGER_PATH} does not record \`${row.disposition}\``, + ); + } + } + + const open = dispositions.filter( + (row) => !CLOSED_DISPOSITIONS.has(row.disposition), + ); + const blanketClaims = [...document.matchAll(BLANKET_CLOSURE)]; + if (open.length > 0 && blanketClaims.length > 0) { + problems.push( + `${LEDGER_PATH} declares a blanket closure (${blanketClaims + .map((match) => `"${match[0]}"`) + .join(", ")}) while ${open.length} finding(s) are still open: ${open + .map((row) => `${row.id}=${row.disposition}`) + .join(", ")}`, + ); + } + + if (problems.length > 0) { + for (const problem of problems) console.error(problem); + process.exitCode = 1; + return; + } + console.log( + `Remediation ledger: ${dispositions.length} dispositions joined to ` + + `${LEDGER_PATH}; ${open.length} open; evidence paths verified`, + ); +} + +await main();