fix(devx): the type-check coverage and debt gates skip paths git ignores - #15841
Merged
baozhoutao merged 3 commits intoSep 5, 2026
Merged
Conversation
`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
…pe-check-gates-skip-gitignored
…pe-check-gates-skip-gitignored
baozhoutao
marked this pull request as ready for review
September 5, 2026 10:47
baozhoutao
enabled auto-merge
September 5, 2026 10:47
Contributor
Author
|
Flipped ready + auto-merge enabled (10:47Z, Generated by Claude Code |
baozhoutao
deleted the
claude/issue-15731-type-check-gates-skip-gitignored
branch
September 5, 2026 11:10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #15731
What changed
check:type-check-coverageandcheck:type-check-debtare 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/clisuite was in flight, that walk found the scratch file a test writes into the gitignoredpackages/cli/tmpand both gates went red with the text a real ratchet break produces, naming a repair (add atsconfig.scripts.json, or wideninclude) that would have widened a config to cover a scratch directory, printed besideUNCHECKED_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
.gitignoreexcludes 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.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.packages/clitest 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 -zper 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'score.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.exposedScratchDirsinscripts/pm/dispatch-gates.mjsdraws this same line for the same reason.--othersis the point), so a force-added file under an ignored directory keeps being judged — it is present in a clean checkout.No ledger row moves.
UNCHECKED_SOURCE_DEBT, every other ledger and everytsconfigare untouched.Measurements
Final head
3d45b4290. Exit codes captured before any pipe.The card's own reproduction, on the fixed tree
A scratch
probe.tsplusprobe.test.tswritten into the gitignoredpackages/cli/tmp, exactly the shape a CLI test leaves behind:And the strongest form of it — the gate's whole output is byte-identical whether those files are on disk or not:
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
✓ 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=3is NOT MEASURED, not a red — it is this gate's own dep-less refusal, in its own words:Its structural half — the half this PR changes — ran and printed
OKimmediately above that, and the--self-testin the same row is green. The numeric half needsturbo run buildover the whole workspace, which is CI's run (lint.ymlbuilds 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 toreturn false:Both legs go red. The production leg reproduces the card's exact finding:
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:
Restore proved byte-identical to
HEADby blob hash, by an emptygit diff HEAD, and by the injected marker being gone. There is no dist leg to preflight: this gate is executed directly from source bynode scripts/..., with no package build and noexportsresolution 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 casesmoves 64 to 86 in the floor. The fourth battery builds a throwaway git repo so the tracked-.gitignore-versus-.git/info/excludedistinction, 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,tmpdoes not swallow a siblingtmpl, 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:31 of the 32 are
EXIT=0; the one non-zero ispnpm check:type-check-debt's NOT MEASURED above. Six of them first refused for want ofnode_modulesin this fresh worktree and were re-run green after an offline install, rather than being left declared.Landing checks
No open PR touches
scripts/check-type-check-coverage.mjs(all 30 open PRs read viapulls/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