Skip to content

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

@hotlong

Found while implementing #12866 (ADR-0006 D2, SDK half). Filed unassigned.

What happens

check-adr-0087-registration.mjs's runtime-interface-only predicate (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:

runtime-interface-only packages/client/src/index.ts#ObjectStackClient cannot be verified:
packages/spec/src/api/contract.zod.ts (a Zod schema) mentions `ObjectStackClient` while
neither declaring nor importing it:
  * `success`, which is what `ObjectStackClient.unwrapResponse` keys on and what
Unresolvable, so refused rather than assumed unrelated (#4690).

The "reference" is packages/spec/src/api/contract.zod.ts line 164 — a sentence inside the BaseResponseSchema docblock explaining what unwrapResponse keys on. It is prose. ObjectStackClient is not imported there, is not in any schema, and objectstack migrate meta has 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.ts in the repo that mentions ObjectStackClient (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

  • Gate: scripts/check-adr-0087-registration.mjs, the runtime-interface-only block (~line 1671 onwards)
  • The blocking mention: packages/spec/src/api/contract.zod.ts:164
  • The changeset that hit it: .changeset/adr0006-d2-client-environments-namespace.md on branch claude/issue-12866-adr0006-d2-sdk-environments

Generated by Claude Code

Activity

  1. self-assigned this
    on Aug 28, 2026
  2. hotlong commented on Aug 28, 2026

    @hotlong
    ContributorAuthor

    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.mjs and 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 honest runtime-interface-only disposition on its changeset must pass once the predicate stops counting comment-prose mentions.

  3. hotlong commented on Aug 28, 2026

    @hotlong
    ContributorAuthor

    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 fresh origin/main at 15d55fb2430f374c97a8a4d9914e9a668758d9f2

    File surface: scripts/check-adr-0087-registration.mjs plus 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

  4. hotlong commented on Aug 28, 2026

    @hotlong
    ContributorAuthor

    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."
      ]
    }
  5. hotlong commented on Aug 28, 2026

    @hotlong
    ContributorAuthor

    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:

    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).

  6. hotlong commented on Aug 28, 2026

    @hotlong
    ContributorAuthor

    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."
      ]
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions