fix(devx): check-regen-pending refuses on an unmet prerequisite instead of reading a crash as a stale artifact (#15722) - #15785
Conversation
…ad of calling the artifact stale `check-regen-pending` does not regenerate and does not compare bytes: it spawns each deferred artifact's `check:` gate and reads the exit status. Every reason a gate has for exiting non-zero therefore arrived wearing the same clothes, and all of them were printed as one word — `stale` — over three lines of whatever the child said. On a checkout with no `node_modules` that is a verdict about a file nothing read. Grade the failure instead. A gate that could not LOAD is reported in #11557's landed frame — `reportPrerequisiteNotMet`, `EXIT_PREREQUISITE_NOT_MET`, both imported from `scripts/import-prerequisite.mjs`, never restated — and the run exits 3. Three shapes, all measured live on an uninstalled tree across the 14 registered paths: the runner missing (`sh: 1: tsx: not found`, 10), a raw `ERR_MODULE_NOT_FOUND` link failure (1), and a child that already refused with `PREREQUISITE NOT MET` and exited 3 (1) — the landed fix undone one process boundary out. Nothing is loosened. The refusal is still a refusal, the marker still keeps every deferred path, and a gate that RAN and failed still reads `stale` and exits 1. The prerequisite branch sits BELOW `defer`, so a merge commit that cannot run its gates still defers rather than being refused — putting that back would restore the #8047 defect whose learned workaround was skipping the whole hook. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…em dash The shared frame already spells `PREREQUISITE NOT MET — <headline>`, so a second em dash inside the headline read as one sentence broken twice. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…ng them The shared frame inlines the `measures` clause into a sentence it does not wrap, so a fourteen-path list there buries the clause that carries the reading. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Two of the new fixture assertions matched a string the raw child output carries either way, so they passed against the pre-fix code — measured under ablation A1, where their parent cases went red and they stayed green. They now assert the text only the refusal produces. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…escribes check-regen-pending.mjs now exports eight functions, not seven; the count is prose in check-declaration-mirrors.mjs, not a pin, and the gate stays green either way — but a stale number there is a lie this change introduced. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
|
ACCEPT — PR #15785 (head The narrowing measurement came first and half 2 is WITHDRAWN on it: the pending set is not a diff-derived predicate at all — it is read from Half 1 as ruled: a spawned Measured by this seat: branch blob at Dev-filed #15783 ( Flip + arm when both required jobs report success on Generated by Claude Code |
|
Flipped ready + auto-merge enabled (08:15Z, Generated by Claude Code |
Fixes #15722
Two halves were filed. The narrowing measurement was taken first, and it withdrew half 2. Half 1 is fixed; half 2 is pinned as a fixture so the reading that withdrew it cannot be re-derived.
1. The narrowing measurement — half 2 does NOT reproduce, with deps or without
The card read the refusal as a predicate over "every
merge=os-regenpath that changed between the previous and the newHEAD". It is not a predicate over paths at all. The hook is three lines:and the pending set is read from a FILE:
The only writer of a path line is
markPending()inscripts/git-merge-regen.mjs— the merge driver — which git invokes only when it actually attempts a text merge on a path. A fast-forward invokes no driver.Reproduced exactly as the card prescribes: branch at
e13ede817^(036b4cac2),git pull --ff-only origin mainacrosse13ede817(#15689, which changedcontent/docs/permissions/system-context.mdx), then the check run the way the hook runs it.Half 2 is WITHDRAWN. What the reporting seat hit was a marker left by an EARLIER real merge in that worktree, which half 1 made impossible to clear: the marker is removed only when every gate passes, the gate can never pass without
node_modules, so the marker was stuck and every later push in that worktree met it — fast-forward or not. The two halves are one causal chain, and fixing half 1 unsticks it.The four readings that separate the marker from the fast-forward (same commit, same tree):
stale+ERR_MODULE_NOT_FOUNDstackcurrent, marker cleared2. Half 1 — the population is 13 gates, not one
The card names one artifact. Measured on an uninstalled tree with all 14 registered paths in the marker, 12 of the 14 were reported
staleover three distinct failures, none of which is a verdict about a file:sh: 1: tsx: not found)staleERR_MODULE_NOT_FOUND)stalePREREQUISITE NOT MET, exit 3stalereadsDist/readsSchemaTreerefusalsThat third row is the landed #11557 fix undone one process boundary out:
check:platform-object-tenancy-censussaysPREREQUISITE NOT METin so many words and its caller relabelled itstale.Before → after, on the card's own artifact
Across the 14 paths:
— stalelines 12 → 0,NOT MEASUREDlines 0 → 12.No new spelling. The frame, the wording and the exit code are #11824's, imported:
reportPrerequisiteNotMet,EXIT_PREREQUISITE_NOT_MET,INSTALL_FIX, andclassifyImportFailurefor the four-way diagnosis (uninstalled / unbuilt workspace package / broken install / a dependency of a whole package). Only thedetailis written here, which is the half #11824's header reserves for the gate.Nothing is loosened.
.githooks/pre-pushmaps it to 1, so the push is still refused. The hook needed no change.reportPrerequisiteNotMetexits before thermSyncat the bottom ofmain()is reachable. Pinned.staleand exits 1. Pinned as the control.defer, so a merge commit that cannot run its gates still defers rather than being refused — putting that back would restore the [finding] Theos-regenpre-commit hook and the documented four-step landing procedure give opposite instructions — one of them has to change #8047 defect whose learned workaround was skipping the whole hook.3. Self-test: 23 → 36 cases, exit 0 both
node scripts/check-regen-pending.mjs --self-test; echo EXIT=$?— before 23 cases, EXIT=0 (run atb25a5fc32); after 36 cases, EXIT=0. Thirteen new, infixtureSelfTest(the battery roster and its floor of 3 are unchanged — the unit is the callee, and no callee was added):stale, "Nothing was measured", the package named, the marker still holding the debt.--ff-onlyacross a changedmerge=os-regenartifact is a fast-forward, writes no marker, and the push is accepted.The two prerequisite stubs are REAL failures, not printed imitations:
node --input-type=module -e "import 'os-regen-fixture-absent-pkg';"makes node fail to link, andos-regen-fixture-absent-runner --checkhands the shell a command that is not on PATH. A fixture that echoed the text would pass against an implementation that matched on text alone.4. Ablation — per half, trap-guarded, blob-hash restore
Both legs run against the COMMITTED implementation. No build or
dist/is involved: the fixture spawnsfileURLToPath(import.meta.url)— the very file being mutated — soablation-dist-preflighthas no artifact to assert about and the on-disk proof is the blob hash plus an anchoredgrep -con both the deleted and the injected text.A1 — remove the grading (
if (unmeasured.length)→if (false), andgateCouldNotRun's result discarded):The
stalecontrol and the three fast-forward cases stay GREEN under A1 — they must, since A1 is exactly the pre-fix behaviour.the gate's runner ... is not installed,refused to measure — ...), and the second A1 run reds them. The first reading is reported here rather than replaced.A2 — re-derive the pending set the way the withdrawn half 2 proposed (
pendingfilled fromgit diff --name-only HEAD@{1} HEADfiltered throughentryForPathwhen the marker is empty):So the half-2 pin is non-vacuous against the exact "fix" the card proposed.
5. Verification
node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack; echo EXIT=$?→ EXIT=0, 31 commands. The first derivation warned STALE TREE (2 family-defining files moved onorigin/main), so it was re-derived from a tree ATorigin/main(2dec9576d) with the same one path: command list identical.check:docs(nopackages/spec/json-schema) andcheck:generated(check:api-surface, nodist) both exit 1 on an unbuilt tree; afterpnpm --filter @objectstack/spec buildthroughscripts/pm/os-verify-lock.sh(VERDICT command-exit 0 · held the lock 180s · waited 81s) both exit 0, with the working tree still clean afterwards.pnpm check:nul-bytes— green (in the 31). Plus a direct scan of the diff:grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'on both changed files, no match.pnpm check:merge-driver— green;node scripts/git-merge-regen.mjs --self-testrun explicitly too: green, "merge driver wiring is consistent (24 path(s) deliberately excluded)".node scripts/pm/check-governed-merges.mjs --test scripts/check-regen-pending.mjs .githooks/pre-push→ NOT governed (0 of 2 paths hit the register), re-run on this final file list.pnpm lint(repo-wide,eslint . --no-inline-config) — green in full, 108s. No narrowing declared.TURBO_SCM_BASE=b25a5fc32 pnpm exec turbo ls --affected→ 0 packages; the diff is rootscripts/only. The two tests that import this module were still run:pnpm --filter @objectstack/spec exec vitest run scripts/dist-freshness.test.ts scripts/schema-tree-freshness.test.ts— 2 files, 21 tests, green.check:scripts-typecheckgreen.node scripts/check-declaration-mirrors.mjsand its--self-test— green. The partial mirror inscripts/check-regen-pending.d.mtsis deliberate and unchanged; only the export count in that gate's prose (seven → eight) follows the module.NOT MEASURED, by name.
dispatch-gatesprints three families whose argv takes a value only a CI run supplies, and they are not runnable here:scripts/check-cross-package-test-inputs.mjs --union-into "$RUNNER_TEMP/turbo-ls.json" --changed "$RUNNER_TEMP/changed-files.txt"scripts/check-shard-attestation.mjs --emit --job test --shard (matrix.shard) --total 6 --out "$RUNNER_TEMP/shard-attestation"scripts/check-test-completeness.mjs "$RUNNER_TEMP/test-core.log" --scheduled "$RUNNER_TEMP/shard-packages.txt" --package-list "$RUNNER_TEMP/turbo-ls.json"Also not a clearance, quoted from the tool: 10 families declare a population too wide to place, and 38 artifact rosters score
silentfor every card in the tree.6. Changeset
skip-changeset. AGENTS.md: "that label is for a diff that publishes nothing from any released package." Both changed files are rootscripts/tooling; no package publishes them.Out of scope, filed
#15783 —
check:system-context-censusitself still dies with a rawERR_MODULE_NOT_FOUNDstack when run directly (a bare top-levelimport ts from 'typescript'inscripts/isystem-census.mjs, a site PR #11824 did not convert). This PR grades that failure at the CALLER; the gate's own reading is the other half and is not touched here.Also deliberately NOT changed: this file's
readsDist/readsSchemaTreebranches keep their exit code, and the summary line still counts them among "the N stale artifact(s)". They already print what is unmet and what clears it, and the card does not name them.🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Generated by Claude Code