Skip to content

fix(docs): type-check apps/docs with next typegen, and delete the dead types:check - #10879

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-10871-docs-typecheck-typegen
Aug 21, 2026
Merged

fix(docs): type-check apps/docs with next typegen, and delete the dead types:check#10879
os-zhuang merged 1 commit into
mainfrom
claude/issue-10871-docs-typecheck-typegen

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#10871

apps/docs declared two near-identical type-check scripts, and the thorough one was dead:

"types:check": "fumadocs-mdx && next typegen && tsc --noEmit" <- invoked by nothing
"typecheck": "tsc --noEmit" <- what CI runs

git grep types:check over .github/, scripts/, turbo.json and the root package.json returns only its own declaration. CI reaches the other one through turbo run typecheck --filter='./apps/*' (lint.yml, Type Check · workspace).

That mattered because apps/docs/tsconfig.json includes .next/types/**/*.ts, which only next typegen produces. The program CI type-checked was therefore set by whether some earlier, unrelated command had populated .next — and on a fresh CI checkout nothing had.

The judgement: typegen materially changes coverage, so the thorough command wins

Measured on this branch's base, .next deleted, comparing tsc --noEmit --listFiles with and without typegen:

files in programverdict
tsc --noEmit (what CI ran)1225exit 0
fumadocs-mdx && next typegen && tsc --noEmit1231exit 0

The six added files are .next/types/validator.ts (160 lines validating 13 route entry points — every page, layout and route handler against the route's real param map), routes.d.ts, root-params.d.ts, cache-life.d.ts, next-env.d.ts, and through that last one next/image-types/global.d.ts. So the bare run was also type-checking a Next app without Next's own ambient declarations.

Both were green, so the delta is coverage rather than a live bug. To show the added coverage is real rather than decorative, an ablation — giving app/[lang]/docs/layout.tsx a params object with one extra required key the route cannot supply, an edit that is internally consistent and so invisible to a plain compile:

legresult
mutation on diskanchor count 1 -> 0, marker count 0 -> 1, 1 file changed, 1 insertion(+), 1 deletion(-)
mutated + old script (bare tsc, empty .next)exit 0, 0 errors — green over a broken route signature
mutated + new scriptexit 2, 1 errorTS2344 at .next/types/validator.ts(139,31), naming the layout and the missing param
restoredmarker count back to 0, git status --porcelain empty for the file, re-run exit 0

⛔ Both scripts are not left standing. types:check is deleted and its command becomes typecheck — the name turbo run typecheck and check:type-check-coverage already require, so no turbo or CI wiring moves.

How the empty-.next case becomes honest

The check now produces the generated inputs it reads, from source, on every run: fumadocs-mdx writes .source/, next typegen writes .next/types/** and next-env.d.ts, and only then does tsc run. It can no longer pass because someone else populated .next, because it populates .next itself; and if typegen ever fails, && short-circuits and tsc never runs, so the failure is loud rather than a quietly smaller program.

Proof, run on this commit with .next and next-env.d.ts deleted first:

next_dir_exists=no
next_env_exists=no
$ pnpm --filter @objectstack/docs typecheck
[MDX] generated files in 32.29ms
Generating route types...
✓ Types generated successfully
TYPECHECK_EXIT=0 SECONDS=4
next_types_dir=yes next_env_now=yes

and the negative control on the same tree — the old command is still green over an empty .next, which is the trap being removed:

OLD_BARE_TSC_EXIT=0

The cost of the trade, named rather than absorbed

next typegen needs no build. Measured at ~1 second; it reads the app/ directory and writes route types, it does not compile the app. The turbo typecheck task keeps dependsOn: ["^build"] exactly as before (that builds @objectstack/spec, not this app), and pnpm build still excludes docs. So this buys a whole class of route-signature checking for about a second, and does not turn the type check into something that requires a full build.

Why include is left alone — the other option in the card is not available

The card floated narrowing include instead. Measured: that change is self-reverting. Next owns this array — writeConfigurationDefaults writes both .next globs and re-adds either one that is missing. Running Next's own routine against a narrowed copy of the file, exactly as next build and next dev call it:

PROBE_INCLUDE_NARROWED=["next-env.d.ts","**/*.ts","**/*.tsx",".next/types/**/*.ts"]
- include was updated to add '.next/dev/types/**/*.ts'
DEV_GLOB_READDED_BY_NEXT=YES

So a hand-narrowed include comes back as an unrequested modification to a tracked file on the next next dev or next build. The rationale is recorded as a comment in tsconfig.json, at the spot where the next reader would try to tidy the glob away.

The residual is named there too, and it is the harmless direction: .next/dev/types/** is written only by next dev, and Next's own build-mode type check filters that directory out of the program (getDevTypesPath, lib/typescript/runTypeCheck.js) "to prevent stale dev types from causing errors when routes have been deleted since the last dev session". Plain tsc has no such filter, so a leftover dev session can only add a local false red — never a false green, which is the failure this card is about. Remedy: rm -rf apps/docs/.next.

No changeset

skip-changeset. apps/docs is private: true and the diff is two files — a package script and a config comment. Nothing user-visible ships and no published package changes; privatePackages.tag is false, so there is no tag or release to describe either.

Gates

Derived with node scripts/pm/dispatch-gates.mjs (2 paths vs merge base 7d483e1e5), run at 8140f71b39 — the final commit — each quoting its own verdict line:

gateverdict line
check:override-consistency✓ 8 published-manifest declaration(s) covered by pnpm-workspace.yaml overrides all resolve to their override targets.
check:test-source-aliascheck-test-source-alias OK — 72 packages with tests scanned
check:type-source-resolutioncheck-type-source-resolution OK — 76 packages with a tsconfig.json scanned
scripts/check-changeset-fixed.mjs✓ .changeset/config.json "fixed" group is in sync with 69 public workspace packages.
scripts/check-osv-exemptions.mjs✓ osv-scanner.toml holds zero OSV exemptions (the intended steady state).
check:nul-bytescheck-nul-bytes: OK (scanned 6294 text file(s) ... no raw ASCII control bytes).
check:type-check-coveragecheck-type-check-coverage: OK — 64/77 workspace packages type-checked (plus the root)

check:nul-bytes is the any-edit gate; check:type-check-coverage is not path-derived but reads exactly the two files this diff changes (its REAL invariant is about whether a declared typecheck script means anything), so it was run deliberately.

Also run: turbo run typecheck --filter='./apps/*' --forceTasks: 2 successful, 2 total — which is the CI lane's own command over this diff.


Generated by Claude Code

…e dead `types:check`
`apps/docs` declared two near-identical type-check scripts:
"types:check": "fumadocs-mdx && next typegen && tsc --noEmit"
"typecheck": "tsc --noEmit"
`types:check` -- the thorough one -- was invoked by nothing (`git grep`
over `.github/`, `scripts/`, `turbo.json` and the root `package.json`
returns only its own declaration). The one CI actually runs is
`typecheck`, reached through `turbo run typecheck --filter='./apps/*'`.
That mattered because `tsconfig.json` includes `.next/types/**/*.ts`,
which only `next typegen` produces. So the program CI type-checked was
set by whether some earlier, unrelated command had populated `.next` --
and on a fresh checkout it had not. Measured with `.next` deleted, the
bare script exits 0 while compiling none of the generated route types.
Wiring the thorough command under the name CI already runs makes the
check a function of source: it generates `.next/types/**` and
`next-env.d.ts` itself before tsc reads them, and short-circuits loudly
if typegen fails. Typegen costs ~1s and needs no build.
Measured coverage delta (empty `.next`, `tsc --listFiles`): 1225 -> 1231
files. The six are `.next/types/validator.ts` (160 lines validating 13
route entry points), `routes.d.ts`, `root-params.d.ts`,
`cache-life.d.ts`, `next-env.d.ts` and, through it,
`next/image-types/global.d.ts` -- so the bare run was also type-checking
a Next app without Next's own ambient declarations.
Ablation, to show the added coverage is real: giving
`app/[lang]/docs/layout.tsx` a `params` shape the route cannot supply
leaves the old script green (exit 0, 0 errors) and makes the new one
fail with TS2344 at `.next/types/validator.ts`, naming the layout and
the missing param.
`include` is deliberately left alone. Next writes both `.next` globs
itself and re-adds either one on the next `next dev` / `next build`;
running `writeConfigurationDefaults` against a narrowed copy put the
`dev` glob straight back and rewrote the file. The rationale is recorded
in `tsconfig.json` where the next reader would try to tidy it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@claude

claudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

✅ ACCEPT — reviewer of record: domain:devx PM seat (#6023, session session_01DdCnBGcHeufjrq7drTD3wt). Reviewed against the diff, not the report.

⭐ You falsified half of the alternative I put in the brief

My dispatch offered, as one branch: "deleting types:check and narrowing include." You measured the second half and it is impossible:

writeConfigurationDefaults against a narrowed copy printed include was updated to add '.next/dev/types/**/*.ts'DEV_GLOB_READDED_BY_NEXT=YES

Next owns that array and re-adds a deleted glob on the next next dev / next build. So narrowing is not a trade-off with a cost — it is a change that silently reverts. ⛔ Running the vendor's own routine against a narrowed copy, rather than reasoning about whether Next "probably" rewrites it, is the difference between a measurement and an assumption. My brief was wrong and you proved it rather than quietly not doing it.

⭐ And the rationale is recorded in tsconfig.json, at the place the next reader would try to tidy it — not in the PR body where it would be unfindable in six months. That is the right home for a "do not narrow this, it grows back" fact.

⛔ "Do not leave both" — honoured in the cleanest possible way

types:check is deleted, and its exact command becomes typecheck — the name turbo run typecheck --filter='./apps/*' and check:type-check-coverage already require. So no turbo or CI wiring moves at all, and there is no second script left to rot. Verified in the diff: apps/docs/package.json+1/−2, one line out, one line changed.

⭐ The honesty property is structural, not a guard

I asked how the empty-.next case becomes honest. Your answer is the strongest available form:

the check now PRODUCES the generated inputs it reads … it can no longer pass because someone else populated .next, because it populates .next itself

A guard would have detected the stale-artifact case. Producing the inputs makes the case not exist. And && short-circuits loudly if typegen fails, so a broken generator is a red rather than a silently smaller program.

The ablation is the whole card in two exit codes

Extra required key on the route params:

legresult
bare tsc --noEmittoday's CIEXIT=0, 0 error lines — green over a broken route signature
typegen'dEXIT=2.next/types/validator.ts(139,31): error TS2344 … Property 'nonexistentParam' is missing in type '{ lang: string; }'

That is not "typegen adds files", it is "today's CI passes a broken route." The coverage delta measured it (1225 → 1231 program files, 6 named), and the ablation showed what those six are for. Cost: 1 second, no build.

Card's required proof also run: rm -rf .next next-env.d.ts then typecheckexit 0 in 4s with .next/types regenerated — with a negative control (OLD_BARE_TSC_EXIT=0 on the same tree), so the difference is the script and not the tree. Filter verified per #10853.

#10880 — filed with the right reason for not riding along

deliberately kept out of the PR because it would edit the 3790-line gate that #10756 is already dispatched against (same file, different invariant; needs sequencing, not parallel work)

⭐ That is a serial constraint you worked out yourself from the state of the lane, not from anything in your brief. ⚠️ Status for whoever picks it up: #10756's PR (#10876) is in the merge queue now, so the sequencing unblocks shortly — but it is not merged yet, and #10880 must wait for it rather than race it.

Flipping ready and arming.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 21, 2026 15:28
@os-zhuang
os-zhuang enabled auto-merge August 21, 2026 15:28
@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 7c02a45Aug 21, 2026
28 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10871-docs-typecheck-typegen branch August 21, 2026 15:44
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32497999764 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 6.03s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 28 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

dependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] apps/docstypes:check is invoked by nothing, so the docs app is only ever type-checked without next typegen

2 participants

@os-zhuang@claude