Uh oh!
There was an error while loading. Please reload this page.
feat(gate): GENERATED_COVERED — a tsconfig include rooted in a generated path must be produced by the typecheck script, or declared as deliberately not produced - #10896
Merged
Conversation
…ted path must be produced by the typecheck script, or declared as deliberately not produced Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
This was referenced Aug 21, 2026
os-zhuang
marked this pull request as ready for review
August 21, 2026 16:30
Uh oh!
There was an error while loading. Please reload this page.
Contributor
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32503331743 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
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 freeto 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#10880
What
check:type-check-coveragegains a sixth invariant, GENERATED_COVERED: a tsconfigincludeentry rooted in a path the repo does not check in is either produced by the package's owntypecheckscript before tsc runs, or carries a row saying it is deliberately not produced, and why.REALalready asks whether atypecheckscript invokes tsc. This asks the next question — whether the program tsc gets is the one the config advertises.apps/docs/tsconfig.jsonincludes.next/types/**/*.ts, written bynext typegen, and the wired script was a baretsc --noEmit: with.nextdeleted it exited 0 having compiled none of the generated route types, while COVERED, REAL, TESTS_COVERED, SOURCES_COVERED and RUNNABLE all passed. Every one of those is a question about files that exist.The failure direction is why it is an invariant and not a lint: a missing generated directory makes tsc read green over files that were never in the program. Every other way of getting the same config wrong (stale directory, failing generator) ends in a red somebody reads.
The design, and the two decisions worth reviewing
1. What counts as "generated" is
git check-ignore, not a hard-coded list. One batched call for the whole workspace. The ignore rules are checked in, so the verdict is a property of the tree rather than of the machine —core.excludesFileis emptied on the command line so a developer's global ignore list cannot change what CI concludes, the index is consulted (a force-added directory is present in a clean checkout, so it reads as not-generated), and existence is never consulted, so the answer does not depend on whether anyone has run a build. A git that cannot answer throws; reading a failed spawn as "nothing here is generated" would retire the invariant silently.Measured on the branch point: 101
includeroots across the workspace, of which 3 are generated — all inapps/docs. The detector's live surface is fully enumerable.2. "Declared, and deliberately not generated" is a passing state. The card is explicit that the remedy is not deleting the offending glob: Next owns that array and
writeConfigurationDefaultsre-adds.next/dev/types/**/*.tson the nextnext dev/next build. SoGENERATED_INCLUDE_ROOTSrows come in two kinds —generator: 'THE-COMMAND'(enforced: the script must run it, ahead of tsc) andgenerator: null(nothing a typecheck runs produces this, on purpose). For anullrow thewhycarries the argument that its absence cannot make the check read green — the one claim here nothing mechanical can verify, which is exactly why it is written where a reviewer sees it. A row that simply omits the key is refused rather than defaulting tonull, so the strict half cannot be switched off by a typo.The table is keyed package-relative (
.next/types, notapps/docs/.next/types), so one row holds for every Next app the workspace ever gains. It holds three rows, and the generator claims are read out of next@16.3.1 — the versionapps/docsresolves — rather than inferred from the command name:dist/cli/next-typegen.jsreacheswriteAppTypeDeclarations(which writesnext-env.d.ts) throughverifyAndRunTypeScript, then writesroutes.d.ts,validator.ts,cache-life.d.tsandroot-params.d.tsinto the disttypesdirectory. Nothing there writes.next/dev/types.Departures from the card, and one addition
check:type-check-coveragewith a whole source directory outside tsc —packages/objectql/scripts/**is invisible to its owntypecheck#10756: the file now carries a fifth invariant,SOURCES_COVERED. The new one is written as the sixth and reuses that work rather than the three-invariant shape the card quotes.apps/docs's generated include entries —next-env.d.tsis gitignored and named byincludetoo, so it needs an honest row rather than a silent exemption on a file-vs-directory technicality.typecheck: "tsc --noEmit && next typegen"satisfies "the script contains the command" and still checks the pre-generator program. This is decided only inside a single script body, where text order is shell order; a generator reached throughpnpm SCRIPTNAMEindirection has no knowable position relative to a tsc in a different body, so it abstains there rather than guessing. That abstention is whytypecheckScriptChainreturns a list of script bodies instead of the joined blobconfigsNamedByTypecheckused to build — a blob would red the correct configtypecheck: 'pnpm gen && tsc'+gen: 'next typegen'.Machinery reused, not re-implemented
readTsconfig,configCovers,configsNamedByTypecheckand the invoked-config scoping are all existing. The only refactor istypecheckScriptChain(extracted fromconfigsNamedByTypecheck, which now calls it) andincludeRoot(extracted so the two invariants that read include prefixes cannot answer the same question differently). Scoped to packages that declare atypecheckscript and to the configs that script invokes, exactly asSOURCES_COVEREDis.The workspace root package is not asked: giving it this observation would mean walking the whole repo through
testCoverage. No root-levelincludenames a generated path today (same measurement). Stated in the code as a limit, not left as an oversight.Verification — self-test, and three ablations with predictions stated first
--self-testgrows 9 semantic cases and 21 observation cases:47 semantic case(s) + 59 observation case(s) + 29 re-measure case(s) + 28 built-closure case(s) + 19 auto-lowering case(s) hold.The positive control cannot be "the real workspace reds" — #10879 already repaired the measured instance, so the fixtures are literal. Each ablation states its prediction, confirms the mutation landed on disk (grep counts on the text actually changed, never an editor's exit code), and has a restore leg proved with
git diff --quiet.apps/docstypecheckback to baretsc --noEmitnever runs that command;.next/dev/typessilentfumadocs-mdx && tsc --noEmit && next typegenAFTER tsc in the same command--self-testexit 1, exactly 5 failures (no-row / never-runs / after / no-reason / no-generator); RECONCILED unaffectedAblation C also shows why the fixtures are load-bearing: with the invariant disabled, the real workspace run stayed green (exit 0). Only the literal fixtures can fail it.
The self-test deliberately does not call
git, so the battery stays hermetic. What guards the detector on the real tree is GENERATED_COVERED's own RECONCILED half: ifgitIgnoredPathsever silently answered "nothing is generated", all three table rows would instantly be rows noincludenames, and the gate reds naming them. A broken detector cannot be a quiet one.Gates run locally, all at
25b8ddd788(the final commit, clean tree)Derived with
node scripts/pm/dispatch-gates.mjs(no paths — it takes the change set from the merge base itself), which resolved 1 path and 7 families:check:type-check-coverage— self-test + real pass, both exit 0. Real verdict:check-type-check-coverage: OK — 64/77 workspace packages type-checked …with the new fourth summary line,generated layer: 3 include entr(ies) across 1 package(s) name a path this repo does not check in -- 2 produced by their own typecheck before tsc, 1 declared deliberately ungenerated.check:entry-guard—✓ check:entry-guard: 129 scripts/ file(s) — every entry guard goes through invoked-as.mjs(self-test: 52 cases pass).check:parse-guard—✓ check:parse-guard: 128 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.(self-test: 46 cases pass).check:cross-package-test-inputs— exit 0.check-ci-filter-parity.mjs—OK: all 82 declared cross-package glob(s) (71 unique) are covered by core or crosspkg ….check:nul-bytes—OK (scanned 6296 text file(s) … no raw ASCII control bytes).Declared narrowing:
check:type-check-debt(--re-measure) was not run locally. It runs a realtsc --noEmitper ledger entry and needs a full workspace install plus a dependency-closure build; this container has no installs at all. This diff touches no measurement code path — only the structural pass and its fixtures — and CI runs that family after its build step.origin/mainmoved from7c02a4529cto9faa9bc51dduring the work (#10882,packages/cli). No overlap with this file; the diff here is one file, +650/−15, confirmed by merge-base rather than by a two-dotgit diff.Release
skip-changeset: this PR edits one CI-internal gate script and publishes nothing. That is this workflow's own prescription for the case — lint.yml states it in those words ("this PR edits a CI-internal script is the textbookskip-changesetcase — such a PR releases nothing"). Note the corollary the same comment records: the changeset-family self-tests run unconditionally in lint.yml precisely so askip-changesetPR cannot skip them.Generated by Claude Code