Skip to content

fix(scripts): ADR-0087 runtime-interface-only reads CODE, not docblock prose (#12881) - #12997

Merged
hotlong merged 5 commits into
mainfrom
claude/issue-12881-adr0087-comment-blind-spot
Aug 28, 2026
Merged

fix(scripts): ADR-0087 runtime-interface-only reads CODE, not docblock prose (#12881)#12997
hotlong merged 5 commits into
mainfrom
claude/issue-12881-adr0087-comment-blind-spot

Conversation

@hotlong

@hotlonghotlong commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Fixes#12881

Step 4 of the runtime-interface-only predicate ("NO metadata surface REFERENCES it") is a text scan over whole metadata-surface files, so it counted a symbol named in a JSDoc prose sentence as a reference it could not resolve, and refused the disposition.

The measurement

packages/spec/src/api/contract.zod.ts line 164 explains, inside BaseResponseSchema's docblock, what ObjectStackClient.unwrapResponse keys on. ObjectStackClient is not imported there, is in no schema, and objectstack migrate meta has nothing to reach through a sentence. It was nevertheless the only .zod.ts in the tree naming it, so a steps-1-to-3-clean claim on packages/client/src/index.ts was refused.

The cost is not one refused claim. The remaining categories are unpublished, already-registered and no-migration-prescription; for a published rename carrying a real before/after table the first two are false and the third is a self-contradiction. The blind spot's effect is to push an honest disposition out and a dishonest one in — the #8299 anti-pattern this block exists to close, arriving through the door #8299 built.

Live incidence is 2 of the 3 runtime-interface-only claims in the current stock, not the 1 the card measured. packages/services/service-datasource/.../datasource-driver-factory.ts#DatasourceDriverHandle was refused by the same shape — a docblock in packages/spec/src/contracts/data-driver.ts:373 naming it while explaining a #11381 typing decision.

The change

Mask comment spans through the shared scripts/js-comment-mask.mjs before every read step 4 performs — the mention scan, declaresLocally, importSpecifiersFor, and the evidence line it quotes back. maskComments rather than stripComments, per that module's own rule: this branch reports a line, and blanking is the projection that keeps line numbers aligned with the file on disk. A private stripper here would be the drift check:comment-mask-adoption exists to refuse.

String literals are deliberately not masked. A name inside a .describe() still refuses: the conservative direction here is to refuse (#4690), and a string is where an authorable value lives. That boundary is pinned in the self-test so a future widening has to be argued rather than slipped in.

The teeth are unchanged, and both directions are pinned

Self-test 212 assertions to 228. RIO-R3 was converted from a comment mention to a code-level mention — it had been a fixture for two rules at once, and keeping the comment text there would have meant deleting the rule it was written for.

new caseasserts
RIO-G2the #12881 shape: a docblock sentence in a *.zod.ts is not a reference
RIO-G3the same for a line comment in an object definition (RIO-R3's old text, asserted the other way)
RIO-G4a commented-out import is not an import
RIO-R10prose does not launder a CODE reference in the same file
RIO-R11prose beside a real import still refuses on the import
RIO-R12a mention inside a string literal is still a reference

Ablation on e44d54a59: reverting only the two masking lines (mutation confirmed on disk — 0 occurrences of the deleted text, 1 of the injected text — and the tree restored byte-identically to the HEAD blob, git diff HEAD clean) turns RIO-G2, G3, G4 red and moves RIO-R10's evidence line to the docblock, 4 failures. RIO-R3/R11/R12 stay red on their own reasons, as they should.

Verdicts elsewhere are unchanged, measured rather than argued

  • --list and --audit-stock over the live stock: byte-identical before and after.
  • Blast radius over the real corpus — 385 metadata surfaces at HEAD, 384 carrying comments, 2,746,193 comment bytes masked: 0 import-shaped lines and 0 declaration-shaped lines exist only inside a comment anywhere in the tree. So on today's tree the other three reads in step 4 provably cannot move; only the mention scan does, which is the intended change.
  • Acceptance probe: PR ADR-0006 D2 (SDK half): rename client.projects.* to client.environments.*, unwrap keys follow the wire, JSDoc names the endpoint (#12866) #12885's changeset, run the way the Check Changeset step runs it, goes from exit 1 (the step-4 refusal, verbatim from the card) to exit 0, verified: packages/client/src/index.ts#ObjectStackClient (class).
  • Gate runtime over the whole surface corpus for all 3 live claims: 0.5s.

One thing the reviewer should know

#12885's changeset also carries a ## Migration section with rewrite tables, and runtime-interface-only inherits the prescription refusal by design. That arm did not fire — findMigrationPrescription returns null on that body, because a framed region closes at the next heading of any level and the tables sit under ### sub-headings. So #12885 now passes this gate, but the prescription arm was never consulted. That is a separate blind spot with its own stock-wide blast radius; filed as #12996 rather than fixed here.

Gates

Union run at e44d54a59, exit codes captured before any pipe. All derived families green, plus both convention-triggered obligations for editing a gate script (bare-root-worklist --self-test, check:pm-dispatch-gates) and the mask module's own gates. Verdict lines quoted from the gates themselves:

check-adr-0087-registration --self-test: 228 assertions over real temp git repos (real scan()/assertInputs() path)
check-adr-0087-registration: this PR adds no declared-breaking changeset (0 non-breaking changeset(s) seen).
check:comment-mask-adoption - 14 private comment-stripper(s), all 14 recorded and every recorded row still reached. A new one reds here.

check:bash32-floor fails here and is not caused by this diff: it fails identically (8 of 153 cases, at the simulated-3.2 harness leg) on a pristine origin/main checkout with this change absent. This diff contains no shell.

Scripts-only, publishes nothing, so skip-changeset rather than a changeset.

Patch round — one CI red the local union missed

Lint & Repo Gates failed in check:objectui-changeset. objectui-changeset-digest.mjs's self-test stages an executable copy of check-adr-0087-registration.mjs into a throwaway repo at fw-gate/ and runs it for real, so the new ./js-comment-mask.mjs import died on ERR_MODULE_NOT_FOUND there and took both #6494 ROUND TRIP cases (red leg and green leg) down with it — reading as a broken round trip when nothing about the round trip had moved.

Fixed the manifest, not the import: the harness's single hard-coded invoked-as.mjs line becomes a list, the same spelling this gate's own I1/I2 fixture already uses. Two staging sites of one gate, now one convention. scripts/check-empty-changeset.mjs also names this gate, but it reads the file's TEXT for a parser-parity assertion and never executes a copy, so it needed nothing.

Reproduced red locally before the fix and green after:

before: EXIT=1 x #6494 ROUND TRIP (red) ... x #6494 ROUND TRIP (green): ... status=1
after: EXIT=0 objectui-changeset-digest --self-test: all checks passed

Full union re-run at 8a7db35d2 after merging current main; every family above still green, plus check:objectui-changeset.

Why the derivation missed it (input for the gate-mapping card, measured not guessed): check:objectui-changeset declares exactly one population — .changeset — so dispatch-gates --residue scores it silent for a diff touching only scripts/check-adr-0087-registration.mjs. It matched only once I edited the digest itself, by gate-script identity. The dependency it really has is a runtime staging dependency — one gate copying another gate's source into a sandbox and executing it — which no path glob declares and no existing convention-trigger names. The "adds or edits a GATE SCRIPT" trigger fires on the edited gate's own families, not on the families of gates that stage a copy of it.

Generated by Claude Code

…k prose (#12881)
Step 4 of the `runtime-interface-only` predicate scanned whole metadata-surface
files for the claimed symbol, so a JSDoc sentence naming a runtime type counted
as an unresolvable reference and refused the disposition. Measured: the only
`.zod.ts` in the tree naming `ObjectStackClient` is contract.zod.ts, which
explains what `unwrapResponse` keys on in one docblock line.
Mask comment spans with the shared scripts/js-comment-mask.mjs before every read
step 4 performs. Strings stay unmasked on purpose. Self-test gains both
directions: RIO-G2/G3/G4 (prose, line comment, commented-out import) and
RIO-R10/R11/R12 (prose beside a code reference, beside a real import, and a
string literal), plus RIO-R3 converted to a code-level mention.
)
The digest self-test stages an executable COPY of check-adr-0087-registration.mjs
into a throwaway repo and runs it. That gate gained a ./js-comment-mask.mjs
import, which the sandbox manifest did not copy, so both #6494 ROUND TRIP cases
died on ERR_MODULE_NOT_FOUND and read as a broken round trip.
Fix the manifest, not the import: the single hard-coded invoked-as.mjs line
becomes a list, matching how the gate's own I1/I2 fixture spells it.
@hotlong

Copy link
Copy Markdown
ContributorAuthor

Re-enqueued by the epic PM: this PR's own merge-queue batch runs (13:52Z) were ALL SUCCESS (CI, Lint & Type Check, Spec Liveness Check, Governed Surface Guard); the entry was dropped when the shared batch was rebuilt around a sibling PR's failure — batch-victim shape, resubmitted as-is per the landing playbook.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check-adr-0087-registration refuses a runtime-interface-only disposition on a symbol named only in a JSDoc comment inside a .zod.ts

1 participant

@hotlong