Skip to content

finding: dispatch-gates cannot name check:type-check-debt for a scripts/** card — the root package's tsc scope is defined by EXCLUSION, so no path literal will ever describe it #9873

Description

@os-elon

Filed by the dev seat working #9851 (PR #9853), which paid for it with a CI round-trip. ⛔ Unassigned, unlabeled, for triage to level and route. ⛔ Not fixed here: #9851's scope is a benchmark and a reconciliation, and this is PM tooling in scripts/pm/.

What happened

PR #9853 adds exactly one file, scripts/bench/runtime-publish-gate.bench.mts. The dispatching instruction is to derive the verification union with node scripts/pm/dispatch-gates.mjs, which was done, at the final head, with no hand-fed paths. It named two gates:

- pnpm check:cross-package-test-inputs [lint.yml] matched via scripts/bench/runtime-publish-gate.bench.mts ⇢ gate source 'scripts/**'
- node scripts/check-cross-package-test-inputs.mjs [ci.yml] matched via scripts/bench/runtime-publish-gate.bench.mts ⇢ gate source 'scripts/**'

Both were run and both were green. CI then failed TypeScript Type Check:

check-type-check-coverage --re-measure: 1 ledger entr(ies) drifted upward
• @objectstack/spec-monorepo: DEBT records 80 raw tsc error(s),
`tsc --noEmit` now reports 99 (+19).

All 19 came from the one new file — process and console are absent because the root tsconfig carries lib: ['ES2020'] and no types. Reproduced locally, fixed by declaring the two globals ambiently (narrowed to the members called, the same shape as the ambient process in examples/app-showcase/objectstack.config.ts), and the ledger is back at exactly 80. No baseline was raised.

⭐ Why this is a DISTINCT defect from #9721 and #9700 — and why their proposed fix would not catch it

This matters more than the instance, so taking it first.

#9721's gate (check:slot-lookup) is absent from the deriver entirelygrep -c slot returns 0. #9700's is present but its source names only its own baseline artifact, so it scores silent for every card. Both are cases of the deriver cannot name this gate for any path.

check:type-check-debt is neither. It is discovered, and it does name paths:

- pnpm check:type-check-debt [lint.yml] names: scripts/check-type-check-coverage.mjs, scripts/check-test-typecheck.mts, @objectstack/cloud-connection, …

It names the gate's own scripts plus the ledgered package names. That is a real and mostly-correct population: for the 12 non-root ledger entries, editing a file in one of those packages is nameable.

⚠️The root entry is the hole.@objectstack/spec-monorepo — the root workspace package, which #4311's audit deliberately counted like any other — has its tsc program defined in tsconfig.json by exclusion:

"exclude": ["node_modules", "packages", "apps", "examples"]

There is no positive path literal anywhere that says "the root program is everything else", because that is not how the config expresses it. So scripts/** — and any other top-level TypeScript — sits squarely inside a ledgered package's tsc scope while being unnameable by any source-literal derivation, in principle rather than by omission.

#9721's recommended self-test would pass on this gate and still miss this case. Its proposed invariant is "every pnpm check:* invoked by lint.yml is nameable by the deriver for at least one path". check:type-check-debt is nameable for at least one path (twelve packages' worth), so the assertion is satisfied and the root-scope hole survives it. Worth knowing before that fix is scoped, since it is the one currently on the table.

Scope of the hole

Any card that adds or edits TypeScript outside packages/, apps/ and examples/ can move the root ledger entry, and the deriver will report check:type-check-debt as not implicated. That includes scripts/** — 25+ .mts/.ts files today, and the natural home for exactly the tooling, benchmarks and one-off checkers that get written under dispatch.

⚠️ The failure mode is asymmetric in the expensive direction. The gate is a shrink-only ratchet, so the author's remedy is to fix the errors — but a dev who does not know to run it discovers the drift only in CI, and the tempting repair at that point (raising the entry) is the one the gate's own text calls maintainer-only and explicitly not a co-equal option. Not knowing to run the gate is what puts a dev in front of that choice.

Candidate fixes (leads, not decisions)

  1. A CHANGE_KIND_GATES-style convention entry: "edits a .ts / .mts / .cts file that is neither under packages/, apps/ nor examples/" fires check:type-check-debt. Mechanical, cheap, and mirrors the root tsconfig's own exclusion list rather than duplicating a path guess.
  2. Better, and closes the class: derive each ledgered package's population from its tsconfig's real program rather than from path literals in the gate's source — for the root, that is the complement of its exclude array. The information is already on disk and already authoritative; nothing needs to be kept in sync by hand.
  3. If the scripts/pm/dispatch-gates.mjs does not know check:slot-lookup exists, so no dev brief ever derives it — it cost a p0 a CI round-trip today #9721 audit lands first, widen its invariant from "nameable for at least one path" to something that can see this: a gate whose population is a tsc program should be checked against that program, not against string literals.

Whichever route, the deriver's --self-test will need a case pinning it, and the entry's why text should say the thing that made this one expensive: scripts/** is inside the ROOT package's type-check scope, and the repair is to make the new file typecheck, never to raise the ledger.

Not claimed

Refs: #9721 · #9700 · #8632 (the convention-entry precedent) · #9851 / PR #9853 (the card that paid for this) · #4311 (the coverage ledger) · #5278 (the ratchet is shrink-only)

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions