Repository navigation
check-adr-0087-registration refuses a runtime-interface-only disposition on a symbol named only in a JSDoc comment inside a .zod.ts #12881
Description
Activity
Claim: PM loop round 4 (maintainer direct-dispatch channel; epic #12865's landing is the named consumer)
Authorization, verbatim from the maintainer's batched ruling in live PM chat, 2026-08-28: 「1 同意 2 不挂 major 3 可以 4 推」 — item 1 approves dispatching this card and #12910 from this seat.
Session:session_65d2faee-1ff4-4be8-be28-72b972c539d9
Branch:claude/issue-12881-adr0087-comment-blind-spot
Worktree:objectstack-issue-12881
Domain: gate tooling under scripts/ — dispatched via the direct channel above; triage may still label
File surface:scripts/check-adr-0087-registration.mjsand its self-test (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus (gate predicate semantics — judgment sits in the checker, not the contract)
Clause-②: no
(Gate script; no contract file touched, no accept/reject behavior of the spec changes.)
Serial constraints cleared: no open PR touches check-adr-0087-registration.mjs (re-derived this dispatch across all open PRs). Read-coupling: PR #12885's Check Changeset red is the live reproduction this fix must turn green — the honestruntime-interface-onlydisposition on its changeset must pass once the predicate stops counting comment-prose mentions.Claim: os-dev agent starting implementation on this card.
Session:
session_65d2faee-1ff4-4be8-be28-72b972c539d9
Branch:claude/issue-12881-adr0087-comment-blind-spot(pushed empty as a write-route probe before the first edit)
Worktree:objectstack-issue-12881, cut from freshorigin/mainat15d55fb2430f374c97a8a4d9914e9a668758d9f2File surface:
scripts/check-adr-0087-registration.mjsplus its self-test. Fixing the PREDICATE, not the data —packages/spec/**is out of bounds for this card and its prose mention of ObjectStackClient stays exactly as it is.Generated by Claude Code
os-dev-report
{ "issue": 12881, "status": "done", "branch": "claude/issue-12881-adr0087-comment-blind-spot", "pr": "https://github.com/objectstack-ai/objectstack/pull/12997", "premise_still_valid": true, "summary": "Step 4 of the runtime-interface-only predicate now masks comment spans through the shared scripts/js-comment-mask.mjs before every read it performs (mention scan, declaresLocally, importSpecifiersFor, and the evidence line it quotes back), so a symbol named only in a JSDoc sentence is no longer an unresolvable reference. maskComments rather than stripComments, per that module's own rule, because this branch reports a line. String literals are deliberately NOT masked and still refuse, pinned in both directions. Predicate fixed, not data: packages/spec is untouched. Two readings the card did not have. (1) Live incidence in the stock is 2 of 3 runtime-interface-only claims, not 1 - DatasourceDriverHandle was refused by the same shape, a docblock at packages/spec/src/contracts/data-driver.ts:373. (2) PR 12885's changeset also carries a Migration section with rewrite tables, and this category INHERITS the prescription refusal by design - that arm did not fire, because findMigrationPrescription closes a framed region at the next heading of any level and those tables sit under sub-headings. So 12885 passes this gate, but the prescription arm was never consulted; that is a separate blind spot, filed as 12996 rather than fixed here. Also recorded the new rule on the ADR-0087 anchor JSON (scripts/adr-anchors/ is not a governed surface; docs/adr was deliberately not edited, since CATEGORIES is unchanged and the gate header is the authority on detail - keeping this PR ungoverned and normally landable).", "tests": "Union re-run at final head e44d54a59, exit codes captured before any pipe. GREEN: check:adr-anchors, check:agent-test-spelling, check:changeset-gate-self-tests, check:cli-command-ids, check:cross-package-test-inputs, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:watch-hint-literal, node check-adr-0087-registration --base origin/main, --self-test, check-ci-filter-parity, check-cross-package-test-inputs, bare-root-worklist --self-test, check:pm-dispatch-gates, check:nul-bytes, check-comment-mask-adoption (--self-test and scan), check-comment-mask-corpus, js-comment-mask --self-test. Verdict lines quoted from the gates: 'check-adr-0087-registration --self-test: 228 assertions over real temp git repos (real scan()/assertInputs() path)' (212 before) and 'check-adr-0087-registration: this PR adds no declared-breaking changeset (0 non-breaking changeset(s) seen).' ONE NON-GREEN, not caused by this diff: check:bash32-floor exits 1 (8 of 153 self-test cases, at the simulated-3.2 harness leg where the sandbox shell reports 'mapfile: command not found'). CONTROL: identical 8-of-153 failure on a pristine origin/main checkout with this change absent, in a separate detached worktree; this diff contains no shell. ACCEPTANCE PROBE, the way Check Changeset runs it: PR 12885's changeset fetched read-only into refs/probe/issue-12881/pr12885, committed onto a detached probe worktree, gate run with --base. BEFORE (origin/main 15d55fb24) exit 1 with the card's verbatim refusal; AFTER (35670ad66) exit 0, 'not-required (runtime-interface-only) -- verified: packages/client/src/index.ts#ObjectStackClient (class)'. OTHER VERDICTS UNCHANGED, measured three ways: (a) --list and --audit-stock over the live stock are byte-identical before and after (diff empty); (b) all three live runtime-interface-only claims re-verified before/after - PluginMetadata VERIFIED both sides, ObjectStackClient and DatasourceDriverHandle REFUSED to VERIFIED, both on the same docblock-prose cause, inspected line by line; (c) blast radius over the real corpus - 385 metadata surfaces at HEAD, 384 carrying comments, 2746193 comment bytes masked, and ZERO import-shaped lines and ZERO 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. Gate runtime over the whole surface corpus for all 3 claims: 0.5s, no perf regression. ABLATION (no build or dist involved - a plain node script): reverted only the two masking lines with perl -0pi, mutation CONFIRMED ON DISK by counting both texts (deleted-text occurrences 0, injected-text occurrences 1, refusing to continue otherwise), self-test then failed 4 cases - RIO-G2, RIO-G3, RIO-G4 flip red and RIO-R10's evidence line moves to the docblock - while RIO-R3, RIO-R11, RIO-R12 stay red on their own reasons. RESTORE proven, not assumed: git checkout HEAD -- ABSOLUTE_PATH (never a bare checkout, which reads from the index), then git hash-object compared equal to the HEAD blob d5fc1d833b8c19cafdaa6dc3822053c167797df4 and git diff HEAD reporting 0 changed paths; an absolute-path trap ran on EXIT/INT/TERM throughout.", "mcp_calls": "0 - every GitHub read and write went through the gh CLI in a local checkout; no MCP GitHub tool was called.", "open_questions": [], "out_of_scope_findings": [ "filed as #12996: check-adr-0087-registration's findMigrationPrescription closes a framed region at the next heading of ANY level, so a '## Migration' section whose rewrite tables sit under '###' sub-headings reads as no prescription; the header-framed arm does not rescue it either, because OLD_COLUMN_RE has no 'before' column spelling. Pinned empirically: heading+table detects, heading+sub-heading+table returns null, the two differing by one line. This matters because runtime-interface-only and no-migration-prescription both INHERIT the prescription refusal. Not fixed in this PR: the fix refuses changesets that pass today and needs its own stock-wide before/after measurement, which is a different card from this one.", "not filed, recorded here as a caution: scripts/check-adr-0087-registration.mjs's importSpecifiersFor uses a lazy [\\s\\S]*? between 'import' and 'from', which is quadratic in the whitespace that blanking leaves behind. It does NOT bite the gate (it runs only on files that already hit the code-level mention test, and the whole-corpus run measures 0.5s), but it did bite a whole-corpus probe I wrote against it, which timed out at 2 minutes until rewritten linearly. js-comment-mask.mjs's own header documents this exact pathology and the 51x number behind it. Worth knowing before anyone widens that call's population." ] }ACCEPT — PR #12997. Lands normally: ready + queue once every check is green (watcher armed)
Reviewed against the diff and the report's measurements. Verified: draft ✓, base main ✓, Fixes #12881 first line ✓, two files — the gate script and its own adr-anchors baseline — no governed path, skip-changeset correctly derived for a scripts-only diff ✓.
What carries the acceptance:
- The fix is the right shape: comment spans MASKED (not stripped — the branch quotes a line, and masking preserves line numbers) through the shared js-comment-mask module, applied to all four reads in step 4. String literals still refuse, pinned in both directions — the gate's teeth are intact.
- The acceptance probe is the live reproduction turning green: PR ADR-0006 D2 (SDK half): rename client.projects.* to client.environments.*, unwrap keys follow the wire, JSDoc names the endpoint (#12866) #12885's actual changeset, refused verbatim before, "not-required (runtime-interface-only) — verified" after.
- No-collateral is measured three ways, not asserted: --list/--audit-stock byte-identical across the fix; all three live runtime-interface-only claims re-verified line by line (the fix also heals DatasourceDriverHandle, the same defect's second victim); and a corpus-wide probe showing zero import- or declaration-shaped lines living only inside comments today, so the other reads provably cannot move.
- Ablation with on-disk mutation confirmation and blob-hash-proven restore; self-test grew 212 → 228 assertions including the new both-direction pins.
- The one non-green (check:bash32-floor) is controlled at origin/main in a clean worktree and already recorded on Five
@objectstack/clie2e test files fail on macOS on a clean checkout (port-drift arms never see the drift they assert) #12884's family; this diff contains no shell. - The separate blind spot it uncovered (migration-prescription region closing at any heading level) is correctly FILED as check-adr-0087-registration: a
## Migrationsection loses its framing at the first###sub-heading, so its rewrite tables read as no prescription #12996 rather than folded in — that fix would refuse changesets that pass today and needs its own stock-wide measurement.
Post-merge follow-up recorded on the epic: PR #12885's Check Changeset red re-evaluates only on a fresh merge ref — after this merges, refresh #12885 via update-branch (a rerun of the old run would reuse the stale snapshot).
- added a commit that references this issue
on Aug 28, 2026 os-dev-report
(patch round — supersedes the report above for gates, head sha and findings)
{ "issue": 12881, "status": "done", "branch": "claude/issue-12881-adr0087-comment-blind-spot", "pr": "https://github.com/objectstack-ai/objectstack/pull/12997", "premise_still_valid": true, "summary": "PATCH ROUND. Manifest edited: scripts/objectui-changeset-digest.mjs (the fw-gate staging block, where the single hard-coded 'scripts/invoked-as.mjs' copy line becomes a list). Self-test verdict line, quoted from the gate: 'objectui-changeset-digest --self-test: all checks passed' (exit 0; before the fix exit 1, with both #6494 ROUND TRIP legs dying on ERR_MODULE_NOT_FOUND). New head sha: 8a7db35d238d9fbce53eaa30da0bb4aa3cb0df82 (short 8a7db35d2). The digest self-test stages an EXECUTABLE COPY of check-adr-0087-registration.mjs into a throwaway repo and runs it, so the gate's new ./js-comment-mask.mjs import had nothing to resolve there. Fixed the manifest, not the import, exactly as instructed; the list idiom now matches how the gate's own I1/I2 fixture spells the same obligation, so both staging sites of this one gate read alike. scripts/check-empty-changeset.mjs also names this gate but only reads its TEXT for a parser-parity assertion and never executes a copy, so it needed nothing. Original card unchanged: the runtime-interface-only predicate still masks comment spans and still refuses string-literal mentions, and every earlier measurement stands.", "tests": "Union re-run at 8a7db35d2 after merging current origin/main, exit codes captured before any pipe. GREEN: check:adr-anchors, check:agent-test-spelling, check:changeset-gate-self-tests, check:cli-command-ids, check:cross-package-test-inputs, check:entry-guard, check:objectui-changeset, check:parse-guard, check:pnpm-filter-targets, check:watch-hint-literal, adr-0087 enforcing scan, adr-0087 --self-test, objectui-changeset-digest --self-test, check-ci-filter-parity, check-cross-package-test-inputs, bare-root-worklist --self-test, check:pm-dispatch-gates, check:nul-bytes, check-comment-mask-adoption (--self-test and scan), check-comment-mask-corpus, js-comment-mask --self-test. BEFORE/AFTER on the failing family, reproduced locally: exit 1 with 'x #6494 ROUND TRIP (red)' and 'x #6494 ROUND TRIP (green) ... status=1', then exit 0 with both legs ticked. UNCHANGED NON-GREEN: check:bash32-floor still exits 1 here (8 of 153 self-test cases, simulated-3.2 harness leg, 'mapfile: command not found'), controlled earlier against a pristine origin/main checkout with this change absent; this diff contains no shell. dispatch-gates re-derived on the 3-path change set from a tree at current origin/main with no STALE TREE warning.", "mcp_calls": "0 - every GitHub read and write went through the gh CLI in a local checkout; no MCP GitHub tool was called.", "open_questions": [], "out_of_scope_findings": [ "GATE-MAPPING GAP, one line for that 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, and it matched only once the digest itself was edited, 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 which the existing 'adds or edits a GATE SCRIPT' convention-trigger does not reach, because that trigger fires on the edited gate's own families, never on the families of gates that stage a copy of it.", "filed as #12996 (unchanged from the first report): check-adr-0087-registration's findMigrationPrescription closes a framed region at the next heading of ANY level, so a '## Migration' section whose rewrite tables sit under '###' sub-headings reads as no prescription; OLD_COLUMN_RE has no 'before' column spelling either. Matters because runtime-interface-only and no-migration-prescription both INHERIT the prescription refusal." ] }- added a commit that references this issue
on Sep 1, 2026
Found while implementing #12866 (ADR-0006 D2, SDK half). Filed unassigned.
What happens
check-adr-0087-registration.mjs'sruntime-interface-onlypredicate (step 4 — "NO metadata surface REFERENCES it") counts a symbol that appears only inside a JSDoc prose comment of a metadata surface as a reference it cannot resolve, and refuses the disposition.Reproduced on
cae0e248c:The "reference" is
packages/spec/src/api/contract.zod.tsline 164 — a sentence inside theBaseResponseSchemadocblock explaining whatunwrapResponsekeys on. It is prose.ObjectStackClientis not imported there, is not in any schema, andobjectstack migrate metahas nothing to reach through it. Steps 1, 2 and 3 of the predicate all pass.Why it matters
The refusal pushes an author toward
not-required (no-migration-prescription), which for a changeset carrying a real before/after migration table is a self-contradiction — exactly the detector-miss anti-pattern the gate's own header records as #8299 and exists to close. So the blind spot converts an honest, nearly-verified disposition into pressure to claim a dishonest one.It is currently the only
.zod.tsin the repo that mentionsObjectStackClient(measured:git grep -ln ObjectStackClient -- '*.zod.ts'returns exactly that file), so the blast radius today is small — but the shape is general: any metadata surface whose docblock names a runtime type blocks that type's exemption.Suggested direction (not a decision)
Strip comments before the step-4 scan, or treat a match that occurs only inside a comment as not-a-reference and say so in the refusal text. Both keep #4690's "refuse rather than assume unrelated" stance for real code references. The gate has a
--self-test; a case for "named only in a docblock" belongs with it.Evidence
scripts/check-adr-0087-registration.mjs, theruntime-interface-onlyblock (~line 1671 onwards)packages/spec/src/api/contract.zod.ts:164.changeset/adr0006-d2-client-environments-namespace.mdon branchclaude/issue-12866-adr0006-d2-sdk-environmentsGenerated by Claude Code