Skip to content

fix(devx): refresh the ledgered packages' dep closure before --re-measure, so a stale dist stops reporting phantom drift - #8330

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-8271-remeasure-stale-dist
Aug 13, 2026
Merged

fix(devx): refresh the ledgered packages' dep closure before --re-measure, so a stale dist stops reporting phantom drift#8330
os-zhuang merged 3 commits into
mainfrom
claude/issue-8271-remeasure-stale-dist

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #8271

check:type-check-debt --re-measure asserted that every workspace dependency of a ledgered package has a built type entry point on disk, and stopped there. Presence is not currency. A dist/*.d.ts that predates its own source sails through that precondition, and tsc then resolves the ledgered package's imports through an artifact describing a package that no longer exists — so the drift the gate reports is not in the source at all.

CI never sees this because lint.yml builds the closure immediately before invoking the gate. Only the local path lacked that step, which is how #8235 came to be filed in good faith as a priority:p0 main-red stanch off four errors that were never in any source.

What changed

--re-measure now refreshes the closure instead of merely asserting it — refreshBuiltClosure() runs the same turbo command with the same filters lint.yml runs immediately before this gate, then measures. Two things are deliberately kept:

  • The nothing-is-built case keeps its existing refusal. A cold closure is minutes, and a gate that silently disappears for minutes is a gate nobody runs before pushing.
  • An mtime freshness read stays on as a backstop, run after the build and never before it, naming any package those filters do not reach.

Why refresh rather than detect-and-refuse

The card offered both directions. Three measurements chose this one:

measurement value
closure build, everything already current 9.5 s (70/70 turbo cache hits) vs. a ~250 s re-measure — under 4% to remove the variable
ONE ledgered package's 7-task closure built cold 3 m 2 s — why the cold case stays a refusal
packages an mtime read flagged as stale in the first worktree it saw 3 of 3 were false alarms — 2 dated by a *.test.ts that no dist/ is generated from, 1 by a git checkout that rewrote a file to byte-identical content

That last row is what settled it: an error rate fine for a backstop is disqualifying for a refusal, because refusing costs a 4-minute lap while the rebuild it would demand costs 9.5 seconds. turbo decides what is genuinely out of date by hashing inputs, which is strictly better than an mtime guess, and its cache replay rewrites the outputs it restores — so a refreshed closure reads as current afterwards.

Verification

The defect, reproduced. Staged deterministically: drop one export from driver-sql/src/index.ts, build that package alone, restore the source from HEAD (git hash-object confirms byte-identical, git status clean). dist/ now carries the pre-change shape while the source is current — the #8235 world.

  • Unfixed gate against that fixture: red — @objectstack/service-analytics: DEBT records 10 raw tsc error(s), now reports 11 (+1), with the source tree byte-identical to HEAD.
  • Fixed gate against the same fixture: green — 33 entries re-measured, 1969 raw errors, surplus: none.
  • Control (clean built tree, before any change): green — 33 entries, 1969 raw errors, surplus: none.

Per #8218's lesson that changing where you measure changes what you measure, the readings are equal item by item, not merely in total: surplus: none means every one of the 33 entries sat exactly at its recorded number, and it holds in both runs. The gate is also observed to have repaired the staged artifact itself.

Self-test: 28 built-closure cases (up from 15), paired the way the file's existing table is — the same graph asserted quiet, then with one flag flipped.

Mutation-tested, and it found a real hole. Seven mutations; the first pass caught six. The survivor was my own case labelled "a package that declares no type entry point is never stale" — it described the node as built: false, so the built guard alone carried it and the type-entry guard it named was never exercised. Fixed, re-run, 7 of 7 caught. A separate weakness found the same way: the source-file cases re-spelled the predicate inline instead of calling it, so they would have stayed green through a walk that had stopped excluding test files; the predicate is now named and called.

Not changed, deliberately

lint.yml step 31 is now redundant (roughly 9.5 s of cache hits before a gate that does the same thing). Removing it is a lint.yml edit pulling in seven more gate families for no measured benefit, and leaving it costs cache hits. Out of scope here.

Gates

pnpm check:type-check-debt (the wired npm script, end to end), check:type-check-coverage, check:type-source-resolution, check:nul-bytes — all green. scripts/pm/dispatch-gates.mjs re-derived against the actual changed path returns exactly the three named families; no delta.

Releases nothing — a repo gate script, no published package touched. Takes skip-changeset.


Generated by Claude Code

claude added 3 commits August 13, 2026 05:33
…sure

`check:type-check-debt --re-measure` asserted that every workspace
dependency of a ledgered package has a built type entry point, and stopped
there. Presence is not currency: a `dist/*.d.ts` that predates its own
source sails through the precondition, and tsc then resolves the ledgered
package's imports through an artifact describing a package that no longer
exists. The drift it reports is not in the source at all.

That is not hypothetical -- #8235 was filed in good faith as a
`priority:p0` main-red stanch off four such errors, while CI's own
re-measure was green on two consecutive runs. CI never sees it because
lint.yml builds the closure immediately before invoking the gate; only the
local path lacked that step.

So run it: `refreshBuiltClosure` invokes the same turbo command with the
same filters lint.yml uses, then measures. The nothing-is-built case keeps
its existing refusal -- a cold closure is minutes and a gate that vanishes
for minutes is one nobody runs before pushing -- and an mtime freshness
read stays on as a backstop over what those filters do not reach.

Fixes #8271

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNfj35pXRxnF1D3gZN5zjc
…py of it

The self-test's source-file cases re-spelled `SOURCE_FILE.test(name) &&
!TEST_FILE.test(name)` inline, so they pinned the two constants and nothing
about how `newestSourceMtime` combines them -- an assertion that would stay
green through a walk that had stopped excluding test files. Name the
predicate and call it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNfj35pXRxnF1D3gZN5zjc
…ames

Mutation testing found this case green against a staleClosure with its
type-entry guard deleted: the node described `built: false`, so the built
guard alone carried it and the label was describing an assertion that was
not being made. Describe it as built -- a shape the fs read cannot produce,
which is the point, since the pure function's contract is total.

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

vercel Bot commented Aug 13, 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)
objectstack Ignored Ignored Aug 13, 2026 5:45am

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants