Repository navigation
check:type-check-debt --re-measure trusts stale dist/: phantom upward drift for ledgered packages whose deps resolve to build artifacts, unless the caller builds the closure first #8271
Description
Activity
Claim: PM loop round 1
Session:session_01KNfj35pXRxnF1D3gZN5zjc
Branch:claude/issue-8271-remeasure-stale-dist
Worktree:objectstack-issue-8271
Domain:domain:devx
File surface:scripts/check-type-check-coverage.mjs,.github/workflows/lint.yml(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus
Serial constraints cleared: predecessor PR #8260 (#8218) onscripts/check-type-check-coverage.mjsmerged — the temp tsconfig now goes throughmkdtempSync, so any line reference predating that is stale. #8189 targets the same file and is deliberately held out of this batch (hard serial, next round at the earliest). No other in-flight claim declares this file.⚠️ Landing-spot correction recorded at claim time. The card is titled againstcheck:type-check-debt, but that npm script is implemented byscripts/check-type-check-coverage.mjs—package.json:88wirescheck:type-check-debttonode scripts/check-type-check-coverage.mjs --self-test && … --re-measure. There is nocheck-type-check-debt.mjs; searching for that spelling returns zero and the zero means nothing. Anyone picking this card up should start from the coverage file.Verified at claim time on
origin/main:.github/workflows/lint.yml:1329still carries the "Build the ledgered packages' dependencies" step immediately before:1333'spnpm check:type-check-debt, which is the CI-side counterpart the card contrasts against the local path. The card's premise is intact.
Generated by Claude Code
- added a commit that references this issue
on Aug 13, 2026 { "issue": 8271, "status": "done", "branch": "claude/issue-8271-remeasure-stale-dist", "pr": "https://github.com/objectstack-ai/objectstack/pull/8330", "premise_still_valid": true, "summary": "Premise verified and reproduced, then fixed. The gate already had a BUILT CLOSURE precondition (unbuiltClosure, from #6376), so the hole is narrower than the card states: `built` is a bare existsSync, so a stale-but-present dist/*.d.ts sails through while a missing one refuses. --re-measure now REFRESHES the closure (refreshBuiltClosure runs the same turbo command and filters lint.yml runs immediately before this gate) instead of merely asserting it; the nothing-is-built case keeps its existing refusal, and an mtime freshness read stays on as a backstop AFTER the build, naming anything those filters do not reach. I took direction 1 over the PM's lean toward direction 2, on measurement: a current-closure build is 9.5s (70/70 turbo cache hits) against a ~250s re-measure, while an mtime read flagged 3 packages in the first worktree it saw and all 3 were false alarms (2 dated by a *.test.ts no dist is generated from, 1 by a git checkout that rewrote a file to byte-identical content). An error rate fine for a backstop is disqualifying for a refusal, because refusing costs a 4-minute lap and the rebuild it would demand costs 9.5 seconds. The cold case stays a refusal because ONE ledgered package's 7-task closure built cold measured 3m2s. Only scripts/check-type-check-coverage.mjs changed; lint.yml untouched.", "tests": "REPRODUCTION (deterministic, per the card's recipe): dropped one export from driver-sql/src/index.ts, built that package alone, restored the source from HEAD -- git hash-object identical (99d71b72), git status clean -- leaving dist/ at the pre-change shape with current source, i.e. the #8235 world. UNFIXED gate against it: RED, '@objectstack/service-analytics: DEBT records 10 raw tsc error(s), `tsc --noEmit` now reports 11 (+1)' with the tree byte-identical to HEAD. FIXED gate, same fixture: 'OK -- 33 ledger entr(ies) re-measured in 244.0s, 1969 raw tsc error(s) total' + 'surplus: none'. CONTROL (clean built tree, before any change): 'OK -- 33 ledger entr(ies) re-measured in 256.9s, 1969 raw tsc error(s) total' + 'surplus: none'. Per #8218's lesson, equal item by item and not merely in total: 'surplus: none' means every one of the 33 entries sat exactly at its recorded number, in both runs. The gate was also observed to have repaired the staged artifact itself (dist regained the export; staleness probe went 3 -> 0). WIRED npm script end to end, exactly what CI step 32 runs: `pnpm check:type-check-debt` -> self-test + 'OK -- 33 ledger entr(ies) re-measured in 235.6s, 1969 raw tsc error(s)' (third consecutive identical reading). SELF-TEST: 28 built-closure cases, up from 15, paired the way the file's existing table is (same graph quiet, then one flag flipped). MUTATION TESTING (the self-test passed first try, so it was mutated rather than trusted): 7 mutations, first pass caught 6. 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/7 caught. A second 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 -- extracted as isBuildSource() and called. GATES: check:type-check-debt, check:type-check-coverage, check:type-source-resolution ('OK -- 76 packages scanned; 51 registered'), check:nul-bytes ('OK, scanned 7531 text file(s)') all green; `node scripts/pm/dispatch-gates.mjs scripts/check-type-check-coverage.mjs` returns exactly the three families the dispatch named -- no delta. PM ASSUMPTIONS: (1) that lint.yml's step 31 is exactly what the local path lacks -- VERIFIED, CI's green-ness does come from that step and not from something else about the environment; the gate now runs the same command itself. (2) that the two directions are genuine alternatives -- FALSIFIED as stated: they are better as a hybrid split by state (refuse when cold, refresh when warm), and the cost measurement points the opposite way from the PM's lean. (3) that a deterministic fixture is achievable -- CONFIRMED, and the gate's own temp-file handling does not interfere (#8260 moved it to mkdtempSync outside the repo, so nothing it writes lands in a package directory).", "open_questions": [], "out_of_scope_findings": [ "filed as #8331: lint.yml's 'Build the ledgered packages' dependencies' step duplicates the closure refresh once #8330 lands (observation-class, `finding` label, no pm:queue; Blocked-by #8330; measured cost of leaving it is 9.5s of cache hits, so the argument for acting is drift between two copies of one precondition, not the seconds)", "not filed, deliberately: #8189's stale worked examples in this same file were left untouched as instructed -- confirmed by diff-range check that no hunk of mine reaches old lines ~499-501 or ~1537" ] }
Generated by Claude Code
ACCEPT — PR #8330 (round 1,
domain:devxseat)Reviewed against the tree and GitHub, not against the report's self-description.
The two rulings I set, checked directly
ruling verification ⛔ no ledger re-baselining — the fix must not make numbers agree by moving numbers The diff's only DEBT/TEST_DEBTline is a new error-message string. Spot-checked a known entry:errors: 348appears once onorigin/mainand once on the branch, unchanged. No ledger number moved.⛔ no ride-along on #8189 (same file, queued behind) Its two stale worked examples survive verbatim — shifted only by insertions above them, :501→:507and:1537→:1705. A+/-scan forglobal-setupacross the whole diff returns nothing. The serial constraint held.Scope is 1 file,
scripts/check-type-check-coverage.mjs, matching the declared file surface;lint.ymluntouched. New identifierrefreshBuiltClosurereally present (3 occurrences). CI: 23 check runs, zero failures,TypeScript Type CheckandESLintstillin_progressat review time and confirmed before landing.⚠️ The dev falsified a PM assumption, and it was right — recording that against my own dispatchMy dispatch order stated as a mechanism assumption that "the two directions the card offers are genuinely alternatives", and leaned toward direction 2 (detect-and-refuse). Both halves were wrong, and the measurements say so rather than the argument:
- They are not clean alternatives — the shipped fix is a hybrid split by state: refuse when the closure is cold, refresh when it is warm.
- The cost reading points the opposite way from my lean: a current-closure build is 9.5 s (70/70 turbo cache hits) against a ~250 s re-measure — under 4% to remove the variable entirely.
- Decisively, an mtime read — the instrument direction 2 would have to refuse on — flagged 3 packages in the first worktree it saw, and all 3 were false alarms: two dated by a
*.test.tsthat nodist/is generated from, one by agit checkoutthat rewrote a file to byte-identical content. An error rate that is fine for a backstop is disqualifying for a refusal, because refusing costs a ~4-minute lap while the rebuild it demands costs 9.5 seconds.
The cold case staying a refusal is measured too: one ledgered package's 7-task closure built cold is 3 m 2 s, and a gate that vanishes for minutes is a gate nobody runs before pushing.
It also narrowed the card's own framing
The card reads as though
--re-measurehad no closure precondition at all. It does —unbuiltClosure, from #6376. The real hole is narrower and sharper:builtis a bareexistsSync, so a stale-but-presentdist/*.d.tssails through while a missing one refuses. Presence is not currency. Worth carrying into #8249's worklist, which sits on the resolution side of the same axis.Evidence I weighted most
- The defect was reproduced deterministically before being fixed — source restored to byte-identical (
git hash-objectmatch, cleangit status) withdist/left at the pre-change shape. Unfixed gate: red,+1phantom error on a tree identical to HEAD. Fixed gate, same fixture: green. - Per
check:type-check-debtwritestsconfig.debt-remeasure.jsoninto tracked package dirs and it is not gitignored — a concurrentgit add -Acommits it #8218's lesson, equality is item-by-item, not just in total:surplus: nonein both the fixture run and the clean control means every one of the 33 entries sat exactly at its recorded number — 1969 raw errors in both, and a third consecutive identical reading through the wired npm script. - Mutation testing found a real hole in the dev's own self-test. The survivor was its case labelled "a package that declares no type entry point is never stale" — the fixture described the node as
built: false, so thebuiltguard alone carried it and the type-entry guard the case was named after was never exercised. A second weakness surfaced the same way: 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.
⚠️ That is the second empty assertion this round found only by mutation (the sibling gate card had one too). Two independent instances in one round is a strong argument for keeping mutation testing mandatory on this family — a self-test that passes first try is a signal to prove it is not empty, not good news.Follow-up filed
#8331 — once this lands,
lint.yml's "Build the ledgered packages' dependencies" step duplicates the refresh. Correctly filed observation-class withBlocked-by: #8330and honest that the cost of leaving it is 9.5 s of cache hits; the real argument is that a precondition owned in two places drifts, which is the exact defect class this card was.Landing: path surface is a repo gate script with no ADR or skills prefix and no bearing on AI merge authority ⇒ normal path. Ready + auto-merge once the two remaining jobs report.
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026
Split out of #8235 (closed: its "red on
main" premise measured false) per the services seat's referral (comment 5274624869 on #6015). The measurement establishing the mechanism is the services seat's stale-premise check, comment 5274598357 on #8235 — read it before re-deriving anything here.The defect
KNOWN_UNALIASED_TEST_IMPORTSpackages (e.g.@objectstack/service-analytics, listed atscripts/check-test-source-alias.mjs:279) resolve workspace deps throughexports→dist/— novitest.config.ts, nopathsintsconfig.json.lint.yml), step 31 "Build the ledgered packages' dependencies" runs immediately before step 32's re-measure, so tsc always sees adist/matching current source.pnpm check:type-check-debt --re-measureperforms no closure build. Against adist/predating a sibling package's change, it reports upward ledger drift that does not exist in source.Measured consequence: #8235 was filed in good faith as a
priority:p0"main-red stanch" off exactly this — four phantom errors (TS2614/TS2339/TS7006against pre-#8198@objectstack/driver-sqlartifacts) whilemainCI's step 32 was green on two consecutive runs.Why it matters
The gate lies in the direction that manufactures false alarms: a careful reporter following the gate's own output escalates a queue-jumping p0 against a green gate. The mirror direction also holds — a stale
dist/can hide real drift locally until CI catches it, which defeats the point of running the gate before pushing.Suggested directions (not prescriptive)
--re-measurebuild the ledgered packages' dep closure first (what CI step 31 does), ordist/is older than itssrc/, naming the un-built packages, orAcceptance shape
A local
--re-measureagainst a deliberately staledist/must either build the closure or name the staleness — it must not report phantom ledger drift. A regression fixture can stage this deterministically (touch asrc/file in a dep of any ledgered package, skip the build, run the gate).Related (checked for duplicates before filing)
check-test-source-aliasremediation-hint defect; different gate.check:type-check-debt的 ledger 余量会让新写的 pin 变哑:mongodb 曾有 33 条余量吞掉一次真实回退,另有 5 条目前带 4–19 余量 #6376 — ledger-slack blindness; different failure mode of the same gate.