Skip to content

fix(devx): the type-check coverage and debt gates skip paths git ignores - #15841

Merged
baozhoutao merged 3 commits into
mainfrom
claude/issue-15731-type-check-gates-skip-gitignored
Sep 5, 2026
Merged

fix(devx): the type-check coverage and debt gates skip paths git ignores#15841
baozhoutao merged 3 commits into
mainfrom
claude/issue-15731-type-check-gates-skip-gitignored

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15731

What changed

check:type-check-coverage and check:type-check-debt are the same script — scripts/check-type-check-coverage.mjs, run without and with --re-measure — and both walked every package looking for non-test source and test files without ever asking whether the repo checks any of them in.

Run while a packages/cli suite was in flight, that walk found the scratch file a test writes into the gitignored packages/cli/tmp and both gates went red with the text a real ratchet break produces, naming a repair (add a tsconfig.scripts.json, or widen include) that would have widened a config to cover a scratch directory, printed beside UNCHECKED_SOURCE_DEBT's "closed to new entries".

As the card put it, and it is worth keeping: the gate is not wrong about its own rule; it is reading a transient file it has no reason to judge.

The rule is unchanged. Its population is not.

Which direction, and why the other two were not taken

Per the dispatch ruling on the card, direction 1 — the gates skip paths git ignores.

These gates judge whether every non-test source sits inside some tsc program. A path .gitignore excludes is, by the repo's own declaration, not a source of the package: never committed, never published, and absent from the clean checkout CI type-checks. So the gate has no claim on it. Nothing about the invariant moves — only the set of files it is asked about stops including files the repo says are not there.

  • Direction 2 (skip a tmp/ directory by convention) is a narrower special case of 1, and it hard-codes one name; a scratch directory called anything else stays exposed. Not taken.
  • Direction 3 (move the scratch outside the package tree) changes one packages/cli test rather than the gate, and leaves every other transient writer — present and future — able to manufacture the same phantom finding. Not taken.

How the verdict is derived

From git, never a hand list: one git ls-files --others --ignored --directory -z per run, memoised across both walk sites (79 packages plus the root). Measured at 41ms for the whole repo, against a structural pass that is the sub-second half of this gate — a spawn per package, let alone per file, would have ended that.

