Repository navigation
[finding] A package can pass check:type-check-coverage with a whole source directory outside tsc — packages/objectql/scripts/** is invisible to its own typecheck #10756
Description
Measured while implementing #10269 (PR #10755). Not repaired there: that card's surface
is four entry guards, and this is a property of the tsconfig include globs.
The measurement
@objectstack/objectql declares a typecheck script, passes it, and is counted as
covered by check:type-check-coverage (64/77 workspace packages type-checked ... 13 in the DEBT ledger, 1 exempt — objectql is in neither ledger). But its tsconfig is
{ "include": ["src/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts"] }so packages/objectql/scripts/** is outside the program the package's own
typecheck compiles (see the CORRECTION below: that is narrower than "no tsc program
at all", which is what this body first claimed). Ablation on
origin/main + the #10269 branch, at ca022387f2:
inject const OS_ABLATION_TYPE_ERROR: number = "definitely not a number";
into packages/objectql/scripts/dry-run-hash-compat.ts
confirm on disk grep -c OS_ABLATION_TYPE_ERROR -> 1
pnpm --filter @objectstack/objectql typecheck -> EXIT 0, zero `error TS` lines
pnpm exec tsc --noEmit --listFiles | grep -c scripts/dry-run-hash-compat.ts -> 0
restore, confirm on disk grep -c OS_ABLATION_TYPE_ERROR -> 0
The file appears zero times in tsc's own file list, and a number initialised with a
string sits in it without moving the gate.
CORRECTION (2026-08-21) — "in no tsc program at all" was too broad
This body originally read "so packages/objectql/scripts/** is in no tsc program at
all". That is wrong as written, and PR #10755 going red is what measured it. The
accurate statement is the one already in the title: that directory is invisible to
the package's own tsc -p, and it is not invisible to every tsc program.
The TEST_DEBT re-measure builds a different project. measureTestDebt calls
remeasureProject (scripts/check-type-check-coverage.mjs), which copies the package
tsconfig and drops every test glob from exclude — so
packages/objectql/src/dry-run-hash-compat.test.ts becomes a root file, and its
line 5 is:
import { runDryRun, type LegacyMetadataRow } from '../scripts/dry-run-hash-compat';tsc pulls every transitively imported file into the program regardless of include,
so the script is compiled there. It is reached through the test, not through
include.
Measured: the import.meta.url that PR #10755 added to that script (the package
declares no "type", so under the repo-wide module: NodeNext it compiles as
CommonJS) produced exactly one TS1470 in the re-measure and drove
@objectstack/objectql from its frozen TEST_DEBT of 355 to 356 — a red
Type Check · debt ledger — while pnpm --filter @objectstack/objectql typecheck
stayed at exit 0 the whole time.
Why the ablation above could not see it: it ran the package's typecheck and a
--listFiles over the package's own config, and that is precisely the program the
file is missing from. It never built the test-inclusive project.
The finding stands and this card stays open. The blind spot is real, and the
accidental coverage described here argues for the gate rather than against it: it is
not a property anyone declared. It holds only for the files some test happens to
import, only while that import exists, and it reports through a shrink-only debt
ledger — as a drifted count on a package the ratchet calls covered — rather than
through the package's own typecheck.
Why this is worth a card rather than a shrug
packages/objectql/scripts/dry-run-hash-compat.ts is not scratch: it is a
compatibility checker with a documented CLI, and packages/objectql/src/dry-run-hash-compat.test.ts
imports runDryRun from it. So a package test depends on a module whose types nothing
verifies.
The same shape holds for packages/core/examples/** (include: ["src/**/*"],
rootDir: "./src"), which docs/qa/platform-checklist/FOLLOW-UPS.md already notes in
passing for phase2-integration.ts — but @objectstack/core has no typecheck script at
all and is a ledgered DEBT entry (#4311), so that half is already tracked. The objectql
half is not: it is a package the ratchet reports as covered.
The gap in the ratchet, stated precisely
check:type-check-coverage is package-granular: it asks whether a package declares a
typecheck script or carries a ledger entry. It cannot see a package that declares one
and whose tsconfig include omits a real source directory. That is the same class AGENTS.md
already warns about for test files —
Never
exclude*.test.ts/*.spec.tsfrom a package'stsconfig.json—tsc --noEmitreads that config, so the exclusion hides the tests from the very check the
typecheckscript advertises (a green gate over source nothing read).
— one level up: a whole directory rather than a file glob, and reached through include
rather than exclude, so the existing prose does not cover it.
Not proposing the remedy here
Several shapes are possible (a sibling tsconfig.scripts.json named in the typecheck
script, the packages/spec pattern; widening include; or a ratchet limb that compares a
package's tracked .ts files against tsc --listFiles), and which one is right depends on
how many packages are in this state — which nobody has counted. Filed so the count gets
made rather than guessed.
Dedupe
Grepped the titles and bodies of all 299 open issues for packages/objectql/scripts,
packages/core/examples, include.{0,20}src/\*\*, outside (every|any) tsconfig and
--listFiles. Only #10269 matches, and only because it names the two paths.
(search_issues was unavailable to this seat.)
Generated by Claude Code
Activity
claude commentedon Aug 21, 2026
Correction: the extent was overstated — the blind spot itself is real, and this stays open
Filed from PR #10755, which went red on
Type Check · debt ledgeratca022387f2andis what measured this.
What this card claimed:
packages/objectql/scripts/**"is in no tsc program atall".
What is true:
packages/objectql/scripts/**is invisible to the package's owntsc -p— that half is exactly right,includeis["src/**/*"]and the directory isnot under it — but it is not invisible to every tsc program.
The TEST_DEBT re-measure builds a different project.
measureTestDebtcallsremeasureProject(scripts/check-type-check-coverage.mjs), which copies the packagetsconfig and drops every test glob from
exclude. That makespackages/objectql/src/dry-run-hash-compat.test.tsa root file, and its line 5 is:tsc pulls every transitively imported file into the program regardless of
include, sothe script is compiled there — reached through the test, not through
include.Measured, both directions, with objectql's dependency closure built first:
TEST_DEBT['@objectstack/objectql']is frozen at 355, so the extra error read as anupward ledger drift on a package the coverage ratchet reports as covered.
Why the ablation in the body could not see this: it ran the package's own
typecheckand a
--listFilesover the package's own config, and that is precisely the programthe file is missing from. It never built the test-inclusive project. The ablation was
sound; only the conclusion drawn from it reached past what it measured.
Not closing this, and the correction argues for the gate rather than against it.
The accidental coverage is not a property anyone declared: it holds only for the files
some test happens to import, only while that import exists, and it reports as a drifted
count on a shrink-only debt ledger rather than through the package's own
typecheck.A source directory unread by the check its package advertises is still the finding.
The body has been corrected in place; the title was already accurate.
Generated by Claude Code
Generated by Claude Code
huangyiirene commentedon Aug 21, 2026
Triage (finding round): promoted →
pm:queue, type Task. Scope as the card frames it: first the census (how many "covered" packages have a real source directory outside their own tsc program), then the remedy sized to the count — thetsconfig.scripts.jsonsibling pattern if few, a ratchet limb comparing tracked.tsfiles againsttsc --listFilesif many. Strengthening only; no gate weakening in scope. Fold-or-serial note for the devx seat: pairs with #10779 (same instrument,scripts/check-type-check-coverage.mjs/remeasureProject) — answer the five-gate test for the pair before dispatch; different defect shapes (include-glob blind spot vs TS6059 billing), so serial with shared context is the likely answer, but one dev taking both with per-card commits is defensible.Generated by Claude Code
claude commentedon Aug 21, 2026
Claim:
/pm-dispatchdomain:devx execution seat — folded dispatch: #10756 + #10779.Session:
session_01DdCnBGcHeufjrq7drTD3wt· Branch:claude/issue-10756-type-check-coverage-blind-spot· Worktree:objectstack-issue-10756Container & model:
M-L,mode:subagent,model: opusFile surface:
scripts/check-type-check-coverage.mjs(+ possiblypackages/objectql/tsconfig*.json).fold-or-serial — answered, as triage required
FOLD. Both cards are the same instrument (
scripts/check-type-check-coverage.mjs) and #10779 is specificallyremeasureProject, which #10756's own census has to reason about. Serialising them would make the second agent re-derive the first's measurements on a file the first just changed. ⛔ Neither is dispatched separately; this claim covers both.⭐ Serial cleared: PR #10820 rewrote this file (+469/−19, adding
compositionAtand a composition check overTEST_DEBT) and merged at ~13:45Z. Branch from currentorigin/main; every line number and count on both cards predates it, so re-derive everything.packages/objectql/scripts/**is invisible to the package's owntsc -p, which is true — but it is not invisible to every tsc program, because the TEST_DEBT re-measure builds a different project. Carry the corrected extent, not the body's first claim.Scope from triage: census first (how many "covered" packages have a real source directory outside their own tsc program), then a remedy sized to the count — the
tsconfig.scripts.jsonsibling pattern if few, a ratchet limb comparing tracked.tsfiles againsttsc --listFilesif many. ⛔ Strengthening only; no gate weakening in scope, and ⛔ never raise the shrink-only TEST_DEBT baseline.Standing constraints
docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md— propose wording in the report.content/docs/releases/**. ⛔ Nevergit stash—refs/stashis shared by every worktree of the repo.cmd > log 2>&1; ec=$?.pnpm --filter <typo>exits 0 with "No projects matched the filters" — a whole verification round can be fictitious and read as green #10853):pnpm --filter <name>with a name that does not exist printsNo projects matched the filtersand EXITS 0. A whole verification round can be fictitious and read as green. Verify your filter actually matched before trusting any run. (My own briefing carried a wrong package name this morning and a dev caught it.)POST /issues/N/labels), never a whole-set PUT; read them back after the size labeler runs.Report back to me; I review against GitHub and land it.
Generated by Claude Code
fix(devx): count a package's unread source directories, and stop bill…
claude commentedon Aug 21, 2026
Folded dispatch #10756 + #10779 — draft PR #10876
{ "issue": 10756, "status": "done", "branch": "claude/issue-10756-type-check-coverage-blind-spot", "pr": "https://github.com/objectstack-ai/objectstack/pull/10876", "premise_still_valid": true, "summary": "Folded #10756 + #10779, one draft PR, per-card commits' worth of reasoning in one commit. #10756: censused all 64 covered packages and found 65 non-test .ts files outside every accounting program — 54 package-root tool configs vs 11 in a real source directory. The count chose the RATCHET LIMB (new SOURCES_COVERED invariant + shrink-only UNCHECKED_SOURCE_DEBT seeded at 10), because 11 directories across 11 packages is too many for the sibling-config route to close and repeating it would still leave the gate blind to the 12th; the limb needs no `tsc --listFiles` since the gate already walks every package tree. The 11th — packages/objectql/scripts, the card's own instance — is REPAIRED with a sibling tsconfig.scripts.json named in the typecheck script (0 errors). #10779: the remedy triage favoured is REFUTED by measurement — setting compilerOptions.rootDir = pkgAbs unconditionally RAISES @objectstack/rest 155 -> 175, because rest deliberately sets rootDir '..' for a paths rule pulling a sibling package's source in (#9960), and pkgAbs is NARROWER than that. Implemented option 2 instead: TS6059 is dropped where it is counted, for the generated re-measure project only. Two ledger entries moved, both DOWN and both exactly attributable, both notes re-tallied so neither carries a compositionAt: objectql TEST_DEBT 355 -> 354, lint 19 -> 16 (lint's 3 were a discovery of this change; its note already named them). No entry rose, and the surplus is unchanged at 12 on plugin-auth — the same pre-existing figure as the base tree — so this opens no new permission allowance.", "tests": "All final numbers at sha 52d0c746d2. (1) Gate union derived with `node scripts/pm/dispatch-gates.mjs` with NO hand-written path list (14 matched families), plus check:entry-guard and check:parse-guard by hand (both invisible to the derivation) and check:nul-bytes: 18/18 EXIT=0, each captured as `cmd > log 2>&1; ec=$?` with no pipe. (2) check:type-check-coverage verdict line: '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) ... 1484 frozen raw errors in TEST_DEBT' + NEW '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).' (3) check:type-check-debt verdict line at 52d0c746d2: '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)'. Base tree measured 1912 with surplus 12 on plugin-auth; delta -4 = objectql -1 + lint -3 exactly. (4) Self-test: '✓ 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.' (grew 34->38, 24->38, 25->29). (5) `pnpm --filter @objectstack/objectql typecheck` EXIT=0 and the log echoes 'tsc --noEmit && tsc --noEmit -p tsconfig.scripts.json', proving the filter matched (#10853 trap). (6) THE REFUTATION was a real measured run, not an argument: 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)'. ABLATIONS — three, each mutated and RESTORED, every leg's mutation confirmed on disk by marker count before any result was read, never by an editor's exit code (one python anchor assertion did fail and correctly refused to write, so that ablation was re-anchored rather than silently skipped): (a) drop the plugin-auth/examples ledger key, count 1->0, gate goes red naming that directory; restored 0->1, green. (b) revert objectql's typecheck to `tsc --noEmit`, marker 1->0, gate goes red on packages/objectql/scripts — proving the sibling config is coverage only because the script NAMES it; restored 0->1, green. (c) remove `dropRootDirDiagnostics: true`, call-site count 1->0, self-test goes red with both #10779 wiring assertions firing; restored 0->1, green. No dist/ or build leg is involved — the subject is a CI gate script, so ablation-dist-preflight does not apply and no rebuild was needed or claimed.", "open_questions": [ { "question": "SOURCES_COVERED governs source files in SUBDIRECTORIES (depth > 0), so the 54 package-root tool configs (vitest.config.ts x32, tsup.config.ts x16, objectstack.config.ts x5, vitest.integration.config.ts x1) across 42 packages are unchecked TypeScript that no invariant names. Should they be governed too? The depth line can also be walked around by moving a file up into the package root. Filed as #10870 rather than decided in this PR; the gate's header argues it both ways.", "options": [ "A — leave as-is: they are one repo-wide convention, not 42 decisions; each is loaded by a tool that transpiles, and ledgering them seeds 42 identical entries, which is the 'ledger nobody trusts' failure #4311's own header warns about", "B — bring them into a program via a shared tsconfig.tools.json named in each typecheck script: closes the class and the evasion, costs 42 manifests plus an unmeasured number of surfaced errors", "C — a separate, deliberately coarse invariant over root tool configs with its own ledger for stragglers (B with a seed)" ], "recommendation": "A for now, which is what shipped — but as a recorded decision (#10870) rather than an oversight. Real business need is the weak axis: nothing is pulling on type-checked vitest configs, and no defect has been traced to one. Startup scope discipline cuts the same way — B expands gate surface across 42 packages with no measured pull. The AI-correctness axis is the one that argues for B eventually (agents add config files constantly), so this is worth revisiting the first time a broken tool config actually costs something, not before." } ], "out_of_scope_findings": [ "filed as #10868: 8 packages' scripts/i18n-extract.config.ts fail to type-check, 1-3 x TS2883 each — the inferred type of `default` names hash-suffixed internal chunks of @objectstack/spec's dist, so it is non-portable by construction; two candidate repairs, one of which lives in spec rather than in the 8 consumers", "filed as #10869: packages/plugins/plugin-auth/examples/basic-usage.ts imports @objectstack/plugin-hono-server, which the package declares in NO dependency block (1 x TS2307) — the census's only file in no tsc program at all, and a genuinely broken example the blind spot hid", "filed as #10870: [finding] the 54 package-root tool configs and the depth > 0 scoping line, for a maintainer to rule on (see open_questions)" ] }Generated by Claude Code
fix(devx): count a package's unread source directories, and stop bill…
docs(plugin-hono-server): re-anchor the dead tracker citations in pac…
docs(plugin-webhooks): re-anchor the dead tracker citation to the com…
docs(service-realtime): re-anchor the dead tracker citation to the co…
chore(objectql): retire the PR-10d.1 dry-run-hash-compat probe and it…