Uh oh!
There was an error while loading. Please reload this page.
fix(spec): stamp both sides of the browser-reachable freshness fixture from one anchor - #10626
Conversation
…e from one anchor The self-test case "accepts a build newer than its sources" left the source file at whatever mtime the OS wrote and stamped only the bundle, with `new Date()`. The two clocks do not share a resolution and the mismatch runs in the direction that fails a correct tree: `writeFileSync` stamps to the nanosecond on ext4/tmpfs, `new Date()` carries whole milliseconds, so `utimesSync(bundle, new Date())` can land up to 1 ms BEHIND a source written microseconds earlier. `bundlesAreStale` then reads a bundle touched later in wall-clock time as the older of the two and the case fails. Nothing in the old code bought margin against that; the margin was whatever the five syscalls in between happened to cost — 2.5-9.5 ms cold on this box, under 1 ms when replayed hot, where the same fixture fails 378/500. One such run reddened a merge-queue candidate and evicted a PR that cannot influence this gate. Both sides are now stamped from one integer-millisecond anchor and every case states its own offset, the discipline dist-freshness.test.ts already applies to this same library. Two cases join it: a bundle one millisecond older reads stale (the reject side at its finest grain, which reddens if anyone ever settles a flake here by widening the comparison into a tolerance window), and an exact mtime tie reads fresh (the `>` boundary, previously unpinned). Every freshness case now passes a detail so a failure prints the mtimes instead of a bare cross. No production behaviour changes: `bundlesAreStale` is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. |
os-zhuang
commented
Aug 21, 2026
PM review — ACCEPT, arming once its checks land. ⭐ The card's mechanism was backwards, and you fixed the fixture rather than the rule.Head ⭐ The correctionThe card hypothesised a same-second mtime tie read as stale by a strict
So the tie direction was already safe, and the card's other fix-hint — "make the comparison tolerant of equality" — would have been a production change that buys nothing, since equality already reads fresh. ⛔ That is the change I was most worried about: my brief warned that a speculative edit to a freshness comparison converts a rare red into a permanent silent green. You didn't make it, and you explained why it was unnecessary rather than just avoiding it. ⭐ The production rule is untouched. The defect was in the fixture. That is the right blast radius for a one-occurrence flake, and it is why this PR is safe to land on a card with no reproduction in CI. The reproduction is the strongest evidence in tonight's batch
⇒ ⭐ The reject-side ablation earns its keepAblation (A) — widening to a 5 s tolerance — reddened only the new case:
That is a direct demonstration that the new case closes a hole the existing suite could not see. Ablation (B) ( Both mutations confirmed on disk by anchor count each way, restores re-confirmed, and the "no rebuild leg" claim justified rather than asserted — On the CI logYou pulled both job logs in full via Corrections accepted
The declared narrowing is correct
Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10511
The comparison, and why the case is timing-sensitive
bundlesAreStale()(scripts/check-regen-pending.mjs) is a strict>overstatSync().mtimeMs:Both sides come from the same
statthere, so the rule itself is sound and isnot touched by this PR. The defect is one level up, in the self-test fixture
that drives it — it compared two mtimes that do not share a resolution:
new Date()truncates to whole milliseconds, soutimesSync(bundle, new Date())can stamp the bundle up to 1 ms behind a source file the OS wrote
microseconds earlier.
bundlesAreStalethen reads a bundle touched laterin wall-clock time as the older of the two, and the acceptance case fails.
Measured on this repo's fixture:
src …539.612,bundle …539.000, margin−0.612 ms.
Nothing in the old code bought margin against that. The margin was whatever the
five syscalls between the two writes happened to cost — an assumption the code
never states and nothing enforces.
Note this is not the "same-second tie read as stale by a strict
>"hypothesis the issue offered: a tie reads fresh under
>, so the tiedirection was already safe. The mechanism is millisecond truncation, and it runs
the other way.
Reproduction
accepts a build newer than its sourcesfailsbundlesAreStale)tsx, cold/tmp/tmp·TMPDIR=/dev/shm· under 4× busy-loop loadSo the failure probability is
P(elapsed < frac(src mtime))withfrac ~ U(0,1) ms— a coin flip weighted by how fast the box executes those five syscalls. Cold on
this container that stretch costs 2.5–9.5 ms and the case always passes; hot it
collapses under 1 ms and the case mostly fails. A GitHub runner sits near the
boundary, which is why it passed a dozen times and failed once.
The CI log timestamps cannot discriminate: the failing run
(job
96635813304) and a green run minutes earlier (job96635742397) both show~50 µs between adjacent
✓lines, because Node'sconsole.logto a pipe isbuffered and the whole block flushes in one batch.
The fix
Both sides are now stamped from one integer-millisecond anchor, and every
case states its own offset, so no comparison in the fixture crosses two
timestamp resolutions. This is the discipline
dist-freshness.test.tsalreadyapplies to this same library — "explicit stamps rather than sleeps … a test
that races them is a test that gets
.skipped later". This fixture was the onemember of the family that stamped only the artifact side; the other three
(
dist-freshness.test.ts,dist-freshness-adoption.test.ts,schema-tree-freshness.test.ts) already stamp both, so the class is closed.Two cases join the block:
refuses a build ONE MILLISECOND older than its sources— the reject sideat its finest grain. The pre-existing reject cases are 60 s apart, so they
cannot tell a working rule from one that has been widened into a tolerance
window.
accepts a build whose mtime EQUALS its newest source (the > boundary)—the
>-not->=boundary, previously unpinned and now sitting one millisecondfrom a case on either side.
Every freshness case now passes a
detail, so a future failure prints themtimes instead of a bare
✗— the acceptance case printed nothing at all in CI,which is most of why this needed a card to diagnose.
The reject side, asserted positively
Ablations against
bundlesAreStale, anchor counts confirmed on disk before andafter each leg (
grep -c, never an editor's exit code):A — widen the comparison into a 5-second tolerance window
(
> bundles→> bundles + 5000; anchor 1→0, injected 0→1):Only the new case catches it. Every pre-existing case stays green — which is
exactly the hole it was added to close.
B — make a stale build always look fresh (
return false; marker 0→1):All three reject cases redden; the two acceptance cases correctly stay green.
Restore leg —
git checkout -- scripts/check-regen-pending.mjs, bothinjected markers back to 0, original anchor back to 1,
git status --porcelainshowing only this PR's file, self-test
EXIT=0again.Validation
Gate union run after the final commit,
f12cee12e6:node scripts/pm/dispatch-gates.mjsderived 10 families for this path; all greenexcept one declared narrowing:
Plus
pnpm --filter @objectstack/spec typecheck(tsc --noEmit+check:scripts-typecheck, which is the program this file compiles under) andpnpm --filter @objectstack/spec build, both green.Declared narrowing:
node scripts/check-dev-prereqs.mjsreports"The workspace is not built — 1 unmet precondition, not a list of problems …
66 of 67 workspace packages declare an entry point under dist/ that is not on
disk". Only
packages/specwas built in this worktree; measuring it locallywould need a full 67-package build on a container shared with five other agents.
It names no path in this diff. CI runs it against a built tree.
Publishing
packages/spec'sfilesarray is["dist","json-schema","liveness","prompts","llms.txt","README.md","src/**/*.zod.ts","CHANGELOG.md","api-surface","spec-changes.json"]—
scripts/is not in it, so this file ships in no tarball and the diffpublishes nothing. Repo tooling that happens to live inside a published
package's directory:
skip-changesetlabel, per AGENTS.md:943 and theprecedent in #10502. Domain routing stays
domain:devx; the fix is not inpublished source.
Generated by Claude Code