Skip to content

fix(devx): read the ledger row recognizer through typeDeclRegions too - #10900

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-10793-typedecl-regions-recognizer
Aug 21, 2026
Merged

fix(devx): read the ledger row recognizer through typeDeclRegions too#10900
os-zhuang merged 2 commits into
mainfrom
claude/issue-10793-typedecl-regions-recognizer

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#10793

The defect

declarationsIn has read through both of #10500's exact discriminators since it was
written:

constcode=codeOnly(text);constskip=typeDeclRegions(code);

The row recognizer read only the first. codeOnly blanks comments and string contents;
it does not blank type declarations, and has no reason to — they are code. So a
single-quoted route: inside an interface / type body was still assembled into a
row.

It was silent in the same two ways as the comment case #10683 closed: rows and
routesDeclared moved together, so the partial-read guard saw no gap, and outsideCode
could not see it either, because the lead genuinely is in code position.

The gap was self-documented in the file — the comment above const code = codeOnly(text)
read ONE of #10500's two discriminators, not both. That comment is now updated rather
than left standing.

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

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' },
];
legresult
base 9faa9bc51drows 2 · routesDeclared 2 · clientsDeclared 1 · declined 0 · outsideCode 0
this branchrows 1 · routesDeclared 1 · clientsDeclared 1 · declined 0 · outsideCode 0

Two rows from a file declaring one, exactly as the card measured — and one row after.

The change

parseLedgerSource now takes typeDeclRegions(code) beside its codeOnly(text) call and
skips a lead that falls inside a region, in two places that must move together:

  • the row loop (if (inTypeDecl(m.index)) continue;), and
  • the first term of routesDeclared.

Both, or the partition rows + declined === routesDeclared breaks: a member counted in the
denominator but skipped by the loop reads as a row this parse declined to read, and fires
a PARTIAL read verdict on an accurate ledger. The ablation below runs that direction on
purpose.

This is the one-line direction the card named. It reuses the same region list
declarationsIn reads rather than growing a second idea of what a type member is; the wider
refactor (the row loop filtering declarationsIn(text)) was not needed and is not here.

Why the skipped member is reported nowhere

#10683 paired its mask with outsideCode so the fix would not trade a phantom row for a new
silence. That argument does not carry over, and the PR says so in the code. A
prose-quoted lead is a would-be row sitting where the mask says code is not, so naming it
tells the reader something. A type member is a correct declaration of a type: the
route: string; member all seven entry interfaces declare has always produced nothing under
the same rule, declarationsIn has excluded the literal-union spelling since #10500, and
--self-test already pinned that deleting the interface changes no count at all. This makes
the recognizer agree with the position the rest of the file already held rather than adding
a new one — and the new fixtures assert that reject side positively, on both declined and
outsideCode.

Population move: measured, and it is zero

The standard the --bridge-coverage header attaches to. Both legs run against the real
tree.

base 9faa9bc51dthis branch bfe43f064f
ledger rows read259 of 259 declared259 of 259 declared
client-bound rows221 of 221 declared221 of 221 declared
reachable4545
UNREACHABLE176176
prose-quoted leads00
brokenScan / exitnone, 0none, 0

Counts alone can hide a swap, so the whole machine-readable report was compared, not the
header: --bridge-coverage --json (31463 bytes, per-ledger rows and every unreachable row)
is byte-identical across the two legs — diff empty in both directions, and re-confirmed
after the 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
ledger has exactly one route: in a type declaration and it is the unquoted route: string;
member. So the card's premise held and the move is free today in a way it stops being the
moment one ledger unions its route: member.

No live figure moved.

Tests

--self-test goes 323 → 339, in the idiom orphanSource established for the client:
key — that fixture already pins this exact spelling on client:, written there because a
literal-union type member opens with the very quote the counter reads. These are its route:
twins, which had nothing keeping them out of the row loop.

Pinned in both directions, because the expensive failure here is the opposite one: a
brace match that ran away would skip the member and the table after it, passing any test
that only asserted the phantom is gone while silently dropping all 259 live rows. So the new
cases assert what is dropped and what is kept, value included:

  • the literal-union route: member is not a row, and the row in code still is, carrying
    GET /api/v1/meta → meta.getTypes;
  • the denominator drops it too, so no phantom gap opens, and nothing is billed as declined;
  • the member reaches no report at all — not declined, not outsideCode;
  • deleting the interface changes nothing, so it contributed no count;
  • a member unioning a client: too mints no client-bound phantom (the expensive shape: such
    a phantom joins the UNREACHABLE population, since no registrar tail matches a route nobody
    mounts) and the surviving row keeps its own binding;
  • type X = { … }, 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, and no prose lead.

Ablation

Predictions written down before either run. Every mutation proved on disk by marker
count
, never by an editing tool's exit code, and each leg restored with
git checkout HEAD -- affected-docs.mjs + git diff --quiet (exit 0 = byte-identical) before the next.
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
a mutation to hide in.

A — remove if (inTypeDecl(m.index)) continue; from the row loop (marker
inTypeDecl 3 → 2, guard line 1 → 0). Predicted 9 reds, named case by case with their
expected/got strings; observed 9, the same cases, the same strings — including
got "GET /api/v1/gone → null" for the phantom's value and got "meta.getGone" for the
stolen binding. The card's own reproduction went back to 2 rows under this leg. Both
directions, as the card required.

B — remove .filter((d) => !inTypeDecl(d.index)) from the denominator (marker 1 → 0).
Predicted 3 certain reds plus a fourth flagged uncertain — whether bridgeCoverageFrom
keys its PARTIAL read verdict on the shortfall or on the declined list. Observed 4: the
uncertain one fired, brokenScan → expected 0, got 1. That is the false red on an accurate
ledger, and it is why both terms had to move together.

Also recorded, because it is the card's thesis reproduced in the harness: under ablation A
none of the 323 pre-existing cases red. No existing fixture puts a quotedroute:
inside a type declaration, which is exactly why this needed fixtures of its own.

Gates

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

gateexitverdict line
pnpm check:docs-audit-scope0✓ 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).
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
pnpm check:entry-guard0✓ 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).
pnpm check:parse-guard0✓ check:parse-guard: 128 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
node scripts/check-ci-filter-parity.mjs0OK: 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.
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:nul-bytes0check-nul-bytes: OK (scanned 6298 text file(s) ... no raw ASCII control bytes).

check:nul-bytes was run although the derivation did not name it — it is owed by any edit. A
control-byte self-scan over both changed files outside the gate came back empty. The
--filter zero-match trap (#10853) was exercised as a negative control:
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.

No changeset: gate tooling publishes nothing, and "this PR edits a CI-internal script" is
the case lint.yml itself calls the textbook one. skip-changeset applied additively, then
read back. Same disposition as #10795, the #10683 fix on this file.

Also in this PR

scripts/docs-audit/README.md — its type-declaration paragraph described the exclusion as
the counter's. After #10683 and #10793 the recognizer reads through both masks too, and a
paragraph that stops at the counter reads as if the recognizer still parses prose. It now
states both, and states why a skipped type member is reported nowhere.

Deliberately not in this PR

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


Generated by Claude Code

The recognizer read through codeOnly (#10683) but not through typeDeclRegions,
which is the second of #10500's two exact discriminators for 'is this
declaration in code position'. Type declarations are code, so a literal-union
`route: 'GET /a' | 'GET /b'` TYPE member still minted a row — silently, for the
same two reasons as the comment case: rows and routesDeclared moved together so
the partial-read guard saw no gap, and outsideCode could not see it because the
lead genuinely is in code position.
The row loop and the first term of the denominator now skip the same region list
declarationsIn has read since #10500, so the two scans answer the question the
same way instead of drifting while both look right.
Measured across all seven live ledgers, before and after: identical — 259 of 259
rows read, 221 of 221 client-bound, 176 unreachable, and the full
--bridge-coverage --json byte-identical. 0 quoted route:/client: leads sit
inside a type declaration today.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
The README's type-declaration paragraph described the exclusion as the
COUNTER's. After #10683 and #10793 the recognizer reads through both masks
too, and a paragraph that stops at the counter reads as if the recognizer
still parses prose.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation labels Aug 21, 2026
@os-zhuangos-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 21, 2026 — with Claude
@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 16:40
@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 58563beAug 21, 2026
33 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10793-typedecl-regions-recognizer branch August 21, 2026 16:55
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32504138883 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 5.96s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 37 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-zhuang@claude