Skip to content

docs-audit: a route-ledger row whose route: is not a string literal is invisible to BOTH the row recognizer and the new partial-read counter #10500

Description

@os-zhuang

Filed unassigned by the os-dev seat implementing #9896 (session session_01DdCnBGcHeufjrq7drTD3wt, PR for #9896). Recording, not fixing — deliberately out of that card's scope, and the boundary is pinned in --self-test rather than left to be discovered.

What

#9896 makes a PARTIAL ledger read loud: parseLedgerSource in scripts/docs-audit/affected-docs.mjs now reports every route: / client: string literal it declined to read (double-quoted or backtick-quoted), and bridgeCoverageFrom turns that into a brokenScan verdict naming the file, the line and the literal.

That counter is exact for string literals and blind to everything else. A row written as

{ route: ROUTES.health, family: 'ops', disposition: 'server-only' },
{ route: META_BASE + '/types', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },

is read by neither half: the recognizer needs an opening ', and the counter counts only values that open with one of the three quotes. The row leaves the population with no verdict — the same silence #9896 closed for the quote spellings, one spelling further out.

Why it was not closed in #9896

The only exact discriminator between a row value and the route: string; member that every ledger's own entry interface declares is the opening quote. Measured on a718ee3dd across all seven ledgers:

ledger route: total quoted non-quoted
plugin-auth/src/auth-route-ledger.ts 57 56 1
rest/src/rest-route-ledger.ts 97 96 1
runtime/src/route-ledger.ts 81 79 2
service-datasource/.../datasource-route-ledger.ts 12 11 1
service-i18n/.../i18n-route-ledger.ts 4 3 1
service-settings/.../settings-route-ledger.ts 5 4 1
service-storage/.../storage-route-ledger.ts 11 10 1

Every non-quoted occurrence is accounted for: 7 are the route: string; interface member (one per ledger) and 1 is an English sentence in a comment in runtime/src/route-ledger.ts:182 ("It never named a mounted route: the branch"). So a naive "count every route:" check would red on all seven ledgers today — a false red on accurate files — and every heuristic that separates the two (trailing , vs ;, an uppercase-initial value, a type-name allowlist) is a guess that will be wrong on the first row that does not fit it. #9896 chose to state the boundary rather than paper over it with one.

How likely is this spelling?

Lower than the template literal #9896 addressed, but not zero. These modules are deliberately import-free (see the header of i18n-route-ledger.ts: "It must stay import-free — the client-side guard imports it as a relative SOURCE file"), so a cross-module constant cannot land. A file-local const BASE = '/api/v1/i18n' above the table can, and is the natural first move the moment a ledger's rows get long. Today: 0 occurrences.

Direction, not a prescription

  1. Leave it, keep the boundary declared. It is pinned in affected-docs.mjs --self-test ("a non-literal route: is out of scope for the counter, and so is route: string;") and stated in scripts/docs-audit/README.md, so the next reader meets it rather than discovering it. Cost: the gap stays real.
  2. Give the ledgers a lint rule instead of the scanner a heuristic — an ESLint rule over **/*route-ledger.ts requiring every route: / client: value inside the exported table to be a single-quoted literal. This turns the scanner's narrowness into an authoring constraint, which is where it can be enforced exactly, and would subsume the 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 verdict as a second line of defence rather than duplicate it.
  3. Parse the ledgers as TypeScript. Exact, and rejected on the same ground the whole scanner rests on: affected-docs.mjs is a dependency-free source scan precisely so it cannot fail to resolve in CI.

(2) looks like the shape worth costing: it is enforcement at the point of writing, and it makes the scanner's assumption a declared rule instead of an implicit one.

Refs: #9896, #9572, #9433, #4690.


Generated by Claude Code

Activity

  1. added theissue type on Aug 21, 2026
  2. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    ContributorAuthor

    Triage: finding + domain:devx + tooling, type Task — observation class: measured 0 occurrences today, the boundary is declared in --self-test and README, and #9896 (in flight) already makes the string-literal half loud. Awaiting first-touch grading. Direction worth carrying to that grading: the card's option (2) — an ESLint rule over **/*route-ledger.ts requiring single-quoted literal route:/client: values — converts the scanner's assumption into an authoring constraint, which is the enforceable end; option (3) is rejected by the scanner's own dependency-free premise.


    Generated by Claude Code

  3. claude commented on Aug 21, 2026

    @claude
    Contributor

    Claim: /pm-dispatch domain:devx execution seat (bug-first round).
    Session: session_01DdCnBGcHeufjrq7drTD3wt
    Branch: claude/issue-10500-ledger-nonliteral-route
    Worktree: objectstack-issue-10500
    Domain: domain:devx
    File surface: scripts/docs-audit/affected-docs.mjs (parseLedgerSource / bridgeCoverageFrom) + its --self-test fixtures

    ⛔ Governed surface out of scope for the diff (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, content/docs/releases/**) and the #9465 epic fence (.changeset/ tooling, cut-rc.yml, release.yml, root package.json).
    ⭐ Changeset: AGENTS.md:943 — "Pure bug fixes do not require a changeset"; a scripts/**-only diff that publishes nothing takes the skip-changeset label via the additive POST /labels (⛔ never a whole-set PUT — see #5533). Precedent: PR #10502.


    Generated by Claude Code

  4. claude commented on Aug 21, 2026

    @claude
    Contributor
    {
      "issue": 10500,
      "status": "done",
      "branch": "claude/issue-10500-ledger-nonliteral-route",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/10637",
      "premise_still_valid": true,
      "summary": "The card's premise holds and was reproduced on a real ledger before any fix: a row spelled `route: I18N_BASE + '/plurals'` in i18n-route-ledger.ts left --bridge-coverage reporting `259 of 259 declared`, exit 0, no verdict -- the denominator never moved. parseLedgerSource now counts every `route:`/`client:` value that is a literal in NO quote (unreadableIn), so such a row is named as unread with its file, line and expression. The recognizer is untouched; `rows` is byte for byte what it was. The trap the quote heuristic guarded -- the `route: string;` member all seven entry interfaces declare -- is excluded on a SECOND exact discriminator rather than a guess: typeDeclRegions (a type member sits inside an interface/type declaration; a table row never does), plus codeOnly comment/string masking via js-comment-mask.mjs. THREE BRIEF CLAIMS WERE FALSE and are reported rather than worked around. (1) The brief's starred recommendation -- 'a real parse is likely the right answer' via scripts/ts-parse.mjs -- would have BROKEN CI: affected-docs.mjs runs in docs-drift-check.yml, the one job that deliberately never runs `pnpm install` (its own comment says so), and ts-parse.mjs opens with `import ts from 'typescript'`. Measured in a worktree with no node_modules: affected-docs.mjs --self-test exits 0 with 281 cases, while importing ts-parse.mjs fails ERR_MODULE_NOT_FOUND. The issue body's rejection of its own option (3) still stands on a live constraint. (2) PR #10612 has NOT landed -- it is open, not in the merge queue; check:parse-guard is wired into neither package.json nor any workflow on main, and the guard is consequently ALREADY RED on main (2 raw parses in check-optional-error-sink-contract.mjs). Only the guard FILE landed, via #10573. (3) affected-docs.mjs IS in the guard's scripts/** population, so a raw ts.createSourceFile was closed off too -- both parse routes shut, which is why this widens the counter only. This PR adds no TypeScript parse in any spelling and is compliant whichever way #10612 lands.",
      "tests": "All at final HEAD ba2d8d4730. Gate union re-derived with `node scripts/pm/dispatch-gates.mjs` NO PATHS PASSED (it took its own change set from merge base 5e50e2e3b; committed 2, working tree 0) -- 4 families, all green, plus check:nul-bytes which my standing rules require on any edit. Exit codes captured before any pipe (redirect-then-capture), and each verdict quoted from the gate's own printed line: `check:docs-audit-scope` EXIT=0 -> '/ affected-docs self-test: 291 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).'; `check:cross-package-test-inputs` EXIT=0 -> 'OK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.'; `node scripts/docs-audit/check-affected-docs.mjs` (the docs-drift-check.yml step) EXIT=0; `check:nul-bytes` EXIT=0 -> 'check-nul-bytes: OK (scanned 6211 text file(s) ...; no raw ASCII control bytes).'. pnpm aliases echoed their script names, so no zero-match silent-green. ESLint clean on the changed file (npx eslint, exit 0); no prettier --write, per #10622. pnpm install ran under scripts/pm/os-verify-lock.sh -> 'VERDICT command-exit 0 . held the lock 6s . waited 0s'. check-parse-guard: 0 findings in my file. Self-test 281 -> 291 cases. ABLATIONS -- three legs, each mutated, proven ON DISK BY ANCHOR COUNT (never an editor exit code), run, restored, restoration proven by anchor count again. NO REBUILD LEG EXISTS AND NONE IS CLAIMED: these are plain .mjs files with no dist/ and no build step, so nothing can be measured against a stale artifact. (A) removed the typeDeclRegions skip -- canonical `skip.some(...)` 1->0, injected `if (false) continue;` 0->1 -- 7 self-test cases red including the explicit negative control 'the `route: string;` interface member is NOT billed as an unread row' (expected false, got true), and on the REAL tree 259 of 266 with 7 broken-scan verdicts, i.e. a false red on all seven accurate ledgers exactly as the card predicted. (B) removed the unreadableIn wiring -- canonical loop 1->0, marker ABLATED-B 0->1 -- 5 cases red and the fixture returned to the pre-fix reading '1 route / 1 client / 0 declined', the original defect reproduced. (C) read raw text instead of codeOnly(text) -- canonical 1->0, marker ABLATED-C 0->1 -- exactly 1 case red ('prose and string payloads are not unread rows', got 2), and on the REAL tree it fired on the live prose at runtime/src/route-ledger.ts line 182 'route: the branch'. Restore leg for all three: anchors back to 1/1/1, markers to 0, `/ affected-docs self-test: 291 cases pass.` and 259 of 259 exit 0. END-TO-END REPRODUCTION on a real ledger (mutation proven on disk, anchor I18N_BASE 0->1, restored to 0): BEFORE = '259 of 259 declared', '221 of 221 declared', exit 0, no verdict (silent); AFTER = '259 of 260 declared', exit 1, 'i18n-route-ledger.ts is a PARTIAL read ... read 3 of 4 declared `route:` value(s) ...; declined 1: line 101: route: I18N_BASE + '/plurals''. SEVEN-LEDGER TABLE re-derived on today's tree, reproducing the card's numbers EXACTLY (route: total/quoted/non-quoted): plugin-auth 57/56/1, rest 97/96/1, runtime 81/79/2, service-datasource 12/11/1, service-i18n 4/3/1, service-settings 5/4/1, service-storage 11/10/1; totals 267/259/8. client: 221/221/0. The card's account of the 8 is TRUE: 7 are the `route: string;` interface member (one per ledger) and 1 is the English sentence in a comment at runtime/src/route-ledger.ts:182. NO non-quoted occurrence is a real row, so there is NO live blind spot today. Read / named-as-unread / SILENT: before 259/0/8, after 259/0/0 -- the 8 reach zero by being correctly EXCLUDED (none is a row), not by being reported; on the injected row it is 259/1/0. The silent count is 0 either way.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #10636: a QUOTED `client:` on a row whose `route:` could not be read is absent from the clientsDeclared denominator -- measured `3 of 3` on a file declaring four. The row itself still lands in a verdict, so this is a sub-count residue, not the silence this card is about. Searched open issues first; no duplicate. Filed unassigned, labels tooling/domain:devx/finding, no pm:queue."
      ]
    }
    

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions