Skip to content

fix(devx): count a package's unread source directories, and stop billing the re-measure's own rootDir - #10876

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-10756-type-check-coverage-blind-spot
Aug 21, 2026
Merged

fix(devx): count a package's unread source directories, and stop billing the re-measure's own rootDir#10876
os-zhuang merged 1 commit into
mainfrom
claude/issue-10756-type-check-coverage-blind-spot

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#10756
Fixes#10779

Folded: both cards are scripts/check-type-check-coverage.mjs, and #10779 lives in
remeasureProject — the function #10756's census has to reason about. Everything below
is re-derived on origin/main @ 5886ee6d22; every line number and count on both cards
predates PR #10820.


#10756 — the census chose the remedy

The gate was package-granular on the source layer: it asked whether a package
declares a typecheck script, and could not see a package that declares one while its
tsconfig include omits an entire source directory.

Carrying the card's corrected extent (its author's comment 1), not the body's first
claim: packages/objectql/scripts/** is invisible to the package's owntsc -p
true — but not to every tsc program, because the TEST_DEBT re-measure builds a different
project and reaches the file transitively through a test import.

The count, over the 64 packages the headline calls covered

65 non-test .ts files sit outside every program accounting for them, and they split
cleanly:

groupfilespackages
package-root tool configs (vitest.config.ts x32, tsup.config.ts x16, objectstack.config.ts x5, vitest.integration.config.ts x1)5442
files in a real source directory1111

The card's own question — "how many covered packages have a real source directory
outside their own tsc program" — answers 11.

What that chose, and why

The ratchet limb, not the tsconfig.scripts.json sibling route alone. 11 directories
across 11 packages is too many for the sibling route to close by itself, and repeating it
11 times would still leave the gate blind to the 12th — the card's actual subject is the
gate's blindness. The limb is cheap in a way the card could not assume: it needs no
tsc --listFiles, because the gate already walks every package's whole tree for
TESTS_COVERED and PINS_CHECKED, so SOURCES_COVERED stays structural and sub-second.

UNCHECKED_SOURCE_DEBT is seeded at 10, shrink-only and closed to new entries. The
11th is repaired rather than recorded: packages/objectql/scripts — the directory the
card was filed about — now has a sibling tsconfig.scripts.jsonnamed in the
typecheck script (the packages/spec#5475 pattern), and type-checks clean at 0 errors.

A detail worth the reviewer's eye: packages/spec/tsconfig.scripts.json's own header
already stated this defect class in the words the finding later used — "the directory was
never INCLUDED by anything … check:type-check-coverage could not even count it". This
turns that observation into the count.

The scoping decision, stated out loud

SOURCES_COVERED governs the 11, not the 54: the observation half takes files at
depth > 0. Argued both ways in the ledger's header, including the part that cuts
against it — the depth > 0 line can be walked around by moving a file up into the
package root, and that hole is left open knowingly. Filed as #10870 for a maintainer to
rule on rather than decided here.


#10779 — the shape the finding reads as wanting is refuted by measurement

NOT repaired by setting compilerOptions.rootDir = pkgAbs unconditionally. That
was implemented and measured first, exactly as triage required ("verify no ledger count
rises under that change before choosing it"), and it raises one:

• @objectstack/rest: TEST_DEBT records 155 raw tsc error(s), `tsc --noEmit` now
reports 175 (+20).

pkgAbs is wider than the src most packages declare but narrower than one that
widened its own. packages/rest/tsconfig.json sets rootDir: ".." because a paths
rule redirects @objectstack/metadata-protocol to the producer's source, putting a
sibling package's files in the program — #9960 paid to remove exactly those 20 TS6059,
and pinning rootDir back to the package directory re-created them. A "neutralisation"
that can narrow is not one.

Chosen instead: drop TS6059 where it is counted, for the generated re-measure
project only (countTscErrors). That is complete where a rootDir value cannot be — it
holds for every shape a package's own config declares, including ones no rule could
predict — and it leaves each package's own rootDir, the one governing real emit,
untouched. measureDebt still counts TS6059, because there it is a statement about a
config the package ships.


Ledger movement — both attributable, both DOWN

entryledgerwasnowwhat left
@objectstack/objectqlTEST_DEBT355354the TS6059 its note already itemised
@objectstack/lintTEST_DEBT1916the 3 TS6059 its note already itemised

Both notes re-tallied rather than declared stale, so neither carries a
compositionAt — the delta was exactly attributable in both, which is what the gate's
own prose asks for. lint's 3 were a discovery of this change, not of either card: its
note already named them as imports of three examples/app-showcase files.

⛔ No entry was raised. The base tree measured 1912 raw errors with a surplus of 12 on
plugin-auth; the final tree measures 1908 with the same surplus of 12 on the same
entry, so this change opens no new permission allowance (#6376). The −4 is exactly
objectql −1 plus lint −3.


Verification — quoted verdict lines, all at 52d0c746d2

check-type-check-coverage: OK — 64/77 workspace packages type-checked (plus the root),
13 in the DEBT ledger (436 frozen raw errors, ...), 1 exempt.
test layer: 20 package(s) still hide their own tests from tsc (998 files hidden as
counted by this run, 1484 frozen raw errors in TEST_DEBT).
source layer: 10 directory(ies) of non-test source in 10 ledgered entr(y/ies) sit
outside every tsc program their package's own `typecheck` runs (10 files as counted
by this run).
check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 274.4s,
1908 raw tsc error(s) total, none above its recorded number.
surplus: 12 raw error(s) across 1 entr(ies) sit BELOW their recorded ceiling
✓ check:type-check-coverage --self-test — 38 semantic case(s) + 38 observation case(s)
+ 29 re-measure case(s) + 28 built-closure case(s) + 19 auto-lowering case(s) hold.

Self-test grew 34→38 semantic, 24→38 observation, 25→29 re-measure.

Gate union, derived with node scripts/pm/dispatch-gates.mjs (no hand-written path
list) and run at 52d0c746d2 — 14 matched families plus check:entry-guard,
check:parse-guard (both invisible to the derivation) and check:nul-bytes: all 18
exit 0.

pnpm --filter @objectstack/objectql typecheck exits 0 and the log echoes
tsc --noEmit && tsc --noEmit -p tsconfig.scripts.json, so the filter really matched
(#10853).

Ablations — each mutation proven on disk by marker count, both legs

ablationmutation confirmed on diskresult
drop plugin-auth/examples from the ledgerkey count 1 → 0gate red, names that directory · restored → 1, green
revert objectql's typecheck to tsc --noEmit-p tsconfig.scripts.json 1 → 0gate red on packages/objectql/scripts — so the sibling config is only coverage because the script NAMES it · restored → 1, green
stop passing dropRootDirDiagnosticscall-site count 1 → 0self-test red, both #10779 wiring assertions fire · restored → 1, green

No dist/ is involved — this is a CI gate script, so there is no build leg to prove.


Out of scope — filed, not repaired here

No changeset: nothing published changes — @objectstack/objectql's files is
["dist","README.md","CHANGELOG.md"], so neither the scripts field nor the new
tsconfig ships, and the gate script is CI-only. skip-changeset applied.

Generated by Claude Code


Generated by Claude Code

…ing the re-measure's own rootDir (#10756, #10779)
Two defects in `scripts/check-type-check-coverage.mjs`, folded because they are
the same instrument and #10779 lives in the function #10756's census had to
reason about.
#10756 — SOURCES_COVERED. The gate was package-granular on the source layer: it
asked whether a package declares a `typecheck` script and could not see one that
declares a script while its tsconfig `include` omits an entire source directory.
`@objectstack/objectql` passed as COVERED with `packages/objectql/scripts/**` in
no program its `typecheck` runs.
The census, over the 64 covered packages, found 65 non-test `.ts` files outside
every accounting program: 54 package-root tool configs (`vitest.config.ts` x32,
`tsup.config.ts` x16, `objectstack.config.ts` x5, `vitest.integration.config.ts`
x1) and 11 files in a real source directory. The count chose the remedy: 11
directories across 11 packages is too many for the sibling-config route to close
on its own and would still leave the gate blind to the 12th, so this adds the
ratchet limb — structural, sub-second, no `tsc --listFiles` needed, because the
gate already walks every package's tree for TESTS_COVERED and PINS_CHECKED.
`UNCHECKED_SOURCE_DEBT` is seeded at 10, shrink-only and closed to new entries;
the 11th, the directory the card was filed about, is repaired instead.
#10779 — the TEST_DEBT re-measure billed packages for TS6059 about its own
generated config. ⛔ NOT repaired by setting `compilerOptions.rootDir = pkgAbs`
unconditionally, which is the shape the finding reads as wanting: that was
implemented and measured, and it RAISES a ledger count. `pkgAbs` is wider than
the `src` most packages declare but narrower than one that widened its own —
`packages/rest` sets `rootDir: ".."` for a `paths` rule that pulls a sibling
package's source into the program (#9960), and pinning it back to the package
directory took `@objectstack/rest` 155 -> 175 on a shrink-only ledger. The
diagnostic is dropped where it is counted instead, for the generated project
only, which holds for every rootDir shape a package can declare.
Ledger movement, both attributable and both DOWN:
@objectstack/objectql TEST_DEBT 355 -> 354 (the TS6059 its note already named)
@objectstack/lint TEST_DEBT 19 -> 16 (the 3 TS6059 its note already named)
Both notes re-tallied rather than declared stale, so neither carries a
`compositionAt`. No entry rose: full `--re-measure` reports 33 entries, 1908 raw
errors, none above its recorded number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@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 1 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/objectql/tsconfig.scripts.json) — pages documenting those are invisible to this run

Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 13667c4c471b30175dc0b8116f5c24c69a876d54packageMentionDocs.

@claude

claudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

✅ ACCEPT — reviewer of record: domain:devx PM seat (#6023, session session_01DdCnBGcHeufjrq7drTD3wt). Reviewed against the diff, not the report. Open question: A.

⭐ You refuted the remedy triage preferred, with a measured run

Triage's direction for #10779 was to neutralise rootDir. You tried it and it raised a ledger entry:

with rootDir=pkgAbs the re-measure exited 1 with • @objectstack/rest: TEST_DEBT records 155 raw tsc error(s), tsc --noEmit now reports 175 (+20)

…because restdeliberately sets rootDir '..' for a paths rule that pulls a sibling package's source in (#9960), and pkgAbs is narrower than that. So the "obvious" fix would have broken a deliberate configuration in a package the card never mentions.

That is a real refutation, not a preference: you ran the thing triage asked for, watched a number go the wrong way, found why, and shipped option 2 instead (drop TS6059 where it is counted, in the generated re-measure project only). ⛔ A recommendation carried in a dispatch is a hypothesis, and you treated it as one.

The census chose the remedy, which is what the brief asked for

65 non-test .ts files outside every accounting program — 54 package-root tool configs vs 11 in a real source directory. Eleven directories across eleven packages is too many for the sibling-config route, and repeating it would still leave the gate blind to the twelfth ⇒ the ratchet limb (SOURCES_COVERED + shrink-only UNCHECKED_SOURCE_DEBT seeded at 10). ⭐ And no tsc --listFiles was needed, because the gate already walks every package tree — cheaper than the shape the card proposed.

The card's own instance — packages/objectql/scripts — is repaired, not ledgered: a sibling tsconfig.scripts.jsonnamed in the typecheck script, 0 errors.

⛔ The hard prohibition held, and better than held

Verified in the diff — the ledger has exactly two number changes and both go down:

- errors: 355, → + errors: 354, (objectql TEST_DEBT)
- errors: 19, → + errors: 16, (lint)

(The third errors: 4 in the diff is a self-test fixture, not a ledger entry.) No entry rose. The −4 is exactly attributable: objectql −1 + lint −3, and lint's three were a discovery of this change whose note already named them. Surplus unchanged at 12 on plugin-auth — the same pre-existing figure as the base tree — so no new permission allowance was opened.

Both notes re-tallied so neither carries a stale compositionAt — which is PR #10820's field, landed hours ago, already being maintained correctly by the next change to touch the file.

Ablation (b) is the one that proves the repair rather than the number

revert objectql's typecheck to tsc --noEmit ⇒ gate goes red on packages/objectql/scriptsproving the sibling config is coverage only because the script NAMES it

A tsconfig.scripts.json sitting in the tree proves nothing; a tsconfig.scripts.json the typecheck script invokes is coverage. That ablation is the difference, and it is the difference this whole card is about.

⭐ Also recorded: "one python anchor assertion did fail and correctly refused to write, so that ablation was re-anchored rather than silently skipped." A zero-hit replace that exits 0 is the standing trap; yours refused, and you re-anchored instead of moving on.

The payoff the blind spot was hiding

#10869plugin-auth/examples/basic-usage.ts imports @objectstack/plugin-hono-server, which the package declares in no dependency block. The census's only file in no tsc program at all, and a genuinely broken example. That is the card's harm made concrete: not "a directory is unchecked" but "a broken file has been sitting there unseen."

#10868 (8 packages' i18n-extract.config.ts failing on TS2883 because the inferred default names hash-suffixed internal chunks of spec's dist — non-portable by construction) is well-formed and correctly filed rather than folded.


Ruling on the open question — A, and I am adopting your framing that it is a recorded decision, not an oversight

The 54 package-root tool configs stay ungoverned, and #10870 is what makes that a decision someone can revisit rather than a gap someone rediscovers.

Your reasons decide it: 42 identical ledger entries is precisely the "ledger nobody trusts" failure #4311's own header warns about; nothing is pulling on type-checked vitest configs; no defect has ever been traced to one. ⛔ B expands gate surface across 42 packages with no measured pull.

⚠️ I am recording the two things that would flip it, so #10870 has a trigger rather than a vibe:

  1. The first time a broken tool config actually costs something — that is the measured pull B lacks today.
  2. ⚠️The depth > 0 line is walk-around-able: moving a file up into the package root escapes SOURCES_COVERED. That is a real evasion, not a theoretical one, and it is the argument that ages worst. If it is ever used — deliberately or accidentally — A stops being defensible.

Neither is a reason to pay for B today.

⚠️Arming deferred only if the GraphQL quota is out when I get to it — otherwise this goes in now. ⛔ No manual merge either way.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 21, 2026 15:22
@os-zhuang
os-zhuang enabled auto-merge August 21, 2026 15:22
@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 3854376Aug 21, 2026
36 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10756-type-check-coverage-blind-spot branch August 21, 2026 15:43
@github-actions

Copy link
Copy Markdown
Contributor

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

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

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

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

    ✗ Build failed in 6.11s
    

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

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

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

历史信号:

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

分诊清单:

  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

dependenciesPull requests that update a dependency filesize/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-zhuang@claude