Out-of-scope finding from implementing #6227 (PR #7114), recorded per PD #10. Unassigned, not queued. Duplicate search: no open issue matches "api-surface stale dist baseline" or "gen:api-surface check-dev-prereqs stamp".
Fact, with the instance that produced it
packages/spec/scripts/build-api-surface.ts reads the built dist — its own docblock says so:
Reads the built dist — run after pnpm --filter @objectstack/spec build.
That is a documented precondition and nothing enforces it. Neither build-api-surface.ts nor build-export-origins.ts consults any freshness signal; they parse whatever .d.ts happens to be on disk.
Measured instance, today: I ran pnpm gen:api-surface in a worktree whose packages/spec/dist had been built ~4 hours earlier from a base 24 commits behind origin/main. The generator did not complain. It wrote a baseline that deleted a line:
--- a/packages/spec/api-surface/contracts.json
+++ b/packages/spec/api-surface/contracts.json
@@ -163,7 +163,6 @@
"JobRetryPolicy (interface)",
- "JobRunOutcome (interface)",
"JobSchedule (interface)",
JobRunOutcome is a live export (packages/spec/src/contracts/job-service.ts:61) and is present in the origin/main baseline. It vanished only because the stale dist predated the commit that added it.
Why this is worse than an ordinary stale-artifact trip
By this generator's own rule, that diff is a breaking change:
A REMOVED export or a CHANGED factory signature is breaking (bump major).
So the failure mode is not a red build — it is a green one carrying a phantom breaking removal:
- Stale dist ⇒ generator writes a baseline missing a real export.
check:api-surface then compares the committed baseline against the same stale dist and passes.
- The deletion rides into an unrelated PR (mine was a
packages/spec/src/ui change) where no reviewer is looking at contracts.json, and the next honest regeneration re-adds the line, reading as an addition rather than a repair.
Every gate is green at every step. This is the same structural blindness check-adr-0087-registration.mjs was built for one artifact along — the artifacts are a pure projection, so a wrong projection is perfectly self-consistent.
The primitive to fix it already exists and already names this exact package
scripts/check-dev-prereqs.mjs defines staleness precisely, and by content hash rather than mtime (#5864):
stale(pkg) ⇔ sha256(build inputs of pkg, now) ≠ contents of <pkg>/dist/.build-input-hash
Its own commentary calls out this very package for this very reason — "packages/spec/dist (the fake-drift amplifier)" — and its rationale is exactly the failure above: "a MISSING dist fails loudly, a STALE one LIES."
The stamp is written by packages/spec's build as its last step. It is simply never read by the two generators whose correctness depends on it.
Suggested direction (not pre-judged)
- Have
build-api-surface.ts and build-export-origins.ts assert dist/.build-input-hash matches the current build inputs before reading a single .d.ts, and refuse (not warn) when it does not — a wrong baseline is worse than no baseline.
- Refusing in
--check mode too, so CI cannot pass against a stale dist either.
- Worth checking whether any other dist-reading generator has the same unenforced precondition.
Why it did not corrupt PR #7114
Caught by eye, not by a gate: the regenerated contracts.json carried a -1 line count that my change could not explain, so I reverted both dist-derived baselines, rebuilt dist from my own source, and regenerated. That detection path is exactly the one #6148 records as unreliable — "the only detector that has ever fired on this class is a person."
Refs: #6227, PR #7114, #5726, #5864, #7090.
Generated by Claude Code
Out-of-scope finding from implementing #6227 (PR #7114), recorded per PD #10. Unassigned, not queued. Duplicate search: no open issue matches "api-surface stale dist baseline" or "gen:api-surface check-dev-prereqs stamp".
Fact, with the instance that produced it
packages/spec/scripts/build-api-surface.tsreads the built dist — its own docblock says so:That is a documented precondition and nothing enforces it. Neither
build-api-surface.tsnorbuild-export-origins.tsconsults any freshness signal; they parse whatever.d.tshappens to be on disk.Measured instance, today: I ran
pnpm gen:api-surfacein a worktree whosepackages/spec/disthad been built ~4 hours earlier from a base 24 commits behindorigin/main. The generator did not complain. It wrote a baseline that deleted a line:JobRunOutcomeis a live export (packages/spec/src/contracts/job-service.ts:61) and is present in theorigin/mainbaseline. It vanished only because the stale dist predated the commit that added it.Why this is worse than an ordinary stale-artifact trip
By this generator's own rule, that diff is a breaking change:
So the failure mode is not a red build — it is a green one carrying a phantom breaking removal:
check:api-surfacethen compares the committed baseline against the same stale dist and passes.packages/spec/src/uichange) where no reviewer is looking atcontracts.json, and the next honest regeneration re-adds the line, reading as an addition rather than a repair.Every gate is green at every step. This is the same structural blindness
check-adr-0087-registration.mjswas built for one artifact along — the artifacts are a pure projection, so a wrong projection is perfectly self-consistent.The primitive to fix it already exists and already names this exact package
scripts/check-dev-prereqs.mjsdefines staleness precisely, and by content hash rather than mtime (#5864):Its own commentary calls out this very package for this very reason — "
packages/spec/dist(the fake-drift amplifier)" — and its rationale is exactly the failure above: "a MISSING dist fails loudly, a STALE one LIES."The stamp is written by
packages/spec's build as its last step. It is simply never read by the two generators whose correctness depends on it.Suggested direction (not pre-judged)
build-api-surface.tsandbuild-export-origins.tsassertdist/.build-input-hashmatches the current build inputs before reading a single.d.ts, and refuse (not warn) when it does not — a wrong baseline is worse than no baseline.--checkmode too, so CI cannot pass against a stale dist either.Why it did not corrupt PR #7114
Caught by eye, not by a gate: the regenerated
contracts.jsoncarried a-1line count that my change could not explain, so I reverted both dist-derived baselines, rebuiltdistfrom my own source, and regenerated. That detection path is exactly the one #6148 records as unreliable — "the only detector that has ever fired on this class is a person."Refs: #6227, PR #7114, #5726, #5864, #7090.
Generated by Claude Code