Skip to content

fix(scripts): ask check:type-check-coverage's four per-package clauses of the ROOT workspace package - #15516

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-15483-type-check-coverage-root-package
Sep 4, 2026
Merged

fix(scripts): ask check:type-check-coverage's four per-package clauses of the ROOT workspace package#15516
baozhoutao merged 2 commits into
mainfrom
claude/issue-15483-type-check-coverage-root-package

Conversation

@claude

@claude claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #15483

check:type-check-coverage built its subject list two ways and only one of them was walked:

  • workspacePackages() walks the 79 dirs workspacePackageDirs(ROOT) returns and calls testCoverage(dir, scripts) on each — the walk that produces hiddenTests, pinFiles, uncheckedSources and generatedIncludes.
  • observed() built the workspace root as { name: rootManifest.name, scripts: rootManifest.scripts ?? {} } — a manifest read, with none of those four fields.

evaluate() asks the per-package invariants inside for (const pkg of packages), and the root is not in that array. So TESTS_COVERED, SOURCES_COVERED, PINS_CHECKED and GENERATED_COVERED were asked of 79 packages and never of the root, while the file's own header said the root is counted "like any other package's". Fourth axis of the same gate after #14630, #14386 and #14918 — each of the four was a clause skipped for a population with the run saying so nowhere.

Route (a): the root joins the population — measured first

The card is explicit that this is LATENT, so the walk was measured on origin/main before choosing a route. testCoverage('.') over the real tree:

observation measured
directories walked / time 106 in 11ms (the walk already skips any subdirectory with a package.json at depth > 0, so the 79 are not re-walked)
test files reached 0
@ts-expect-error pin files 0
non-test source files reached 14
of those, unread by the root program 0

Root tsconfig.json declares no include, so readTsconfig gives it roots: [''] — every file it does not exclude (node_modules, packages, apps, examples) is in the program. The root can therefore be walked like any other package with zero new findings, which is route (a): no ledger row added, no exclusion widened, UNCHECKED_SOURCE_DEBT still empty and still closed. What was missing is the guard — for the day that config grows an include, or a root tsconfig.build.json excludes a test glob, or the repo gains its first root-level test file carrying a pin.

The repair

  • perPackageClauses(pkg, state) lifts the four clauses out of evaluate's loop, so the loop is no longer what defines their population. evaluate asks it of every enumerated package and once of the root.
  • observed() walks the root: ...testCoverage('.', rootCoverageScripts), plus dir: '.'.
  • The clauses are decided against the root's COVERAGE script, not its aggregator. This is the card's own objection — PINS_CHECKED anchors on configsNamedByTypecheck, and the root's coverage runs through typecheck:root while its typecheck slot is turbo run typecheck. That script is handed to testCoverage under the typecheck key and carried back as coverageScript; coverageScriptName (typecheck:root) is interpolated into the findings, so a remedy quotes the slot its reader has to go and edit instead of sending them to the aggregator. For the 79 both are typecheck and every message is byte-identical — the 50 pre-existing semantic cases pass unchanged.
  • The RECONCILED sweeps and the three layer figures read the same subject list (clauseSubjects = [...packages, root]). A sweep still running over the enumerated packages alone would report a root PHANTOM_PIN_DEBT row as stale the moment it was written.
  • SCOPED_CLAUSES.asked reads coverageScript, not pkg.scripts?.typecheck: the root's manifest typecheck is always defined, so the old predicate would report the root as ASKED on a run where the clause was not asked of it — a scope line lying about scope.
  • SCOPE_LINE_BOUNDARY is gone, as the card requires: it named [finding] check:type-check-coverage never asks TESTS_COVERED / SOURCES_COVERED / PINS_CHECKED / GENERATED_COVERED of the ROOT workspace package — its header says the root is included "like any other package's" #15483 and stops being true. It is replaced by a population sentence, still printed, because the headline counts the enumerated packages (79) and this line now counts the clause population (80) — a reader comparing the two numbers needs the sentence that says where the extra subject comes from.
  • The header sentence this card was filed against now says how "included" is made mechanical, and by what.

The line, before and after

before:  Every other per-package invariant was asked of all 79. ⭐ ... ⚠️ Counted over the
         enumerated workspace packages: the ROOT package is not one of them, and four
         per-package clauses are never asked of it (#15483).

after:   GENERATED_COVERED was NOT asked of 5 of them (@objectstack/cloud-connection,
         @objectstack/console, @objectstack/hono, @objectstack/observability,
         @objectstack/spec-monorepo) -- they declare no `typecheck` script (`typecheck:root`
         for the workspace root) ... Every other per-package invariant was asked of all 80.
         ⭐ ... Counted over the enumerated workspace packages PLUS the ROOT package, which
         is walked and asked the same four per-package clauses as the rest since #15483 --
         against its `typecheck:root` script, the one that type-checks the root's own
         TypeScript.

The root appears in the GENERATED_COVERED skipped list because it declares no typecheck:root today — the same empty, not hidden population the other four sit in, now said out loud instead of being unsayable.

Self-test

Five new semantic cases and one new observation case, each pinned by an ablation on this branch's committed tree (mutate, confirm the mutation on disk by counting the removed and injected text, run, restore, verify the file's blob hash equals its HEAD blob):

ablation reds
delete problems.push(...perPackageClauses(root, state)) the four planted root cases — TESTS_COVERED, SOURCES_COVERED, PINS_CHECKED, GENERATED_COVERED — each got []
phantomSeen back over packages alone a PHANTOM_PIN_DEBT row for a ROOT pin is covered, not reported stale — the row reads as stale
SCOPED_CLAUSES.asked back on pkg.scripts?.typecheck the ROOT is in this population, and one with no typecheck:root is named like anyone elseexpected [{skipped:["root"]}], got []

Each of the four clause cases plants its finding on the root and nothing else (packages: []), because the defect was not a wrong answer — it was four questions never asked, which no amount of green over the 79 could distinguish from four answers of "nothing here". Each also pins the script name in the remedy, so a root reader is not sent to edit the workspace aggregator.

SELF_TEST_BATTERIES gained the six rows at floor 1 each (registerCase(c.label) stays the first statement of the driving loop; the table row is the battery), observation cases was re-floored 63 → 64, SELF_TEST_BATTERY_FLOOR 56 → 61, and the two prose figures quoting them moved with them (63 → 64, 74 → 75).

Verification

Every command below ran on the final commit 6be83bdc5 (git rev-parse --short HEAD, tree clean), with each exit code captured before any pipe.

node scripts/check-type-check-coverage.mjs                      EXIT=0
  check-type-check-coverage: OK — 75/79 workspace packages type-checked (plus the root),
  4 in the DEBT ledger (53 frozen raw errors, .../4311), 1 exempt.
  clause scope: GENERATED_COVERED was NOT asked of 5 of them (... @objectstack/spec-monorepo)
  ... Every other per-package invariant was asked of all 80. ... Counted over the enumerated
  workspace packages PLUS the ROOT package ... since #15483

node scripts/check-type-check-coverage.mjs --self-test          EXIT=0
  ✓ check:type-check-coverage --self-test — 55 semantic case(s) + 75 observation case(s)
  + 45 re-measure case(s) + 28 built-closure case(s) + 19 auto-lowering case(s)
  + 18 exit-code case(s) hold.

pnpm -s check:pm-dispatch-gates                                 EXIT=0
  ✓ dispatch-gates self-test: 1402 cases pass.

pnpm check:nul-bytes                                            EXIT=0
  check-nul-bytes: OK (scanned 7515 text file(s) -- 7515 tracked, 0 untracked-not-ignored;
  skipped 7 binary; no raw ASCII control bytes).

pnpm lint            (repo-wide `eslint . --no-inline-config`)  EXIT=0
  os-verify-lock: VERDICT command-exit 0 · held the lock 95s · waited 0s

On origin/main @ 615fac3a0, for comparison (a detached worktree at that commit): the same two commands print 50 semantic case(s) + 74 observation case(s) and the pre-repair scope line — asked of all 79 with the ⚠️ ... the ROOT package is not one of them ... (#15483) boundary. That is the premise, still true on current main at dispatch time.

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 26 commands from the real change set (one path: scripts/check-type-check-coverage.mjs, three-dot against merge base 615fac3a0). All 26 were run; 25 EXIT=0.

⚠️ The 26th, pnpm check:type-check-debt, is NOT MEASURED — neither a pass nor a red: EXIT=3, check-type-check-coverage: PREREQUISITE NOT MET — --re-measure cannot run: 33 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk. A property of a fresh worktree, not of this diff: origin/main's own copy refuses identically, EXIT=3, same message and same count of 33. CI builds the closure before that step.

Repo-wide pnpm lint was run in full rather than narrowed, so no scoping argument is owed. All figures were measured on a shared box; the lock excludes other locked runs and nothing else.

skip-changeset: the diff is one file under scripts/, publishes nothing from any package.


🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

…package

`check-type-check-coverage.mjs` built its subject list two ways and only one
of them was walked: `workspacePackages()` ran `testCoverage(dir, scripts)` over
the 79 enumerated dirs, while `observed()` built the root from its manifest
alone -- `{ name, scripts }`, with none of the four observation fields. The
per-package invariants are asked inside `for (const pkg of packages)`, so
TESTS_COVERED, SOURCES_COVERED, PINS_CHECKED and GENERATED_COVERED were asked
of 79 packages and never of the root, while the file's own header said the root
is counted "like any other package's".

Measured on main @ 5023630 before deciding: the root walk reaches 106
directories in 11ms, 0 test files, 0 `@ts-expect-error` pins and 14 non-test
source files, and 0 of them sit outside the root program -- so the root can
join the population with zero new findings, and the repair is a guard rather
than a burn-down.

- `perPackageClauses(pkg, state)` extracts the four clauses out of the loop, so
  the loop is no longer what defines their population; `evaluate` asks it of
  every package and once of the root.
- The root is walked by `testCoverage('.')`, whose `walk` already skips any
  subdirectory carrying a `package.json` at depth > 0, so the 79 are not
  re-walked and nothing is billed twice.
- The clauses are decided against the root's COVERAGE script (`typecheck:root`)
  rather than its `typecheck` aggregator, carried as `coverageScript` /
  `coverageScriptName`. Anchoring PINS_CHECKED and GENERATED_COVERED on
  `turbo run typecheck` would manufacture findings instead of finding them, and
  a finding now quotes the slot its reader has to edit.
- The RECONCILED sweeps and the three layer figures read the same subject list,
  so a root ledger row cannot read as stale the moment it is written.
- `SCOPED_CLAUSES.asked` reads `coverageScript`: the root's manifest `typecheck`
  is always defined, so the old predicate would have reported the root as ASKED
  on a run where the clause was not asked of it.
- The `SCOPE_LINE_BOUNDARY` sentence naming this card is replaced by the
  population sentence -- it stops being true -- and the line's count is now 80,
  which is why the sentence stays printed: the headline counts 79.

Self-test: five new cases (one red per clause planted on the root, plus the
RECONCILED half) and one scope case pinning that a root with no `typecheck:root`
is named in the skipped list. 55 semantic + 75 observation cases; battery roster
and floor moved with them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@github-actions github-actions Bot added the size/l label Sep 4, 2026
@baozhoutao baozhoutao added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 4, 2026 — with Claude
@baozhoutao
baozhoutao marked this pull request as ready for review September 4, 2026 19:30
@baozhoutao
baozhoutao enabled auto-merge September 4, 2026 19:30
@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

Reviewed against the three-dot diff origin/main...claude/issue-15483-type-check-coverage-root-package (merge base 615fac3a0, head 6be83bdc5): one file, scripts/check-type-check-coverage.mjs, +399/−184, matching the PR file list; route (a) of the claim's ruling, measured first (root walk: 106 dirs, 0 test files, 0 pins, 14 non-test sources all inside the root program → zero new findings, no ledger row). The removed block is evaluate's per-package loop body lifted into perPackageClauses(pkg, state) so the loop no longer defines the population; observed() walks the root via testCoverage('.'); the clauses judge the root against its COVERAGE script (typecheck:root, carried as coverageScript / coverageScriptName), never the turbo run typecheck aggregator — the card's own objection; SCOPED_CLAUSES.asked reads coverageScript; the boundary sentence naming this card is removed and replaced by a population sentence (79 enumerated / 80 asked). Self-test 50 → 55 semantic (four planted root cases + the root PHANTOM_PIN_DEBT case), observation 63 → 64, battery floor 56 → 61; three trap-guarded ablations red exactly the planted cases. Derived family 25/26 green, check:type-check-debt NOT MEASURED identically on main's copy. Whole-repo lint green. Hot-file scan: #15516 is the only open PR on the gate (#15482 landed 18:40Z). skip-changeset is right. Fixes #15483 closes the fourth axis on merge. Flipping ready and arming auto-merge now.


Generated by Claude Code

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

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants