Skip to content

fix(docs-audit): count a declared client: whose row was never assembled - #10674

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-10636-clients-declared-residue
Aug 21, 2026
Merged

fix(docs-audit): count a declared client: whose row was never assembled#10674
os-zhuang merged 1 commit into
mainfrom
claude/issue-10636-clients-declared-residue

Conversation

@claude

@claudeclaudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes#10636

A sub-count residue, not a live false green. After #10500 the row itself does land in a verdict — its unreadable route: is named with its line and the file is flagged PARTIAL read — so nothing here was silent. What was wrong is a denominator: it quietly omitted a value the file plainly declares, and understating a denominator is the direction that makes a partial read look more complete than it is.

The residue

clientsDeclared was two row-relative terms:

constclientsDeclared=rows.filter((r)=>r.client).length+declined.filter((d)=>d.key==='client').length;

A correctly single-quotedclient: on a row whose route: could not be read is in neither: its row never became a row, and its own spelling is the one the recognizer reads, so nothing declined it.

Reproduction, re-derived on today's tree

Card measured this on ba2d8d4730; re-derived at merge base 72d75ebb55 with the card's own row appended to packages/services/service-i18n/src/i18n-route-ledger.ts (client: ' occurrences in that file: 3 ⇒ 4, confirmed on disk before each run):

BEFORE (origin/main) read 3 of 4 declared `route:` value(s) and 3 of 3 declared `client:` value(s); declined 1: line 101: route: I18N_BASE + '/plurals'
AFTER (this branch) read 3 of 4 declared `route:` value(s) and 3 of 4 declared `client:` value(s); declined 2: line 101: client: 'i18n.getPlurals' (no row read it), line 101: route: I18N_BASE + '/plurals'

Totals: 221 of 221221 of 222. The card's 3 of 3 reproduces verbatim on today's tree. The mutation was then reverted (anchor counts back to 0/0/3) and the run repeated: 259 of 259 / 221 of 221, exit 0, no verdict — the seven live ledgers are unmoved, because all seven are wholly single-quoted and every row was assembled, so no client: can be orphaned.

What each side counts after this change

beforeafter
numerator (clientRows)client: values READ and bound to a rowunchanged, byte for byte
denominator (clientsDeclared)bound values + declined ones found inside a row windowevery client: the file declares in code position — bound, declined, non-literal, or claimed by no row

The recognizer is not widened: rows comes out of the loop it always did. An unclaimed value is pushed to declined, so it is named with its own line rather than only bumping a number — the property #9896 argued for, and the reason the two sides stay comparable: every unit added to the denominator arrives as a named "declared, not read" event that the numerator was never going to count.

Attribution stays window-relative. The file-wide sweep bills its findings to no row, which is what answers the objection the old comment made against counting client: file-wide (a value in prose would be billed to a row that never declared it): codeOnly masks prose and string payloads, typeDeclRegions excludes type members, and the sweep names rather than binds.

Implementation: unreadableIn and the new unclaimedClientsIn are now filters over one scan (declarationsIn), so "what is a declaration in code position" is decided once instead of by two scans that can drift into disagreeing while both look right.

Slightly wider than the card, same defect: the sweep keys on no row claimed it, not on the quote, so it also counts a declined-spellingclient: that sits outside every row window (before the first assembled row, or in the gap a truncated 1200-char window leaves) — previously missed for exactly the same reason. Pinned by its own fixture.

Reject side, asserted positively

The entry-interface member must not be billed. All seven live ledgers spell it client?: string;, which the scan's \b(route|client)\s*: never even matches — so the live tree cannot exercise this. The fixture therefore uses the hostile spelling, a literal-union type member that opens with the very quote the counter reads:

exportinterfaceEntry{route: string;client: 'i18n.getLocales'|'i18n.getPlurals'}

The quote test alone would swallow that; typeDeclRegions is what keeps it out. Pinned twice — no line-1 entry in the declined list, and deleting the interface changes no count.

Ablations (each mutation confirmed on disk by anchor count, each restored and re-run green)

ablationanchorredwhat it proves
A — remove the unclaimedClientsIn sweepcall sites 2 ⇒ 111the fix is what moves every new number, including the two fixtures that already contained the shape
B — remove the typeDeclRegions skipskip line 1 ⇒ 013the reject side is load-bearing: the literal-union member is counted, and #10500's route: string; pins red too (live tree: 259 of 266)
C — remove the !claimed.has(...) guardguard 1 ⇒ 022double counting; live tree goes to 221 of 442 — the exact "denominator widened past what the numerator can ever count" failure
D — remove the codeOnly maskmask 1 ⇒ 03prose and string payloads become declarations (live tree: 259 of 260, the English sentence in runtime/src/route-ledger.ts)

Ablation A was predicted at 9 red by name before running and returned exactly those 9; it is 11 after the extra fixture below it was added.

Self-test: 291 cases on origin/main ⇒ 308 here (+17).

Gates

Union derived with node scripts/pm/dispatch-gates.mjs, no paths passed (change set: 1 path, three-dot vs merge base) — 4 families, hand-cross-checked against every workflow naming this surface. All run at 19c982a268, under scripts/pm/os-verify-lock.sh:

os-verify-lock: VERDICT command-exit 0 · held the lock 2s · waited 199s (3m19s)
✓ affected-docs self-test: 308 cases pass. (check-affected-docs.mjs, docs-drift-check.yml)
✓ affected-docs self-test: 308 cases pass. (check:docs-audit-scope, lint.yml)
✓ check-audit-scope self-test: 24 cases pass.
✓ docs-accuracy-audit scope is in sync with content/docs/: 189 hand-written doc(s).
OK: 13 package(s) read outside themselves, all declared… (check-cross-package-test-inputs)
check-nul-bytes: OK (scanned 6212 text file(s)…)

No dependency import was added — this file stays runnable with node alone, which is what docs-drift-check.yml needs (it deliberately never runs pnpm install).

Changeset: scripts/**-only, publishes nothing ⇒ skip-changeset.


Generated by Claude Code

…bled
`clientsDeclared` was computed from two row-relative terms, so a correctly
single-quoted `client:` belonging to a row whose `route:` could not be read
fell out of both: its row never became a row, and its own spelling is the one
the recognizer reads, so nothing declined it. The value left the denominator
without a word while the file plainly declared it.
The declared-`client:` sweep is now file-wide over the same code-only,
type-declaration-excluded source #10500 built for the non-literal case, minus
the values a row window already claimed. It is billed to no row: an unclaimed
value is pushed to `declined`, so it is NAMED with its own line rather than
only bumping a number.
Fixes#10636
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 21, 2026
@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.

@claude

claudeBot commented Aug 21, 2026

Copy link
Copy Markdown
ContributorAuthor

PM review — verified against the diff, not the report. Approving.

The change is right and the arithmetic holds under my own reading, not just the author's.

The offset alignment is the part that could silently break, and it is sound.claimed
is keyed on absolute keyword-start offsets from three different collectors:
m.index + client.index for a bound value, m.index + d.index for an in-window decline,
and d.index from declarationsIn(text) for the file-wide sweep. Those only agree if
codeOnly() is length-preserving (it blanks contents rather than removing them) and if all
three index the client token rather than its value. Both hold.

Ablation C is what proves it, and it proves more than it claims. Dropping the
!claimed.has(...) guard takes the real tree from 221 of 221 to 221 of 442exactly
double. That is only possible if the guard excluded all 221 claimed values, i.e. every
offset matched across collectors. A single misaligned index would have produced 443 or
some other odd figure. So the real tree's unchanged 221 of 221 is not the weak evidence
it looks like: the ablation exercises the alignment on all 221.

The reject side is asserted positively, which is the right instinct. All seven live
ledgers spell the member client?: string;, which /\b(route|client)\s*:/ never matches —
so the live tree cannot exercise the type-member rejection, and a fixture using the
hostile spelling (client: 'a' | 'b' — a literal-union member opening with the quote the
counter reads) is the only way to pin it. The quote test alone would swallow that;
typeDeclRegions is what rejects it. Pinned twice, and ablation B reds exactly it.

unreadableIn's population is genuinely unchanged. Moving the skip test above the
quote test reorders two continues, so the emitted set is still exactly
(not in skip) AND (unquoted). Byte for byte, as claimed.

The denominator/numerator symmetry is the thing #10636 was actually about, and the
comment block now states it where the arithmetic lives: the sweep pushes to declined — a
value named with its own line — rather than only bumping a count. A denominator widened
past what the numerator can ever count would trade an undercount for a permanently red
ratio, which is what ablation C's 221 of 442 demonstrates. Good that this is pinned
rather than argued.

Scope is slightly wider than the card (it also catches a declined-spelling client:
outside every row window), the PR body says so, and it is the same defect class. Fine.

The card's framing is kept honestly: sub-count residue, not a live false green. The
real tree's number does not move and should not — all seven ledgers are wholly
single-quoted with every row assembled, so no client: can be orphaned today.

CI green (29 checks). skip-changeset correct — gate tooling, publishes nothing.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 21, 2026 08:42
@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit db4341dAug 21, 2026
33 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10636-clients-declared-residue branch August 21, 2026 08:56
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.

docs-audit: a quoted client: on a row whose route: could not be read is absent from the clientsDeclared denominator

2 participants

@os-zhuang@claude