Skip to content

ci(turbo): derive the type-check inputs guard from each package's tsc program (#3514) - #4176

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3514-turbo-inputs-guard
Aug 10, 2026
Merged

ci(turbo): derive the type-check inputs guard from each package's tsc program (#3514)#4176
yinlianghui merged 1 commit into
mainfrom
claude/issue-3514-turbo-inputs-guard

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3514

Premise check first — half of the card had already shipped

The card's "Suggested fix" one-liner ($TURBO_ROOT$/vitest.config.mts in the type-check inputs) is already on main: the blocking PR #3513 merged as 5534087 and carried it, exactly as the PM re-scope comment of 2026-08-07 ruled. So there was no one-liner left to add, and the card's own reproduce script inverts on this tip — planting a type error in the root vitest.config.mts now moves the cache key rather than replaying a stale green.

What survived is the durability half, and the delegated ruling asked for the derived guard rather than pinned pairs. That is what this PR is. It also found a third instance of the class, live on main today.

The mechanism

Turbo hashes a task from its inputs. $TURBO_DEFAULT$ covers only files inside the package directory, and globalDependencies is unset — so any file a package's tsc program reads from elsewhere in the repo is invisible to the cache key. When such a file changes, turbo does not re-run the task; it replays the previous verdict. .github/workflows/ci.yml persists .turbo/cache through actions/cache, so one poisoned entry rides into later runs.

Twice now the list was fixed by hand after the fact: scripts/vite-*.ts, then vitest.config.mts (#3476 / #3513). Two hand-fixes for one structural cause is this repo's threshold for a gate.

What ships

1. scripts/__tests__/turbo-type-check-inputs.test.ts — derives the requirement instead of restating it. For every workspace package with a type-check script it resolves the tsc projects that script actually drives (bare tsc --noEmit, tsc -p, and tsc -b plus its project references), parses each through TypeScript's own parseJsonSourceFileConfigFileContent so fileNames is the real program root set, collects the extends chain via extendedSourceFiles, and reports every file that lands outside the package directory. Each one must be matched by a $TURBO_ROOT$ input, or the test reds naming the package, the file and the entry to add.

Three companion assertions keep the guard from decaying into the thing it guards against: the sweep must find packages and a non-empty out-of-package set (a silently-degraded derivation would pass vacuously); every $TURBO_ROOT$ entry must match a file on disk; and every entry must still be required by some package's program, so the list can only shrink.

Stated narrowings, in the file's docblock: it asserts over program root files (config include / files / extends / references), not the transitive import closure — building 40+ real programs needs a built workspace and minutes of CPU, and the narrowing is sound for this failure because a composite project must list every file in its program, which is why both known drifts were include entries. Glob syntax it does not implement throws rather than being approximated, since an approximated match that comes out true is a file waved through while turbo does not hash it. Unparsed type-check script shapes and unknown workspace glob shapes throw for the same reason.

2. turbo.json — the two entries the guard's first run named:

+        "$TURBO_ROOT$/tsconfig.json",
+        "$TURBO_ROOT$/tsconfig.base.json"

39 packages' tsconfigs extend the repo-root tsconfig.json; examples/byo-backend-console extends the root tsconfig.base.json. Compiler options are as load-bearing as source — flipping strict there changes every package's verdict — and neither file was hashed.

Measured

Cache key, before the fix. Mutating the root tsconfig.json leaves the hash untouched:

$ npx turbo run type-check --filter @object-ui/types --dry=json     # PRE-FIX inputs
  baseline                       e78d50ad01f506da
  after mutating root tsconfig   e78d50ad01f506da     <- frozen

End to end, @object-ui/types, pre-fix inputs — warm the cache green, plant a real TS2688 in the root tsconfig.json, re-run:

=== [1] PRE-FIX, clean tree: warm the cache ===
 Tasks:    1 successful, 1 total
Cached:    0 cached, 1 total
  Time:    22.714s
exit=0

=== [3] PRE-FIX, defect present: re-run  <-- RED BASELINE ===
 Tasks:    1 successful, 1 total
Cached:    1 cached, 1 total
  Time:    44ms >>> FULL TURBO
exit=0

=== [4] control: same tree, cache bypassed (--force) ===
error TS2688: Cannot find type definition file for 'objectui-3514-does-not-exist'.
 Tasks:    0 successful, 1 total
exit=2

The verdict depended on cache state, not on the code. With the fix, same tree, same defect:

=== [5] apply the fix (turbo.json), defect still present ===
error TS2688: Cannot find type definition file for 'objectui-3514-does-not-exist'.
Cached:    0 cached, 1 total          <- cache MISS
exit=1

=== [6] restore tsconfig.json, keep the fix: back to green ===
 Tasks:    1 successful, 1 total
exit=0

The card's own file, for symmetry. With $TURBO_ROOT$/vitest.config.mts present, touching the root Vitest config moves @object-ui/console's hash (bed674f43a6c248f to aea440d108ddd00e); with that entry removed it is frozen at e4f708651f5bce3c. #3513's entry is genuinely load-bearing, and this guard would have caught its absence.

Reverse verification

Each $TURBO_ROOT$ entry removed in turn, and a phantom entry added:

[2] drop $TURBO_ROOT$/tsconfig.json      -> 39 packages red, e.g.
    "@object-ui/app-shell's type-check program reads tsconfig.json from outside
     packages/app-shell ... Add "$TURBO_ROOT$/tsconfig.json" to turbo.json's
     `type-check` inputs."

[3] drop $TURBO_ROOT$/vitest.config.mts  -> Tests  1 failed | 42 passed (43)
    "@object-ui/console's type-check program reads vitest.config.mts from
     outside apps/console ..."          <- the card's drift #2, caught

[4] add $TURBO_ROOT$/no-such-file-3514.json -> Tests  2 failed | 41 passed (43)
    "matches no file in the repo. It is hashing nothing while reading as coverage"
    "is not required by any package's type-check program any more"

Gates

pnpm exec vitest run scripts/__tests__/turbo-type-check-inputs.test.ts
   Test Files  1 passed (1)      Tests  43 passed (43)

pnpm exec vitest run scripts/__tests__          (whole scripts suite)
   Test Files  33 passed (33)    Tests  665 passed (665)

pnpm type-check:scripts                          exit=0
node scripts/check-control-bytes.mjs             OK (3853 tracked text files)
npx eslint scripts/__tests__/turbo-type-check-inputs.test.ts   exit=0

Changeset: none owed — check-changeset-presence.mjs arbitrates: "No source of a released package changed in this range, so no changeset is owed." (turbo.json plus a test, no released package src/.)


Generated by Claude Code

… program (#3514)

The hand-maintained `$TURBO_ROOT$` inputs list has been wrong twice for one
structural reason: a package's tsc program reaches outside its package
directory, where `$TURBO_DEFAULT$` cannot see it and `globalDependencies` is
unset. Turbo then replays the previous verdict instead of re-running, and CI
persists `.turbo/cache`, so the stale green travels.

scripts/__tests__/turbo-type-check-inputs.test.ts derives the requirement
instead of restating it: it walks every workspace package's real type-check
program (the projects its `type-check` script drives, their include/files root
set, their `extends` chain, and `tsc -b` project references) via TypeScript's
own config parser, collects every file landing outside the package directory,
and asserts turbo's inputs cover each one.

Its first run named a third, wider instance: 39 packages extend the repo-root
`tsconfig.json` and `examples/byo-backend-console` extends `tsconfig.base.json`,
neither of which was hashed. Both entries ship here.

Measured on this branch, @object-ui/types, pre-fix inputs: warm the cache green,
plant a real TS2688 in the root tsconfig.json, re-run -> `FULL TURBO` in 44ms,
exit 0, while --force on the same tree exits 2. With the fix the same tree is a
cache miss and exits 1 naming the error.

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

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 10, 2026 5:21pm

Request Review

@github-actions github-actions Bot added the tests label Aug 10, 2026
@yinlianghui
yinlianghui marked this pull request as ready for review August 10, 2026 17:32
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit eb5f8ce Aug 10, 2026
19 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3514-turbo-inputs-guard branch August 10, 2026 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants