Observed in a scaffolded 20.1.0 app (private, ivndev001/dz-showcase): a commit with a genuine
type error (an island props interface whose index signature included undefined, not assignable
to JsonValue) passed bin/check's typecheck step locally and failed the same step in CI, on
the same commit. Control on the SAME tree: rm tsconfig.tsbuildinfo then x verify --only typecheck → red. So the gitignored incremental tsconfig.tsbuildinfo made the step report green
over a tree that does not typecheck — a violation of this repo's axiom 5, "green must mean the
same thing for everyone": CI (no buildinfo, always cold) and a local run (warm) disagreed on the
identical commit.
What the step actually runs
packages/cli/src/verify-checks.ts's typecheck step: bunx tsc -b --pretty false at the repo
root, no --force, matching verify-step.ts's and cmd-verify.ts's own description of the step.
A scaffolded app's tsconfig (packages/cli/src/templates/scaffold-repo.ts's rootTsconfig) is a
single flat project — "incremental": true, no composite, no references — pinned to
"typescript": "^7.0.2" (TYPESCRIPT_VERSION, same file). tsconfig.tsbuildinfo is written next
to tsconfig.json (default location, no tsBuildInfoFile override) and is in the scaffold's
.gitignore.
What I tried to reproduce it with, and could not
Working hypothesis going in: tsc -b's incremental mode is either (a) not truly content-hash
sound (some fast-path trusts file mtime over content) or (b) persists a stale semantic-diagnostics
verdict across a run that never re-examined a changed file. Both are real, version-dependent
behaviours of TypeScript's build-mode incremental checker historically.
Tested directly against this repo's own pinned typescript@^7.0.2 (confirmed via
bunx tsc --version → Version 7.0.2, the "TypeScript 7" native rewrite):
- Unchanged-tree replay.
tsc -b on an erroring single project, then tsc -b again with
nothing touched at all. Stayed red both times — diagnostics are persisted in
semanticDiagnosticsPerFile inside .tsbuildinfo and re-reported on an up-to-date project, not
dropped.
- Mtime backdating.
touch -d '2000-01-01' on the erroring file (content unchanged from the
run that recorded it), then touch -d it again with different bad content but an OLD mtime
relative to .tsbuildinfo's own mtime. Still red — the recorded fileInfos[].version is a
content hash, not an mtime, and tsc -b reads and hashes the file regardless of mtime ordering
in this version.
- The exact
git stash / checkout / checkout back / stash pop sequence the working
tree's prior history actually had: clean baseline committed and typechecked (green, writes
.tsbuildinfo); dirty edit introduces the type error; git stash (never typechecked in this
state); an unrelated commit on the feature branch; git checkout to another branch and back;
git stash pop restores the bad edit. First tsc -b to ever see this exact bad tree: red.
Repeated with git stash genuinely reverting mtimes to a fresh "now" (git rewrites mtime on
checkout/stash pop for every file whose content it writes — I could not get a scenario
where the erroring file's mtime was OLDER than .tsbuildinfo's own mtime as a result of these
operations; mtimes only moved forward).
- The same sequence across a real composite, multi-project reference graph (root
tsconfig
with no files, referencing two composite sub-projects, one importing the other, matching the
shape this framework's OWN root tsconfig.json uses with its 32 references — a different
shape from a scaffolded app's flat one, tried in case the bug needs project boundaries). Error
introduced in the upstream project via the same stash/checkout/pop sequence. Still red, both on
first sight and on a repeat run with nothing changed.
- Deleted-then-restored file, with different content than either version tsc had seen before.
Deleted the imported file (correctly reported TS2307), then recreated it with new bad content
under the same path. Still red.
All five runs — cold or warm, single-project or multi-project, mtime-manipulated or not — correctly
caught the type error. I did not find a way to make tsc -b @7.0.2 report green over a tree with a
real type error using the mechanisms named in the task brief (project references, mtime games via
git, a deleted/restored file). I did not test: a node_modules type change (no vector for that
in a workspace-symlinked monorepo with skipLibCheck: true, and none in a scaffolded app either,
short of a bun install that changes a @types/* version — untried, lower priority since it is
not implicated by anything in the original report), and a genuinely concurrent second tsc
process racing the same .tsbuildinfo (e.g. an editor's TS server, or x dev's own typecheck,
writing to the same file x verify's one-shot run reads) — I have no way to drive that
deterministically in this sandbox, and the original report does not mention a second process
running.
What this leaves
- Either the original failure depended on a detail of the dz-showcase working tree or its exact
operation sequence that I have not reconstructed (a concurrent writer is my leading unexcluded
suspect — it is the one class of cause that does not require tsc's content-hash check itself
to be unsound, only for two processes to interleave writes to one file), or
- TypeScript 7.0.2's incremental build is sound against everything above and the defect needs a
detail from the actual dz-showcase tree (exact tsconfig, exact git history, exact TypeScript
version actually installed there at the time — bun.lock can pin a different resolved version
than the ^7.0.2 range if the app's lockfile is older) that a synthetic repro cannot supply
without that tree.
I'm not shipping a mitigation (e.g. --force on the gate's typecheck step, or a "cold typecheck
in CI-parity mode") against this: I have no reproduction to justify the cost, and axiom 5's
argument needs a demonstrated failure, not a hypothetical one. Filing this so the investigation
(and the five ruled-out mechanisms) aren't lost, and so anyone who CAN reproduce it starts from
here instead of from the same five dead ends. If it recurs, the next data point that would move
this forward is the dz-showcase tree's actual resolved typescript version at the time
(bun.lock), and whether x dev or an editor TS server was running against the same app root
concurrently with the bin/check run that came back green.
Observed in a scaffolded 20.1.0 app (private,
ivndev001/dz-showcase): a commit with a genuinetype error (an island props interface whose index signature included
undefined, not assignableto
JsonValue) passedbin/check'stypecheckstep locally and failed the same step in CI, onthe same commit. Control on the SAME tree:
rm tsconfig.tsbuildinfothenx verify --only typecheck→ red. So the gitignored incrementaltsconfig.tsbuildinfomade the step report greenover a tree that does not typecheck — a violation of this repo's axiom 5, "green must mean the
same thing for everyone": CI (no buildinfo, always cold) and a local run (warm) disagreed on the
identical commit.
What the step actually runs
packages/cli/src/verify-checks.ts'stypecheckstep:bunx tsc -b --pretty falseat the reporoot, no
--force, matchingverify-step.ts's andcmd-verify.ts's own description of the step.A scaffolded app's tsconfig (
packages/cli/src/templates/scaffold-repo.ts'srootTsconfig) is asingle flat project —
"incremental": true, nocomposite, noreferences— pinned to"typescript": "^7.0.2"(TYPESCRIPT_VERSION, same file).tsconfig.tsbuildinfois written nextto
tsconfig.json(default location, notsBuildInfoFileoverride) and is in the scaffold's.gitignore.What I tried to reproduce it with, and could not
Working hypothesis going in:
tsc -b's incremental mode is either (a) not truly content-hashsound (some fast-path trusts file mtime over content) or (b) persists a stale semantic-diagnostics
verdict across a run that never re-examined a changed file. Both are real, version-dependent
behaviours of TypeScript's build-mode incremental checker historically.
Tested directly against this repo's own pinned
typescript@^7.0.2(confirmed viabunx tsc --version→Version 7.0.2, the "TypeScript 7" native rewrite):tsc -bon an erroring single project, thentsc -bagain withnothing touched at all. Stayed red both times — diagnostics are persisted in
semanticDiagnosticsPerFileinside.tsbuildinfoand re-reported on an up-to-date project, notdropped.
touch -d '2000-01-01'on the erroring file (content unchanged from therun that recorded it), then
touch -dit again with different bad content but an OLD mtimerelative to
.tsbuildinfo's own mtime. Still red — the recordedfileInfos[].versionis acontent hash, not an mtime, and
tsc -breads and hashes the file regardless of mtime orderingin this version.
git stash/checkout/checkout back/stash popsequence the workingtree's prior history actually had: clean baseline committed and typechecked (green, writes
.tsbuildinfo); dirty edit introduces the type error;git stash(never typechecked in thisstate); an unrelated commit on the feature branch;
git checkoutto another branch and back;git stash poprestores the bad edit. Firsttsc -bto ever see this exact bad tree: red.Repeated with
git stashgenuinely reverting mtimes to a fresh "now" (git rewrites mtime oncheckout/stash popfor every file whose content it writes — I could not get a scenariowhere the erroring file's mtime was OLDER than
.tsbuildinfo's own mtime as a result of theseoperations; mtimes only moved forward).
tsconfigwith no files, referencing two composite sub-projects, one importing the other, matching the
shape this framework's OWN root
tsconfig.jsonuses with its 32references— a differentshape from a scaffolded app's flat one, tried in case the bug needs project boundaries). Error
introduced in the upstream project via the same stash/checkout/pop sequence. Still red, both on
first sight and on a repeat run with nothing changed.
Deleted the imported file (correctly reported
TS2307), then recreated it with new bad contentunder the same path. Still red.
All five runs — cold or warm, single-project or multi-project, mtime-manipulated or not — correctly
caught the type error. I did not find a way to make
tsc -b @7.0.2report green over a tree with areal type error using the mechanisms named in the task brief (project references, mtime games via
git, a deleted/restored file). I did not test: anode_modulestype change (no vector for thatin a workspace-symlinked monorepo with
skipLibCheck: true, and none in a scaffolded app either,short of a
bun installthat changes a@types/*version — untried, lower priority since it isnot implicated by anything in the original report), and a genuinely concurrent second
tscprocess racing the same
.tsbuildinfo(e.g. an editor's TS server, orx dev's own typecheck,writing to the same file
x verify's one-shot run reads) — I have no way to drive thatdeterministically in this sandbox, and the original report does not mention a second process
running.
What this leaves
operation sequence that I have not reconstructed (a concurrent writer is my leading unexcluded
suspect — it is the one class of cause that does not require
tsc's content-hash check itselfto be unsound, only for two processes to interleave writes to one file), or
detail from the actual dz-showcase tree (exact tsconfig, exact
githistory, exact TypeScriptversion actually installed there at the time —
bun.lockcan pin a different resolved versionthan the
^7.0.2range if the app's lockfile is older) that a synthetic repro cannot supplywithout that tree.
I'm not shipping a mitigation (e.g.
--forceon the gate'stypecheckstep, or a "cold typecheckin CI-parity mode") against this: I have no reproduction to justify the cost, and axiom 5's
argument needs a demonstrated failure, not a hypothetical one. Filing this so the investigation
(and the five ruled-out mechanisms) aren't lost, and so anyone who CAN reproduce it starts from
here instead of from the same five dead ends. If it recurs, the next data point that would move
this forward is the dz-showcase tree's actual resolved
typescriptversion at the time(
bun.lock), and whetherx devor an editor TS server was running against the sameapp rootconcurrently with the
bin/checkrun that came back green.