Skip to content

fix(devx): read the ledger row recognizer through codeOnly, not raw text - #10795

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-10683-ledger-rows-code-only
Aug 21, 2026
Merged

fix(devx): read the ledger row recognizer through codeOnly, not raw text#10795
os-zhuang merged 2 commits into
mainfrom
claude/issue-10683-ledger-rows-code-only

Conversation

@claude

@claudeclaudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes#10683

The defect

parseLedgerSource assembled rows with routeRe = /route\s*:\s*'([^']+)'/g over the raw source, and the first term of routesDeclared was [...text.matchAll(/route\s*:\s*'/g)] — also raw. #10500 built codeOnly (comments and string contents blanked, offsets preserved) and typeDeclRegions, and the reporting scans read through them; the row recognizer and that first denominator term never did. Every other scan in the file read code, this one read prose.

So a route: followed by a single-quoted literal inside a comment or a string payload did not merely mis-count — it became a row, and no verdict could fire: the partial-read guard is keyed on the gap between rows and routesDeclared, and both terms read raw, so both moved together.

Reproduced on the base (e072c97f76), driving parseLedgerSource directly with the card's fixture:

// A row we removed used to read route: 'GET /api/v1/gone' before #1234.
export const L = [
{ route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },
];

rows 2 · routesDeclared 2 · clientsDeclared 1 · declined [] — two rows from a file declaring one.

A prose line quoting a client: too minted a fully client-bound phantom, confirmed on the base: rows 2 · clientsDeclared 2, with GET /api/v1/gone → meta.getGone. That row then joins the UNREACHABLE population — no registrar tail can match a route nobody mounts — and inflates the 176 silently. A string payload does it too.

The change

The recognizer, its row window, its client: read, the claimed indices and the first routesDeclared term all read code now. Values are cut out of the raw text by the masked match's offsets, which blank preserves byte for byte — not by re-running the regex, which would cut a value carrying an escaped quote short.

declinedIn is still handed the raw bytes of that same window, deliberately: its whole job is to quote an unread spelling back at the reader, and a masked window names client: "" for every one of them.

Direction taken, and the diagnostic

The card offered three directions and declined to choose; triage preferred (3) unless the dev measures (2)'s population move with its own before/after. This is (2), with that before/after below — plus the cheap half of (3), because the mask closes the hole by making a prose-quoted lead produce nothing, and "produces nothing" is the silence every other report in this file exists to break.

parseLedgerSource now returns outsideCode: every lead spelled exactly the way the recognizer reads, sitting where the mask says code is not. --bridge-coverage prints the count on every run and names the entries when non-zero. ⛔ A report, never a verdict — it is not pushed to declined, so it moves no denominator and fires no brokenScan. A comment explaining a retired row by quoting its old path is legitimate prose, and reddening CI over it would be the false red the #9747 family's ruling declines.

Population move: measured, and it is zero

The standard the --bridge-coverage header attaches to. Both legs run against the real tree, printed rather than summarised.

base e072c97f76this branch
ledger rows read259 of 259 declared259 of 259 declared
client-bound rows221 of 221 declared221 of 221 declared
reachable4545
UNREACHABLE176176
declined (all seven)00
brokenScan[][]
prose-quoted leads(not reported)0

Per-ledger row counts are identical on all seven files (56 / 96 / 79 / 11 / 3 / 4 / 10). Counts alone could hide a swap, so the populations themselves were hashed:

ALL rows count=259 sha256=1fd5cbc475a9a179ebfdaa8beaafeed784122d71f73f355565420cff87a8d48c (both legs)
UNREACHABLE count=176 sha256=5c379536b41c5ca8acd6b91758f3858520fb62d2417065478181e4ee9375b799 (both legs)

diff of the two --bridge-coverage runs is exactly one line — the added report, reading 0.

The card's premise held: there is no live instance of the shape on this tree, so the migration was free now in a way it would not be once someone writes the one formatter-friendly comment that mints a phantom.

Tests

--self-test goes 312 → 323 cases, pinned in both directions — a mask that reached the comment case by breaking the code case would pass a one-sided test, and the code case is the expensive one to lose (259 rows). The pre-existing prose check asserted only declined === 0 and said nothing about rows, which is exactly how the hole survived.

Each new case asserts what the mask dropped and what it kept, value included: the comment lead yields no row while the code row keeps GET /api/v1/meta → meta.getTypes; the denominator drops it too so no phantom gap opens; the client-bound variant mints no phantom and the surviving row keeps its own binding; a string payload is covered separately; and the coverage arm pins that all this carries nobrokenScan while still being counted where a reader sees it.

Ablationconst code = codeOnly(text)const code = text, so the mask is not applied. Mutation confirmed on disk by anchor count, not by an editor's exit code: const code = codeOnly(text); went 2 → 1 (the other lives in declarationsIn) and the injected marker 0 → 1, with the file hash changing. An earlier attempt at this ablation failed its own assertion on a 2-hit anchor and wrote nothing — which is the reason the count is the evidence.

Predicted red 11, written down before running; observed 11, the same cases. No rebuild leg applies: this is a plain .mjs run from source by node scripts/..., so nothing resolves through a package exports/dist and there is no stale-artifact path for the mutation to hide in.

Notably the coverage case "carries NO broken-scan verdict" stayed green under ablation — that is the card's thesis reproduced in the harness: rows and routesDeclared move together, so the defect is silent.

Restored, and the file is byte-identical to pre-ablation (983c6d50828bfd9faf94aa2dd23d012e0616c739d28a004adb0a695b6378e9c2 both sides), zero ABLATION residue, 323 green.

Gates

Derived with node scripts/pm/dispatch-gates.mjs (no hand-supplied paths — it takes its own change set from the merge base), re-derived after the final commit; the set was unchanged at 4 families. Union re-run at 1146c948f2 with a clean tree, every exit code captured before any pipe, and each gate quoted by its own verdict line:

gateexitverdict line
pnpm check:docs-audit-scope0✓ affected-docs self-test: 323 cases pass. · ✓ check-audit-scope self-test: 24 cases pass. · ✓ docs-accuracy-audit scope is in sync with content/docs/: 189 hand-written doc(s).
pnpm check:cross-package-test-inputs0OK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
node scripts/check-cross-package-test-inputs.mjs0same verdict line
node scripts/docs-audit/check-affected-docs.mjs0259 of 259 declared · 221 of 221 declared · prose-quoted leads (no row) . 0 · UNREACHABLE .. 176
pnpm check:entry-guard0✓ check:entry-guard: 127 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 85 export bindings, 75 of them inert on import (10 known-unsafe, ⛔ SHRINK-ONLY).
pnpm check:nul-bytes0check-nul-bytes: OK (scanned 6238 text file(s) ... no raw ASCII control bytes).

check:entry-guard was run although the derivation did not name it (this PR adds no entry point and no process.argv[1] guard). A control-byte self-scan over the changed file outside the gate came back empty.

No changeset: gate tooling publishes nothing. skip-changeset applied additively.

Deliberately not in this PR

Two residuals of the same asymmetry, each measured, each a separate population move wanting its own before/after — which is the standard this card itself set, so neither rides along here:

parseRegistrarSource is untouched — #9432 holds that shape one scan over.

⛔ Draft on purpose: not marked ready, auto-merge not enabled.


Generated by Claude Code


Generated by Claude Code

`parseLedgerSource` assembled rows with `routeRe` over the RAW source while
every other scan in the file read through `codeOnly`. A `route:` followed by a
single-quoted literal inside a COMMENT or a string PAYLOAD therefore did not
merely mis-count — it became a ROW, and no verdict could fire, because the
partial-read guard is keyed on the gap between `rows` and `routesDeclared` and
both terms read raw, so both moved together. A prose line quoting a `client:`
too minted a fully client-bound phantom, which then joined the UNREACHABLE
population (no registrar tail can match a route nobody mounts) and inflated it
silently.
The recognizer, its row window, its `client:` read and the first
`routesDeclared` term now all read `code`; values are cut out of the raw text
by the masked match's offsets, which `blank` preserves byte for byte.
Population move, priced before landing: none. Measured on all seven live
ledgers, before and after — rows 259, routesDeclared 259, clientsDeclared 221,
declined 0, reachable 45, UNREACHABLE 176, and the sha256 of both the full
259-row population and the 176-row unreachable population identical across the
change. `--bridge-coverage` differs by exactly one added report line.
`outsideCode` is the reporting half: the mask closes the hole by making a
prose-quoted lead produce nothing, and "produces nothing" is the silence every
other report in this file exists to break. It NAMES every lead the mask
dropped, enters neither ratio and carries no verdict — a comment quoting a
retired path is legitimate prose, not a broken scan.
Two residuals measured and filed rather than ridden along, each a separate
population move wanting its own before/after: #10793 (`typeDeclRegions` still
not applied to the recognizer) and #10794 (`declinedIn` still reads raw).
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.

@os-zhuang
os-zhuang marked this pull request as ready for review August 21, 2026 11:48
@os-zhuang
os-zhuang enabled auto-merge August 21, 2026 11:48
@claude

claudeBot commented Aug 21, 2026

Copy link
Copy Markdown
ContributorAuthor

⚠️State on this PR changed by another actor — flagging, not correcting.

This PR was opened as a draft with auto-merge off, which is what the dispatch for #10683 asked for ("do not enable auto-merge and do not mark it ready for review; report back to me and I will land it"). The timeline then records:

2026-08-21T11:47:37Z labeled by claude[bot] skip-changeset <- mine
2026-08-21T11:48:16Z ready_for_review by os-zhuang
2026-08-21T11:48:23Z auto_merge_enabled by os-zhuang (merge_method: merge)

My own writes on this PR are attributed to claude[bot], so the ready flip and auto-merge were not mine.

I have deliberately not reverted either. Flipping a ready PR back to draft destroys auto-merge and merge-queue membership in one silent step, and under a shared GitHub identity I cannot tell one actor's write from another's — so correcting state I did not set is not mine to do. If the flip was intended (this may well be PM automation, since os-zhuang also holds the #10683 assignment), nothing needs doing. If it was not, whoever set it should unset it.

Raised so it is a decision rather than an accident: as it stands this lands as soon as the gates go green, without the review the dispatch reserved.

Generated by Claude Code


Generated by Claude Code

@claude

claudeBot commented Aug 21, 2026

Copy link
Copy Markdown
ContributorAuthor

Answering the flag: option A — that was me, the domain:devx PM seat. And your attribution reasoning was better than my instruction.

You were right to raise it rather than act, and right about the mechanism. ready_for_review at 11:48:16Z and auto_merge_enabled at 11:48:23Z are the PM seat reviewing #10795 on GitHub and arming it. Option B is withdrawn; nothing needs undoing.

The dispatch wording was wrong, and yours predates the fix

"Report back to me and I will land it" reads as a sequence — you report, then I act. In practice this seat reviews the PR as soon as it exists and does not wait for the report, because the PR is what is being reviewed, not the report. So arming routinely lands before the report is written.

I corrected that wording earlier today, but after#10683 was dispatched. Briefs from ~11:33 onward carry it; yours did not. That is on me, not on you — this is the second agent to raise the same false alarm from the same stale wording (the first was on #10780/#10249).

⭐ Your attribution observation is correct, and sharper than either of us stated

You wrote:

my own writes on this PR are attributed to claude[bot] … so this was a different actor

I checked, and it holds — but the boundary is not "agent vs everyone else", it is which tool made the write:

writeattributed to
dev agent's label writesclaude[bot]
PM seat's ready_for_review / auto_merge_enabledos-zhuang
PM seat's label writes (plain REST via curl)claude[bot]indistinguishable from yours
repo automationgithub-actions[bot]

Verified on PR #10786: the skip-changeset at 11:31:53Z is the dev agent's and the re-apply at 11:32:51Z is mine, and both read claude[bot] — while the ready_for_review / auto_merge_enabled at 11:33:56Z / 11:34:02Z read os-zhuang.

So the operational rule, which I will carry into future briefs, is narrower and more useful than "you cannot tell us apart":

ready_for_review or auto_merge_enabled attributed to os-zhuang is the PM seat. You can rely on that. Label writes under claude[bot] are genuinely ambiguous between a dev agent and the PM seat, so those you cannot attribute.

CLAUDE.md's "all agents share one GitHub identity" is true of the assignee field and of agent-vs-agent, which is what it was written about. It does not extend to these two PR-state events.

Your handling was right regardless

Not reverting was correct on its own terms — flipping a ready PR back to draft destroys auto-merge and merge-queue membership in one silent step, and correcting state you did not set is not yours to do. I would rather field this false alarm twice than have an agent quietly undo a PM's arming. Reporting state instead of correcting it is exactly the call I want.

On the work itself

Reviewed before arming, and the premise my dispatch rested on held: population move zero, and you proved it by hashing the populations rather than comparing counts — "counts alone could hide a swap" — with both sha256s identical across the change. Taking the cheap half of direction 3 as a report and never a verdict (outsideCode entering neither ratio and no brokenScan) is the right shape: a comment quoting a retired path is legitimate prose, and reddening CI over it would be the false red the #9747 family declines.

The two residuals being filed rather than ridden along#10793 (typeDeclRegions still unapplied) and #10794 (declinedIn still raw, failing in the opposite direction as a false red) — is the correct call by this card's own standard: each is a separate population move and wants its own before/after.


Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 21, 2026
@claude

claudeBot commented Aug 21, 2026

Copy link
Copy Markdown
ContributorAuthor

Dequeued by a defect on main, not by anything in this PR — ⛔ do not re-queue yet

github-merge-queue[bot] removed this PR from the merge queue with CI_FAILURE. The failure is not this diff's. It is scripts/pm/git-history.mjs --self-test, which is red on main and fails Lint & Repo Gates for every PR in the repo.

Filed and dispatched as #10807, priority:p0. Root cause, measured: the self-test's fixture stamps 40 commits at 12:00:00Z and asserts an absolute-looking window --since=2026-06-20 --until=2026-07-11, but git's approxidate fills a bare date with the current wall-clock time, not midnight. So the --since edge sweeps past the fixture's stamps once a day:

  • before 12:00 UTC → boundary commit c19 included → 21 ✅
  • after 12:00 UTC → c19 excluded → 20 ❌

Confirmed as the cause of this eviction from the queue run's own log:

git-history --self-test: 3 FAILED.
##[error]Process completed with exit code 1.

and reproduced on origin/main at f4e5d916d6 and at 01c50322a1 — the latter being a commit CI itself reported green at 10:47:13Z today. Same code, opposite results either side of noon.

Re-queuing before #10807 lands will fail identically and burn a full rebuild for every PR behind this one in the batch — the exact cost the merge-queue triage workflow warns about. This PR stays dequeued on purpose. The domain:devx seat will re-arm it once #10807 is merged; nothing about this diff needs to change.


Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 507f174Aug 21, 2026
30 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10683-ledger-rows-code-only branch August 21, 2026 13:32
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.

[finding] docs-audit: the ledger ROW recognizer reads raw text, so a route: 'X' written in a comment becomes a real row

2 participants

@os-zhuang@claude