Skip to content

fix(docs-audit): give declinedIn the type-declaration discriminator - #11500

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-10901-declinedin-discriminators-v2
Aug 24, 2026
Merged

fix(docs-audit): give declinedIn the type-declaration discriminator#11500
os-steve merged 1 commit into
mainfrom
claude/issue-10901-declinedin-discriminators-v2

Conversation

@claude

@claude claude Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #10901

declinedIn was the last route:/client: scan in scripts/docs-audit/affected-docs.mjs still deciding "is this in code position?" for itself. #10793 taught the row recognizer — and the first term of its denominator — to read through both of #10500's exact discriminators. Its complement did not move, so a literal-union route: TYPE member written in either of the two quotes the recognizer declines was billed as a value the parse failed to read: a named entry, a PARTIAL-read verdict, and exit 1, on a ledger that is completely accurate. That is the false red the type-declaration exclusion exists to prevent.

Route taken: (2), the narrow one — chosen on measurement, not on adjective

Triage ruled (2) and left (3) available "if it turns out equally cheap". It is not. I built (3) as a working probe (fold declinedIn into declarationsIn as its missing third filter, with the one declaration list computed once per file so the in-window call cannot mis-lex a slice) and ran the differential against origin/main on five shapes. (3) passes the existing 339 self-test cases, so the measurement had to be behavioural rather than a pass/fail:

shape origin/main route (3) route (2), this PR
A literal-union route: type member, double-quoted declined 1, exit 1 declined 0 declined 0
B same, backtick declined 1, exit 1 declined 0 declined 0
C declined spelling quoted in a comment (#10794) declined 1, exit 1 declined 0 declined 1, exit 1
D value on the next line after the colon declined 2, routesDeclared 3 declined 1, value unnamed declined 2, routesDeclared 3
E, F, G trailing entry interface, quoted client: union declined 1, exit 1 declined 0 declined 0

(3) changes four populations, two of them uninvited:

So (2) it is: it changes exactly the population this card is about, in both quote spellings, at both call sites. D is pre-existing and byte-identical on origin/main and on this branch — filed unassigned as #11494.

What changed

The region list arrives as a required parameter from the one caller that already computes it, with no default — a call site that forgot the discriminator would silently reintroduce exactly the second opinion this closes. offset makes the returned index absolute, so the in-window slice's coordinate translation happens once inside the function instead of at one of the two call sites.

Both call sites were reachable and both are covered:

  • a leading entry interface arrives through the file-wide route: sweep;
  • a trailing one lands inside a row window and reached the in-window client: sweep. Measured before the fix on that shape: clientsDeclared 2 on a file declaring one client, one named declined entry, exit 1.

⛔ The other discriminator is deliberately not applied. This still reads raw bytes, so a declined spelling quoted in a comment is still billed as an unread row (#10794). Applying codeOnly here is also not free of its own accord: it blanks string contents, and this scan's whole job is to quote the unread spelling back at the reader, so a masked window would name route: "" for every entry. The boundary is pinned in --self-test so whichever card eventually takes on #10794 moves that pin rather than finding none.

Evidence

All figures below are from this branch at c2fc7771.

The non-vacuity proof is synthetic, and it has to be. The live delta is zero, so a green live run proves nothing about this change — stated as a prediction before running, then checked.

Fail-before / pass-after, identical case text, run against origin/main's declinedIn and against this one:

✗ self-test "a double-quoted literal-union `route:` TYPE member is not an unread row": declined → expected parseLedgerSource=0, got 1
✗ self-test "and a double-quoted member moves no denominator either": declared → expected "1 row / 1 route / 1 client", got "1 row / 2 route / 1 client"
✗ self-test "a double-quoted type member carries NO broken-scan verdict": brokenScan → expected bridgeCoverageFrom=0, got 1
… (backtick twins, both `client:` call-site cases, both trailing cases, the `type X = { … }` spelling)
✗ affected-docs self-test failed (15 case(s)).      ← before
✓ affected-docs self-test: 366 cases pass.          ← after (339 before this PR, +27)

Both quote spellings are pinned apart, not assumed. They come out of one regex alternation and one code path and were measured behaving identically — which is the reason to pin them separately rather than to trust one for both.

Live safety check — predicted NO CHANGE, and it held. --bridge-coverage before and after is byte-identical (md5 201a2b4850f62b5f9f0133547d0e907f both runs), exit 0 both:

  ledger rows read ........... 268 of 268 declared
  client-bound ledger rows ... 222 of 222 declared
  prose-quoted leads (no row) . 0
    reachable ................ 45
    UNREACHABLE .............. 177

The claimed live-zero was verified here rather than inherited, with a positive control. Across all seven ledgers: 0 quoted route:/client: leads inside any type declaration, out of 490 quoted code leads. The same scan finds the lead in the fixture (1 per spelling), so the zero is a measurement and not a broken scan.

The exclusion is still load-bearing — measured, not assumed. Ablating typeDeclRegions to return [] (mutation proven on disk by anchored grep; restored by an EXIT INT TERM trap; affected-docs.mjs is executed directly by node, so the edited bytes are the bytes that run):

origin/main, ablated:   268 of 275 declared, exit 1
this branch, ablated:   268 of 275 declared, exit 1      ← identical: not weakened
this branch, ablated, --self-test: 46 case(s) fail       (15 before this PR)

That is today's tree's form of the note the file records (259 of 266): seven ledgers, one route: string; member each, 268 + 7 = 275.

--bridge-coverage's consumer is unaffected. .github/workflows/docs-drift-check.yml reads only bridge.measured, bridge.reachable, bridge.clientRows and bridge.unreachable from the --json arm. This change does not touch rows, so none of those four can move; the header's shape is unchanged.

Gates

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (change set taken from the script's own merge-base derivation, re-derived after the final commit — the set was unchanged), and run at c2fc7771. All green, each quoted from the gate's own verdict line:

  • pnpm check:docs-audit-scope✓ affected-docs self-test: 366 cases pass. / ✓ check-audit-scope self-test: 24 cases pass.
  • pnpm check:entry-guard✓ check:entry-guard: 139 scripts/ file(s) — every entry guard goes through invoked-as.mjs
  • pnpm check:parse-guard✓ check:parse-guard: 138 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
  • pnpm check:cross-package-test-inputs / node scripts/check-cross-package-test-inputs.mjsOK: 14 package(s) read outside themselves, all declared
  • pnpm check:pnpm-filter-targets✓ check:pnpm-filter-targets: 120/148 --filter occurrence(s) … resolve
  • pnpm check:pm-governed-merges✓ check-governed-merges --self-test: 129 assertions
  • node scripts/check-ci-filter-parity.mjsOK: all 89 declared cross-package glob(s) … are covered
  • node scripts/docs-audit/check-affected-docs.mjs✓ affected-docs self-test: 366 cases pass.
  • pnpm check:nul-bytescheck-nul-bytes: OK (scanned 6448 text file(s) … no raw ASCII control bytes).

No narrowing to declare: pnpm lint (eslint . --no-inline-config, repo-wide) was run in full under the shared verify lock — exit 0 in 68s.

No changeset: this touches one CI/tooling script under scripts/ and publishes nothing, so the PR carries skip-changeset.

Closing-reference hygiene, and one measurement that changed how I report it

An earlier revision of this work carried a closing keyword before a reference to #10794 in both the PR
body and the commit message. #10794 was closed not planned by a human on 2026-08-21 and this change
explicitly does not act on it, so either would have recorded that decision as undone by an unrelated PR.
Both are gone here: the body was reworded, and this branch was re-landed from origin/main with the
commit message reworded too, since a pushed commit message cannot be corrected without a force-push.
The predecessor is #11495, closed as superseded; its diff and this one are byte-identical (same blob
cac567a9, same patch md5 f99e1da7e5c3c5df6e19aaaa54a90808, same +184 / -14).

⚠️ resolve/resolves/resolved is itself a closing keyword, alongside close/closes/closed
and fix/fixes/fixed — rewording one into another is not a fix. The wording here (takes on) uses
none of them, and both body and message were rescanned for keyword+#n and keyword+URL forms.

⚠️ The closed_by_pull_requests REST field is not usable as proof of this, in either direction.
After the fix it reads total=0 on #10794 — but it also reads total=0 on #10901, whose intended
closing line is intact at the top of this body, and a control over four other OPEN PRs in this repo carrying Fixes #n
(#11469, #11466, #11406, #11393, draft and non-draft alike) shows total=0 on all four target issues.
So its zero says nothing about any particular PR. The discriminating evidence is the source text that the
merge-time parser actually reads — the stored body and the commit message, both rescanned above — not the
field. (closingIssuesReferences via GraphQL would have been the direct check; it returns HTTP 403 for
this token.)


Generated by Claude Code

…#10901)

`declinedIn` was the last `route:`/`client:` scan in `affected-docs.mjs` still
deciding "is this in code position?" for itself. #10793 taught the row recognizer
and the first term of its denominator to read through both of #10500's exact
discriminators; its complement did not move, so a literal-union `route:` TYPE
member written in either of the two quotes the recognizer declines was billed as
a value the parse FAILED to read — a named entry, a PARTIAL-read verdict and
exit 1, on a ledger that is completely accurate.

The region list now arrives as a required parameter from the one caller that
already computes it, with no default: a call site that forgot the discriminator
would silently reintroduce exactly the second opinion this closes. `offset`
makes the returned index absolute, so the in-window slice's coordinate
translation happens once inside the function instead of at one of the two call
sites.

Both call sites were reachable and both are covered: a leading entry interface
arrives through the file-wide `route:` sweep, a trailing one lands inside a row
window and reached the in-window `client:` sweep (measured before the fix:
`clientsDeclared` 2 on a file declaring one client, exit 1).

⛔ The other discriminator is deliberately NOT applied. This still reads raw
bytes, so a declined spelling quoted in a COMMENT is still billed as an unread
row — that is #10794, closed `not planned`, and not this change's to reverse.
The boundary is pinned in `--self-test` so whichever card eventually takes on #10794
moves the pin rather than finding none.

Self-test: 339 -> 366 cases. 15 of the new cases fail against the previous
`declinedIn` and pass after. Live delta is zero and predicted so in advance:
0 quoted `route:`/`client:` leads inside any type declaration across all seven
ledgers, and `--bridge-coverage` is byte-identical before and after
(268 of 268 route / 222 of 222 client / 177 unreachable, exit 0).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

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

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants