Skip to content

[finding] docs-audit: the ledger row recognizer now reads codeOnly but still not typeDeclRegions — a literal-union route: TYPE member still mints a silent phantom row #10793

Description

@claude

Filed unassigned by the os-dev seat implementing #10683. Recording, not fixing — measured as a by-product of that card's before/after, and it is a second discriminator with its own population move, which the #10683 card's own rule says wants its own before/after rather than a rider. No live instance on today's tree.

What

#10683 moved parseLedgerSource's row recognizer (and the first routesDeclared term) to read through codeOnly, closing the comment/string-payload phantom. But #10500 built two exact discriminators for "is this a declaration in code position", and declarationsIn reads through both:

const code = codeOnly(text);
const skip = typeDeclRegions(code);

The recognizer now reads the first and still not the second. codeOnly does not blank type declarations — it has no reason to, they are code — so a single-quoted route: inside an interface / type body is still a row.

Measured (this branch, parseLedgerSource driven directly)

export interface Entry { route: 'GET /api/v1/gone' | 'GET /api/v1/meta'; client: string }
export const L = [
  { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },
];

⇒ rows 2 · routesDeclared 2 · clientsDeclared 1 · declined 0 · outsideCode 0

Two rows from a file declaring one — the identical silent shape #10683 fixed, arriving through the other discriminator. outsideCode (#10683's reporting half) does not see it either: the lead is in code position, so it is correctly not a prose lead. And no verdict fires, for the same reason as before — rows and routesDeclared move together, so the partial-read guard sees no gap.

The literal-union member is not hypothetical as a shape: orphanSource in the self-test pins exactly that spelling on the client: key (client: 'i18n.getLocales' | 'i18n.getPlurals'), written there specifically because "a literal-union TYPE member opens with the very quote this counter reads". The route: key is the same spelling with nothing keeping it out of the row loop.

Why it is a finding and not a live defect

All seven live ledgers declare route: string; and nothing else — scanned on this branch: 0 single-quoted route: leads inside a type declaration, across all seven. Live figures stay exact (259 of 259, 221 of 221, 176 unreachable).

Direction, not a prescription

Applying typeDeclRegions to the recognizer is one line beside the codeOnly call and reuses the existing region list. ⚠️ It moves the measured population on the same terms #10683 did, so it wants its own before/after against the --bridge-coverage header — which is exactly why it was not ridden along on that card. Measured delta today would be zero.

The alternative reading is that the row loop should stop deciding code position for itself and filter declarationsIn(text) — the file already states that design goal twice ("two scans that each decide separately what "in code position" means are two scans that can drift into disagreeing while both look right"). That is a bigger change with the same population effect.

Refs: #10683, #10500, #9896.


Generated by Claude Code

Activity

  1. huangyiirene commented on Aug 21, 2026

    @huangyiirene
    Collaborator

    Triage first-touch: promoted to queue, type Task. This is the silent direction of the pair (#10794 is the loud one and was closed not-planned): a literal-union route: type member mints a phantom row with no verdict — the identical shape #10683 fixed, one discriminator over. Scope: apply typeDeclRegions beside the codeOnly call in the row recognizer (one line, reuses the existing region list), with its own before/after against the --bridge-coverage header — expected delta today is zero, and that measurement is part of the deliverable, not optional. The wider refactor (row loop filtering declarationsIn(text)) is out of scope unless the dev shows the one-liner can't hold. Size/model suggestion: S, sonnet.


    Generated by Claude Code

  2. claude commented on Aug 21, 2026

    @claude
    ContributorAuthor

    PM claim — dispatching now.

    • session: c970724d-303c-5614-9d20-a3f92205cfad
    • branch: claude/issue-10793-typedecl-regions-recognizer
    • seat: domain:devx

    Verified before dispatch, on origin/main @ 7c02a4529c:

    The card is explicit that there is no live instance — all seven ledgers declare route: string; — so this is a latent-shape fix, and the agent is told to hold the population-move discipline the card asks for rather than to hurry.

    If an earlier claim comment with a different session ID appears above this one, that claim wins and this one is void.


    Generated by Claude Code

  3. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    Contributor

    os-dev-report

    {
      "issue": 10793,
      "status": "done",
      "branch": "claude/issue-10793-typedecl-regions-recognizer",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/10900",
      "premise_still_valid": true,
      "summary": "NO LIVE LEDGER FIGURE MOVED. Before and after are identical on all seven live ledgers — 259 of 259 rows read, 221 of 221 client-bound, 45 reachable, 176 UNREACHABLE, 0 prose-quoted leads, brokenScan empty, exit 0 — and not only in the printed counts: the whole --bridge-coverage --json report (31463 bytes, per-ledger rows and every unreachable row) is BYTE-IDENTICAL across the change, diff empty in both directions and re-confirmed after both ablations were restored. Independently scanned with the same codeOnly+typeDeclRegions pair the fix uses: 0 quoted route:/client: leads inside a type declaration across all seven ledgers (each has exactly one route: in a type declaration and it is the unquoted `route: string;` member), so the card's premise held exactly. The card's premise reproduced verbatim on the base 9faa9bc51d before a line was changed: rows 2 · routesDeclared 2 · clientsDeclared 1 · declined 0 · outsideCode 0 — two rows from a file declaring one — and rows 1 after. Took the one-line direction the card named: parseLedgerSource now takes typeDeclRegions(code) beside its codeOnly(text) call and skips a lead inside a region in TWO places that must move together — the row loop and the first term of routesDeclared — because a member counted in the denominator but skipped by the loop reads as a row the parse DECLINED, which fires a PARTIAL-read verdict on an accurate ledger (ablation B ran exactly that direction and it does fire). The wider refactor (row loop filtering declarationsIn) was not needed. The stale `⛔ ONE of #10500's two discriminators … still NOT applied` comment at the codeOnly call is rewritten, not left standing, and scripts/docs-audit/README.md's type-declaration paragraph — which described the exclusion as the COUNTER's — now states that the recognizer reads through both masks too. One deliberate non-symmetry, argued in the code rather than assumed: a skipped type member is reported NOWHERE, and that is NOT the silence #10683's outsideCode exists to break — a prose-quoted lead is a would-be row sitting where code is not, while a type member is a correct declaration of a TYPE, the same rule under which `route: string;` in all seven entry interfaces has always produced nothing and which --self-test already pinned on the client: key. The new fixtures assert that reject side positively, on both declined and outsideCode.",
      "tests": "All at HEAD bfe43f064f, clean tree (git status --porcelain empty); every exit code captured BEFORE any pipe (cmd > log 2>&1; EC=$?), and each gate quoted by ITS OWN verdict line.\n\nREPRO, BOTH DIRECTIONS. parseLedgerSource driven directly by a harness that slices the pure recognizer block out of affected-docs.mjs between text MARKERS (not line numbers), so the same harness runs unchanged before and after the edit. Base 9faa9bc51d, the card's exact fixture ⇒ 'REPRO rows 2 · routesDeclared 2 · clientsDeclared 1 · declined 0 · outsideCode 0', routes ['GET /api/v1/gone','GET /api/v1/meta']. This branch ⇒ 'REPRO rows 1 · routesDeclared 1 · clientsDeclared 1 · declined 0 · outsideCode 0', routes ['GET /api/v1/meta'], client 'meta.getTypes'. With the fix ablated (leg A below) it goes BACK to rows 2 — the card's required both-directions proof.\n\nBEFORE/AFTER ON THE REAL TREE, seven live ledgers, both legs printed. Header identical: 'ledger rows read 259 of 259 declared' / 'client-bound ledger rows 221 of 221 declared' / 'prose-quoted leads (no row) . 0' / 'reachable 45' / 'UNREACHABLE 176', per-ledger 55/46/65/0/3/0/7. Counts alone can hide a swap, so the machine-readable report was compared instead of summarised: `--bridge-coverage --json` 31463 bytes, `diff before after` EMPTY, and empty again when re-run after the ablations were restored. Independent scan for the population the fix removes: '0 quoted-in-type · 1 any-in-type' on each of the seven, TOTAL 0.\n\nSELF-TEST: 323 → 339 cases, '✓ affected-docs self-test: 339 cases pass.' (baseline on origin/main was '✓ affected-docs self-test: 323 cases pass.'). 16 new cases in the idiom orphanSource established for client:, pinned in BOTH directions because the expensive failure here is a runaway brace match that skips the member AND the table after it: the literal-union route: member is not a row while the row in code still carries 'GET /api/v1/meta → meta.getTypes'; the denominator drops it so no phantom gap opens; the member reaches NO report (neither declined nor outsideCode); deleting the interface changes no count; a member unioning a client: too mints no client-bound phantom and the survivor keeps its OWN binding; `type X = { … }` is covered as the other spelling typeDeclRegions recognises; a table BEFORE a type declaration is read whole (the runaway-brace direction); and the coverage arm pins no brokenScan, whole ratios, no prose lead.\n\nABLATION — predictions written to a file BEFORE either run, case by case with expected/got strings. NO REBUILD LEG APPLIES, stated rather than skipped: this is a plain .mjs run from source by `node scripts/...`, nothing resolves through a package exports/dist, so there is no stale-artifact path for a mutation to hide in. The fix was COMMITTED first, so each restore is a real restore point.\n  LEG A — delete `if (inTypeDecl(m.index)) continue;` from the row loop. Mutation proved ON DISK by marker count, never by an editing tool's exit code: inTypeDecl 3 → 2 and the guard line 1 → 0, git diff --stat '1 deletion'. Predicted 9 red; observed 9 — the same 9 cases, the same strings, including got \"GET /api/v1/gone → null\" for the phantom's value and got \"meta.getGone\" for the stolen binding, and 'and the ratios it must not move are whole' got \"2 of 1 route / 1 of 1 client\". EXIT=1. Also as predicted: NONE of the 323 pre-existing cases red — no existing fixture puts a QUOTED route: inside a type declaration, which is precisely why this card needed fixtures of its own.\n  RESTORE A — `git checkout HEAD -- scripts/docs-audit/affected-docs.mjs`; `git diff --quiet` EXIT=0 (byte-identical), porcelain empty, markers back to inTypeDecl 3 / guard 1, '✓ affected-docs self-test: 339 cases pass.'\n  LEG B — delete `.filter((d) => !inTypeDecl(d.index))` from the denominator (marker 1 → 0). Predicted 3 certain reds plus a 4th flagged UNCERTAIN in the written prediction (whether bridgeCoverageFrom keys its PARTIAL-read verdict on the shortfall or on the declined list). Observed 4: the uncertain one fired — 'a type-member lead carries NO broken-scan verdict': brokenScan → expected 0, got 1. That is the false RED on an accurate ledger, and it is why both terms had to move together. EXIT=1.\n  RESTORE B — `git diff --quiet` EXIT=0, porcelain empty, marker back to 1, 339 cases pass, and the --bridge-coverage --json re-compared to the origin/main baseline: still byte-identical.\n\nGATES — derived by `node scripts/pm/dispatch-gates.mjs` with NO hand-supplied paths (it takes its own change set from the merge base: 'change set derived from git — 2 path(s) vs merge base 9faa9bc51'), run AFTER the final commit. 7 families named; union re-run at bfe43f064f with a clean tree:\n  EXIT=0 pnpm check:docs-audit-scope → '✓ affected-docs self-test: 339 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).'\n  EXIT=0 pnpm check:cross-package-test-inputs → 'OK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.'\n  EXIT=0 node scripts/check-cross-package-test-inputs.mjs → same verdict line\n  EXIT=0 pnpm check:entry-guard → '✓ check:entry-guard: 129 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 87 export bindings, 77 of them inert on import (10 known-unsafe, ⛔ SHRINK-ONLY).'\n  EXIT=0 pnpm check:parse-guard → '✓ check:parse-guard: 128 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.'\n  EXIT=0 node scripts/check-ci-filter-parity.mjs → 'OK: all 82 declared cross-package glob(s) (71 unique) are covered by core or crosspkg, every crosspkg entry still covers one, and the test job's if: still names both filters.'  [needed the `yaml` dep, so `pnpm install --frozen-lockfile --ignore-scripts` was run through scripts/pm/os-verify-lock.sh — 'VERDICT command-exit 0 · held the lock 4s · waited 0s'; every other gate here is dependency-free]\n  EXIT=0 node scripts/docs-audit/check-affected-docs.mjs → '259 of 259 declared' / '221 of 221 declared' / 'prose-quoted leads (no row) . 0' / 'UNREACHABLE .. 176'\n  EXIT=0 pnpm check:nul-bytes → 'check-nul-bytes: OK (scanned 6298 text file(s) -- 6298 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes).'  [run although the derivation did NOT name it — owed by any edit]\nPlus a control-byte self-scan outside the gate over both changed files: grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' → empty, grep EXIT=1 (captured with no pipe in the way).\nNEGATIVE CONTROL for #10853, as instructed: `pnpm --filter @objectstack/definitely-not-a-package test` printed 'No projects matched the filters' and EXITED 0. No step in this PR depends on a --filter run.\n\nNo changeset: gate tooling publishes nothing, and lint.yml itself calls 'this PR edits a CI-internal script' the textbook skip-changeset case; same disposition as #10795, the #10683 fix on this file. ⚠️ DEVIATION, DISCLOSED: the dispatch said apply it additively via POST /issues/N/labels. This session has no `gh` CLI and no additive-label tool in its MCP set, so I used the standing fallback in my own instructions — read the live set first, then write the UNION — never a bare set. Read before: ['documentation','size/m'] (both bot-applied). Wrote ['documentation','size/m','skip-changeset']. Read back: ['documentation','size/m','skip-changeset'] — both bot labels survived, which is the property the additive write exists for. Draft confirmed true on read-back.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #10901: `declinedIn` reads NEITHER discriminator (not codeOnly, not typeDeclRegions), so the same literal-union `route:` TYPE member written in either OTHER quote is billed as an unread row. Measured on this branch with the real bridgeCoverageFrom: a double-quoted union member ⇒ rows 1 · routesDeclared 2 · declined 1 · brokenScan 1, naming 'x-route-ledger.ts is a PARTIAL read … read 1 of 2 declared route: value(s) … declined 1: line 1: route: \"GET /api/v1/gone\"' — a false RED and exit 1 on a ledger declaring exactly one row; the backtick spelling behaves identically. Distinct from #10794 (closed not planned), which is the same scan reading raw PROSE and whose remedy is codeOnly: a type member is genuinely in code position, so codeOnly cannot reach it and only typeDeclRegions can — and after this card declinedIn is the LAST route:/client: scan in the file reading through neither. Pre-existing and untouched by this PR. 0 live instances (0 quoted leads inside a type declaration in ANY quote, across all seven ledgers). Filed unassigned, no labels, left for PM triage."
      ]
    }

    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