Repository navigation
docs-audit: a PARTIAL ledger parse is silent — one backtick-quoted route: drops a row from the bridge population with no verdict and exit 0 #9896
Description
Filed unassigned by the os-dev seat resuming #9572 (session session_01XqDQYVU5smx29ts9pAErja, PR #9893). Recording, not fixing — out of that card's scope, which the first-touch grading pinned to reporting the reach ratio.
What
parseLedgerSource in scripts/docs-audit/affected-docs.mjs recognizes a ledger row only through a single-quote-only regex:
const routeRe = /route\s*:\s*'([^']+)'/g;PR #9893 adds a broken-scan verdict for the all-or-nothing case — a ledger file matching the naming convention that parses to zero rows now exits non-zero with the row recognizer no longer reads this file's shape. That guard is rowsParsed === 0, so it only catches a ledger where every row is unreadable.
A partial parse is still silent, and that is the more likely shape.
Measured on b55014880
Rewriting exactly one of packages/services/service-i18n/src/i18n-route-ledger.ts's three rows — leaving the other two single-quoted:
| ledger written as | client-bound rows seen | verdict | exit |
|---|---|---|---|
| all three single-quoted (today) | 221 | none | 0 |
| one row double-quoted | 220 | none | 0 |
| one row backtick-quoted | 220 | none | 0 |
| all three double-quoted | 218 | parsed 0 rows |
1 |
The row simply leaves the population. Nothing anywhere prints that the ledger declared a row the recognizer could not read — the same defect class as #4690's "zero is a broken scan, not a clean repo", one level down: here a partial read passes for a complete one.
Why the backtick row is the realistic one
A double-quoted string is reformatted back to single quotes by the repo's formatter, so that spelling is unlikely to land. A template literal is not — route: `GET /api/v1/i18n/locales` is formatter-stable, survives review as ordinary TypeScript, and is the natural spelling the moment anyone interpolates a base path into a ledger row. It measured identically above: the row vanishes, exit 0, no verdict.
The same narrowness applies to the client: capture in the row window, so a row with a single-quoted route: and a backtick-quoted client: parses as server-only — it stays in rowsParsed and leaves the client-bound population, which is invisible to the rowsParsed === 0 guard by construction.
Direction, not a prescription
Two shapes, different costs:
- Widen the recognizer to all three quote spellings (and strip
${...}the wayrouteTailOfalready does). Cheap, but silently changes the measured population, so it wants before/after numbers per docs-drift's route bridge counts identifiers found inside COMMENT text — an English sentence in a handler can mint a route anchor #9432's standard. - Detect the shape it declines — count
route\s*:occurrences and compare against rows actually parsed, emitting the difference as a broken-scan verdict. This keeps the recognizer as narrow as it is and makes its narrowness say so, which is the reporting posture [finding] docs-drift's route bridge can reach only 45 of the 221 client-bound ledger routes — 176 have no registrar tail at all, and the variable-path recall half of #9503 is a small slice of it #9572 and [finding] docs-drift's route-bridge exclusions are silent — only the cross-cutting CAP publishes what it withheld, the two KIND rules do not #9433 both argue for. Cheaper to review; produces a number rather than a behaviour change.
(2) looks like the right first move: it cannot change what the bridge reaches, only what it admits it could not read.
Refs: #9572 (the reach report this was found while resuming), #9433, #4690, #9432.
Generated by Claude Code
Activity
os-zhuang commentedon Aug 21, 2026
Claimed by the
domain:devxPM seat (#6023), sessionsession_01DdCnBGcHeufjrq7drTD3wt, branchclaude/issue-9896-docs-audit-partial-ledger-parse.Taken by age at concurrency 5.
fold-or-serial: serial, folds with nothing — checked by file against all eight PRs this seat has open.⭐ Flagged to the dev as the highest-value framing: this is a partial result that reads like a complete one, which is the defect class this repo punishes hardest — and it has recurred three times tonight alone. #10466 shipped a completeness guard printing
OK (24 of 29 …)directly over the one package it could not grade; #8999 found a census carrying a standing 50% blind spot that three separate sweeps "confirmed"; and I twice built a verdict on an indicator that lies rather than errors. ⇒ Making the drop loud is the deliverable, and ⛔ the enumeration of which spellings drop must not itself rest on an uncontrolled search.Generated by Claude Code
os-zhuang commentedon Aug 21, 2026
os-dev-report
{ "issue": 9896, "status": "done", "branch": "claude/issue-9896-docs-audit-partial-ledger-parse", "pr": "https://github.com/objectstack-ai/objectstack/pull/10501", "premise_still_valid": true, "summary": "Confirmed the premise on a718ee3dd and went one step past it. CENSUS: all seven route ledgers are wholly single-quoted — 259 `route:` and 221 `client:` string-literal declarations, 259 parsed / 221 bound, delta 0 — so the backtick `route:` is NOT the only dropping spelling, it is one of four measured drops: (1) backtick `route:` -> 221->220 client-bound rows, exit 0; (2) double-quoted `route:` -> same; (3) single-quoted `route:` beside a backtick `client:` -> 221->220 while rowsParsed stays 3, invisible to the `rowsParsed === 0` guard by construction; (4) NEW and worse — the row window is delimited by the same single-quote-only lead, so a declined row does not close the PREVIOUS row's window and that row INHERITS the declined row's client. Backtick-quoting `GET /api/v1/meta` in rest-route-ledger.ts moved `meta.getTypes` onto the server-only `GET /api/v1/docs` with clientRows still 221 — a count comparison is blind to it, which is why the fix keys on the DECLINED SPELLING rather than on a shortfall (the issue's option 2, refined). `parseLedgerSource` now returns `{rows, declined, routesDeclared, clientsDeclared}` — `rows` byte-identical to before, the recognizer is NOT widened — and `bridgeCoverageFrom` raises a `brokenScan` verdict per ledger that declined anything, naming file, line and literal, capped at 3 named entries. LOUD = a failure (exit non-zero), not a warning, because the only consumer whose signal is read is `check-affected-docs.mjs` as a workflow step, where the exit code IS the verdict; and it qualifies as a verdict on the same test the file already applies (it cannot fire on a tree where the scan works — today 259 of 259). Both halves of the fraction now print on every run. The drift workflow's broken-scan banner no longer promises the scan 'came back structurally empty', which would have let a reader skip exactly the partial case.", "tests": "Gates at final commit 4e7229a668 (working tree clean), family from `node scripts/pm/dispatch-gates.mjs` with NO path args (3 paths, 10 matched families, all run). Exit codes captured before any pipe (`cmd > file 2>&1; EXIT=$?`), and each line below is the gate's OWN verdict line, not a bare $?: affected-docs.mjs --self-test `✓ affected-docs self-test: 281 cases pass.` | check-audit-scope.mjs --self-test `✓ check-audit-scope self-test: 24 cases pass.` | check-audit-scope.mjs `✓ docs-accuracy-audit scope is in sync with content/docs/: 181 hand-written doc(s).` | check-affected-docs.mjs exit 0 with `ledger rows read ... 259 of 259 declared` / `client-bound ledger rows ... 221 of 221 declared` | check-node-version.mjs `OK (29 setup-node step(s) across 26 workflow(s), all on Node 22).` | check-workflow-status-functions.mjs `✓ 34 assertions` + `OK (scanned 26 workflow file(s), 45 job(s) ...)` | check-required-contexts.mjs `✓ 124 assertions` + exit 0 | check-shard-attestation.mjs `✓ 92 assertions` + `✓ 2 aggregate gate(s) count 3 declared leg(s)` | check-cross-package-test-inputs.mjs `All 60 self-test cases passed.` + `OK: 12 package(s) read outside themselves, all declared` | check-nul-bytes.mjs `OK (scanned 6146 text file(s) ... no raw ASCII control bytes)` | npx eslint on the changed .mjs exit 0. ABLATIONS — no rebuild is involved and none was needed: affected-docs.mjs is executed directly by node with no dist/ between the edit and the run, and the ledgers it reads are read from the working tree by readFileSync, so `ablation-dist-preflight` does not apply here. Every mutation was confirmed ON DISK by grepping for BOTH the injected and the removed text (never an editor exit code), and every restore by `git diff --quiet`: (A) backtick one `route:` in i18n-route-ledger.ts — injected x1 / removed x0 — gate EXIT=1, `258 of 259 declared`, `✗ broken scan: ... read 2 of 3 declared string-literal \\`route:\\` value(s) ... declined 1: line 96: route: \\`GET /api/v1/i18n/locales\\``; same input on main printed 220, no verdict, exit 0. (B) backtick one `client:` only — EXIT=1, `220 of 221 declared`, names `line 96: client: \\`i18n.getLocales\\``, rowsParsed still 3. (C) backtick `GET /api/v1/meta` in rest-route-ledger.ts — EXIT=1, `client-bound ledger rows 221 of 221` (count perfectly clean) yet `read 95 of 96 ... line 131`. (D) ablate the new verdict itself (`if (false && (...))`, ablated x1 / original x0) — self-test reds on 4 pins each naming what it lost, restored byte-clean at 281 pass. (E) ablate in the OPPOSITE direction, silently widening routeRe to all three quotes — 5 pins red including `the narrow population is UNCHANGED ... expected 2, got 4` and `read + declined accounts for every declared route: ... expected 4, got 6`; restored byte-clean. POSITIVE CONTROL for the zero-hit census: the all-single-quoted self-test fixture is pinned to declined=0 AND routesDeclared===rows.length, paired with a partial fixture pinned to declined=3, plus ablation (A) firing on the real tree. BLAST RADIUS: `affected-docs.mjs` is the ONLY regex reader of ledger source text in the repo (grep for `route\\s*:` across all .mjs/.mts/.ts returns it alone); 30+ files import the ledgers as VALUES and none reads them as text (grep readFileSync over that set: zero ledger reads), so a quote respelling is invisible to them by construction. The radius is that one file's two output paths — the `--bridge-coverage` gate and the advisory `bridgeCoverage` rendered in the drift PR comment — both of which already carried `brokenScan`, so no new field needed wiring. Cost of a silent drop, measured: of the 45 reachable client-bound rows, 6 name a client method at least one hand-written page carries, carrying 7 (row,page) nominations across 4 distinct pages including api/client-sdk.mdx — dropping one of those 6 removes a real doc nomination; dropping any of the other 215 removes none today but silently shrinks the denominator the gate and the PR comment report.", "open_questions": [], "out_of_scope_findings": [ "filed as #10500: a route-ledger row whose `route:` is not a string literal (`route: ROUTES.health`) is invisible to BOTH the recognizer and the new counter — the only exact discriminator against the `route: string;` member every ledger's entry interface declares is the opening quote (measured: 8 non-quoted `route:` across the 7 ledgers, 7 of them that interface member, 1 an English sentence in a comment), so every separating heuristic would red on accurate files today. Boundary pinned in --self-test and stated in the README rather than papered over." ] }Notes not in the JSON: the PR is #10501, draft,
skip-changesetapplied and read back —documentation,ci/cd,skip-changesetare all present, so the bot's labels were not clobbered. No changeset: the diff isscripts/docs-audit/**and.github/workflows/**, no publish surface at diff time. The empty placeholder branch left by the killed predecessor was reset ontoa718ee3ddwith-Bbefore any work; nothing was lost.Generated by Claude Code
Generated by Claude Code
fix(tooling): give a non-literal ledger `route:` a verdict instead of…
fix(tooling): give a non-literal ledger `route:` a verdict instead of…