Three deliberate properties:

  • --exclude-per-directory=.gitignore, not --exclude-standard. A rule living only in .git/info/exclude (or a developer's core.excludesFile) covers the clone it lives in and nothing else. What licenses dropping a path from this gate's subject is the repo's declaration that the path is not part of the tree; honouring a per-clone rule would let a developer quietly shrink the gate's population locally while CI, whose checkout has no such file, still judges the directory. A gate must conclude the same thing in both places. exposedScratchDirs in scripts/pm/dispatch-gates.mjs draws this same line for the same reason.
  • Tracked paths are never listed (--others is the point), so a force-added file under an ignored directory keeps being judged — it is present in a clean checkout.
  • A git that cannot answer is a throw, never an empty set. Reading a failed spawn as "this tree ignores nothing" would silently restore the very phantom findings this filter exists to remove.

No ledger row moves. UNCHECKED_SOURCE_DEBT, every other ledger and every tsconfig are untouched.

Measurements

Final head 3d45b4290. Exit codes captured before any pipe.

The card's own reproduction, on the fixed tree

A scratch probe.ts plus probe.test.ts written into the gitignored packages/cli/tmp, exactly the shape a CLI test leaves behind:

git check-ignore verdict: .gitignore:53:tmp/	packages/cli/tmp/probe.ts
tracked files under packages/cli/tmp: 0
EXIT=0 :: node scripts/check-type-check-coverage.mjs   [WITH the scratch files present, FIXED tree]
EXIT=0 :: --self-test  [WITH the scratch files present]

And the strongest form of it — the gate's whole output is byte-identical whether those files are on disk or not:

clean-tree vs scratch-present output diff EXIT=0 (0 = byte-identical)
  source layer: 0 directory(ies) of non-test source in 0 ledgered entr(y/ies) sit outside
  every tsc program that accounts for their package (0 files as counted by this run).

The clean-tree output is also byte-identical to the same gate's output on the base commit, which is the ledger-untouched claim as a measurement rather than an assertion.

The two gate rows

EXIT=0 :: node scripts/check-type-check-coverage.mjs --self-test
EXIT=0 :: pnpm check:type-check-coverage
EXIT=3 :: pnpm check:type-check-debt
EXIT=0 :: pnpm check:nul-bytes

✓ check:type-check-coverage --self-test — 55 semantic case(s) + 97 observation case(s) + 45 re-measure case(s) + 28 built-closure case(s) + 19 auto-lowering case(s) + 18 exit-code case(s) hold.

The EXIT=3 is NOT MEASURED, not a red — it is this gate's own dep-less refusal, in its own words:

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 -- @objectstack/core, @objectstack/driver-memory, ...

  ⛔ This is NOT a pass and NOT a finding: nothing was measured, so this run says
  NOTHING about whether any DEBT or TEST_DEBT number is still correct.
  (Exit code 3, distinct from a finding's 1 ...)

Its structural half — the half this PR changes — ran and printed OK immediately above that, and the --self-test in the same row is green. The numeric half needs turbo run build over the whole workspace, which is CI's run (lint.yml builds the closure before this step), not a targeted local one. This PR cannot move those numbers in any case: the structural output is byte-identical on a clean tree.

Ablation of the ignore filter

Trap-guarded, at the final head. The predicate the walk applies (isIgnoredPath) reverted to return false:

HEAD_BLOB=4666caf866a81586c1c2d464bff5c16df3123e96
PRE_BLOB =4666caf866a81586c1c2d464bff5c16df3123e96
ON-DISK PROOF: deleted-anchor before=1 after=0 ; injected-marker=1
MUT_BLOB =d30cbfa335664a1144cc0d3576211ed3fda368a6
EXIT=1 :: [ABLATED] node scripts/check-type-check-coverage.mjs --self-test
EXIT=1 :: [ABLATED] node scripts/check-type-check-coverage.mjs   (scratch file present)
POST_BLOB=4666caf866a81586c1c2d464bff5c16df3123e96
git diff HEAD EXIT=0 (0 = restored, nothing left over)
residual marker count: 0  (0 = gone)

Both legs go red. The production leg reproduces the card's exact finding:

check-type-check-coverage: 1 problem(s)

  • packages/cli/tmp: 1 non-test source file(s) here sit outside every tsc program that
    `@objectstack/cli`'s `typecheck` script runs ... Add a sibling `tsconfig.scripts.json`
    and NAME it in the `typecheck` script ... UNCHECKED_SOURCE_DEBT ... is closed to new entries.

and the self-test leg names 7 failing cases, including the two that are the red and the green of this card as one assertion:

• walkPackageFiles — with the scratch directory ignored, only the tracked source survives:
    expected ["src/engine.ts"], got ["src/engine.ts","tmp/probe.ts"]
• readIgnoredPaths — a directory a TRACKED .gitignore covers is out of the gate's subject
    -- the scratch tree of the card: expected true, got false

Restore proved byte-identical to HEAD by blob hash, by an empty git diff HEAD, and by the injected marker being gone. There is no dist leg to preflight: this gate is executed directly from source by node scripts/..., with no package build and no exports resolution between the edit and the run.

New self-test coverage

22 new cases in four batteries, one per joint — how git's listing is read, the predicate the walk applies, the walk's own output, and what git is actually asked of a real fixture repository. observation cases moves 64 to 86 in the floor. The fourth battery builds a throwaway git repo so the tracked-.gitignore-versus-.git/info/exclude distinction, and a force-add that re-opens an ignored directory, are pinned by assertions rather than by a paragraph. Each ignore case carries its positive control: the tracked directory beside the ignored one is still judged, tmp does not swallow a sibling tmpl, and the same basename under another package is a different path.

Derived gate families

node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack — re-derived at the final head and identical to the set that was run:

✓ dispatch-gates --ran: 32 derived famil(ies) accounted for — 32 run, 0 NOT-MEASURED.
Run reconciliation — 32 derived, 32 run, 0 NOT-MEASURED, 0 UNRUN.

31 of the 32 are EXIT=0; the one non-zero is pnpm check:type-check-debt's NOT MEASURED above. Six of them first refused for want of node_modules in this fresh worktree and were re-run green after an offline install, rather than being left declared.

Landing checks

EXIT=0 :: node scripts/pm/check-governed-merges.mjs --test scripts/check-type-check-coverage.mjs
  ✅  NOT governed — ordinary queue landing applies to a PR with exactly this file list.
EXIT=0 :: git merge-tree --write-tree origin/main HEAD

No open PR touches scripts/check-type-check-coverage.mjs (all 30 open PRs read via pulls/N/files, HTTP 200 each).

skip-changeset: this PR changes one root tooling gate script and publishes nothing from any package.

Out of scope

A separate observation from the same investigation — newestSourceMtime / STALE_BUILD — is already filed as #15822 and is deliberately not addressed here. It remains open.


🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

`check:type-check-coverage` and `check:type-check-debt` (the same script, with
and without `--re-measure`) walked every package for non-test source and test
files without asking whether the repo checks any of them in. Run while a
`packages/cli` suite was in flight, that walk found the scratch file a test
writes into the gitignored `packages/cli/tmp`, and both gates went red with the
text a real ratchet break produces -- naming a repair (add a
`tsconfig.scripts.json`, or widen `include`) that would have widened a config to
cover a scratch directory, beside UNCHECKED_SOURCE_DEBT's "closed to new
entries".

The rule is unchanged; its population is not. A path `.gitignore` excludes is by
the repo's own declaration not a source of the package -- never committed, never
published, and absent from the clean checkout CI type-checks -- so the gate has
no claim on it. The verdict comes from git, in ONE `ls-files --others --ignored
--directory -z` per run memoised across both walk sites, never a hand list and
never a spawn per package. `--exclude-per-directory=.gitignore` rather than
`--exclude-standard` is deliberate: a rule living only in `.git/info/exclude`
covers one clone, and letting it shrink this gate's subject would make a
developer's box and CI conclude different things -- the same line
`exposedScratchDirs` draws in scripts/pm/dispatch-gates.mjs.

No ledger row moves: on a clean tree the gate's output is byte-identical, and
`UNCHECKED_SOURCE_DEBT`, every other ledger and every `tsconfig` are untouched.

Pinned by 22 new `--self-test` cases in four batteries: how git's listing is
read, the predicate the walk applies, the walk's own output (the same fixture
walked with an empty index yields the scratch file -- the card's finding -- and
with the real index does not), and what git is asked of a real fixture
repository, including the tracked-`.gitignore`-versus-`.git/info/exclude`
distinction and a force-added file that re-opens an ignored directory.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Flipped ready + auto-merge enabled (10:47Z, method: MERGE). All seven required contexts on 3d45b4290 read non-failing: Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Governed Surface Queue Guard success; Build Core, Temporal Conformance (live PG + MySQL) skipped; no other red. git merge-tree --write-tree origin/main <branch> → EXIT=0 against 450e03003. Watched; on landing #15731 closes.


Generated by Claude Code

@baozhoutao
baozhoutao added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit 384a573 Sep 5, 2026
36 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-15731-type-check-gates-skip-gitignored branch September 5, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check:type-check-coverage and check:type-check-debt go red on the gitignored packages/cli/tmp while a CLI test run is in flight

2 participants