@@ -152,6 +152,7 @@ import { tmpdir } from 'node:os';
152152import { dirname , join , relative , resolve } from 'node:path' ;
153153import { fileURLToPath } from 'node:url' ;
154154
155+ import { buildStamp } from '../../../scripts/check-regen-pending.mjs' ;
155156import { scanSource } from '../../../scripts/js-comment-mask.mjs' ;
156157import { inspectBundleFreshness } from './lib/dist-freshness' ;
157158
@@ -927,6 +928,71 @@ function selfTest(): never {
927928 ! inspectBundleFreshness ( fresh , 'check' , RERUN ) . fresh ,
928929 mtimes ( ) ,
929930 ) ;
931+
932+ // ── The acquittal: an mtime accusation that CAN be answered (#16175) ────
933+ // Every case above is the mtime rule convicting, and until #16175 there was
934+ // nothing it would accept as an answer. So it also refused the tree a
935+ // `git merge`, `git checkout` or `git worktree add` leaves behind: those
936+ // re-check-out an UNCHANGED source file — bytes identical, mtime bumped —
937+ // and the build that follows is a turbo cache hit that rewrites nothing, so
938+ // every dist/ mtime stays where the previous build left it. Measured on the
939+ // real tree: `touch packages/spec/src/data/query.zod.ts` with `git status`
940+ // empty made this gate exit 1 and prescribe a multi-minute rebuild of
941+ // bundles that were already exactly current.
942+ //
943+ // The evidence is `dist/.build-input-hash`, which every build of the package
944+ // writes AFTER its unconditional `tsup` pass. It may only ever ACQUIT, and
945+ // the two cases after this one are what keep that non-vacuous.
946+ const stampFile = join ( fresh , 'dist' , '.build-input-hash' ) ;
947+ // Two steps, for the reason `dist-freshness.test.ts` gives: `buildStamp`
948+ // computes `actual` only when a syntactically valid digest is recorded, so
949+ // seed a placeholder, read what the sources really hash to, then write that.
950+ // Asked of the rule's own reader rather than hardcoded — the input set
951+ // includes turbo.json's globalDependencies and every input's repo-relative
952+ // path, so a literal here would rot into a `mismatch` that reads exactly
953+ // like the refusal these cases exist to tell apart. The stamp is written
954+ // last, so it records the sandbox as it now stands, tsup.config.ts included
955+ // (that file is in the digest's input set as well as in the mtime rule's).
956+ writeFileSync ( stampFile , `${ '0' . repeat ( 64 ) } \n` ) ;
957+ const digest = buildStamp ( fresh ) . actual ;
958+ check (
959+ 'the sandbox build-input digest can be computed at all (the fixture is not vacuous)' ,
960+ typeof digest === 'string' && / ^ [ 0 - 9 a - f ] { 64 } $ / . test ( digest ) ,
961+ JSON . stringify ( digest ) ,
962+ ) ;
963+ writeFileSync ( stampFile , `${ digest } \n` ) ;
964+ check (
965+ 'ACQUITS an mtime-stale tree whose build stamp matches the sources (#16175)' ,
966+ inspectBundleFreshness ( fresh , 'check' , RERUN ) . fresh ,
967+ mtimes ( ) ,
968+ ) ;
969+
970+ // The half that makes the case above non-vacuous. If the acquittal were
971+ // keyed on the stamp's mere PRESENCE rather than on the digest, this would
972+ // stay green — and that is #7122's false green restored, one axis over. The
973+ // stamp is written first and the source edited after, so the recorded digest
974+ // is genuinely stale rather than never-valid.
975+ writeFileSync ( srcFile , 'export const a = 2;\n' ) ;
976+ stamp ( srcFile , 0 ) ;
977+ check (
978+ 'and CONVICTS the same tree the moment a source byte actually changes' ,
979+ ! inspectBundleFreshness ( fresh , 'check' , RERUN ) . fresh ,
980+ mtimes ( ) ,
981+ ) ;
982+
983+ // Absence of the freshness input is not licence to acquit (#4690), and
984+ // neither is a truncated or half-flushed write. Anything that is not 64 hex
985+ // characters is `unstamped`, which leaves the mtime verdict standing. The
986+ // source is restored to the bytes the digest above was taken over first, so
987+ // the ONLY reason this refuses is the stamp itself.
988+ writeFileSync ( srcFile , 'export const a = 1;\n' ) ;
989+ stamp ( srcFile , 0 ) ;
990+ writeFileSync ( stampFile , 'not-a-digest\n' ) ;
991+ check (
992+ 'ignores a build stamp that is not a digest at all' ,
993+ ! inspectBundleFreshness ( fresh , 'check' , RERUN ) . fresh ,
994+ mtimes ( ) ,
995+ ) ;
930996 } finally {
931997 rmSync ( fresh , { recursive : true , force : true } ) ;
932998 }
0 commit comments