Measured while executing #13137 (PR #13264), domain:cli dev seat, session session_01TvqBFLRzXdSPcbusDoED9k. Recording only — filed unassigned, no lane. ⛔ Not a defect in the ablation preflight's own job; a gap in what anything tells you the RESTORE leg has to cover.
What happened, in the order it happened
An ablation of packages/spec/src/data/field.zod.ts (declare a key on the FieldSchema shape, delete five alias-table entries), run exactly as the ablation discipline prescribes:
- mutate, prove it on disk by blob hash — OK;
pnpm --filter @objectstack/spec build, then node scripts/ablation-dist-preflight.mjs packages/spec MARKER — "marker present in 26 built files", OK;
- run the suite — reds in the predicted places, OK;
- restore the mutated source with
git checkout HEAD -- packages/spec/src/data/field.zod.ts, blob hash back to its HEAD value, git diff HEAD for that path empty — OK;
- rebuild —
ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL, exit 1, in ~5 seconds.
Why step 5 failed
The build in step 2 runs gen:schema, which rewrote the committed baseline. git diff HEAD over the whole tree, after step 4:
packages/spec/authorable-surface/data.json | 1 +
"data/Field:reference",
+ "data/Field:referenceTo",
"data/Field:referenceVia",
So on the rebuild the key is present in the baseline and gone from the Zod graph — which the surface gate correctly reads as a retirement, and refuses with the ADR-0049 / ADR-0104 enforce-or-remove prescription (tombstone, D2 conversion, major changeset). Every word of that refusal is right. It is answering a question nobody asked, about a key that never existed.
Why it is worth a card
The failure is silent in the direction that matters. After step 4 the tree is, by every check the ablation discipline names, restored:
git diff HEAD -- packages/spec/src/data/field.zod.ts — empty;
git hash-object on that path == its HEAD blob;
grep for the injected marker in that file — 0.
⇒ an agent that scopes its restore proof to the path it mutated — which is exactly what the discipline says to do, and what a trap written around one file does — concludes the tree is clean and keeps measuring. The next thing it sees is either a build refusal it did not cause, or, if it skips the rebuild, a suite still running against dist/ that still carries the ablation. In this run the restore-leg rebuild failed and ablation-dist-preflight --absent then correctly reported "marker still present in 26 built files" — but that pairing only held because the rebuild's non-zero exit was captured. A restore leg that reads only the suite's colour would have reported a false red on an already-restored tree.
⚠️ The general shape, which is not specific to this one gate: a package whose build writes committed artifacts turns an ablation into a two-file mutation, and only one of the two files is the one you chose to mutate. packages/spec is the instance because gen:schema writes authorable-surface/, json-schema.manifest/ and api-surface/; AGENTS.md documents those artifacts and their merge driver, but says nothing about ablation, and scripts/ablation-dist-preflight.mjs proves presence/absence in dist/ and deliberately says nothing about the working tree.
What would close it (⛔ not a proposal I acted on)
Any one of these; the first is the cheapest and needs no new code:
- A sentence in the ablation discipline: restore with
git checkout HEAD -- <package> (the whole package), and prove it with a WHOLE-TREE git status --porcelain, not with a per-path diff. That is what recovered this run.
- A tree-cleanliness assertion in
ablation-dist-preflight.mjs --absent — it is already the restore leg's designated instrument, and it already refuses loudly rather than passing quietly. It would have named the leaked baseline line in step 5's place, before the confusing gate refusal.
- Name the generated-artifact packages wherever the ablation rebuild requirement is stated, so "rebuild the package you mutated" reads as "and restore what that rebuild wrote".
Reproduction
Any mutation of an authorable key on a packages/spec metadata schema, followed by that package's build. The key does not have to be plausible — the baseline records whatever the Zod graph had at build time.
Measured while executing #13137 (PR #13264),
domain:clidev seat, sessionsession_01TvqBFLRzXdSPcbusDoED9k. Recording only — filed unassigned, no lane. ⛔ Not a defect in the ablation preflight's own job; a gap in what anything tells you the RESTORE leg has to cover.What happened, in the order it happened
An ablation of
packages/spec/src/data/field.zod.ts(declare a key on theFieldSchemashape, delete five alias-table entries), run exactly as the ablation discipline prescribes:pnpm --filter @objectstack/spec build, thennode scripts/ablation-dist-preflight.mjs packages/spec MARKER— "marker present in 26 built files", OK;git checkout HEAD -- packages/spec/src/data/field.zod.ts, blob hash back to its HEAD value,git diff HEADfor that path empty — OK;ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL, exit 1, in ~5 seconds.Why step 5 failed
The build in step 2 runs
gen:schema, which rewrote the committed baseline.git diff HEADover the whole tree, after step 4:"data/Field:reference", + "data/Field:referenceTo", "data/Field:referenceVia",So on the rebuild the key is present in the baseline and gone from the Zod graph — which the surface gate correctly reads as a retirement, and refuses with the ADR-0049 / ADR-0104 enforce-or-remove prescription (tombstone, D2 conversion, major changeset). Every word of that refusal is right. It is answering a question nobody asked, about a key that never existed.
Why it is worth a card
The failure is silent in the direction that matters. After step 4 the tree is, by every check the ablation discipline names, restored:
git diff HEAD -- packages/spec/src/data/field.zod.ts— empty;git hash-objecton that path == itsHEADblob;grepfor the injected marker in that file — 0.⇒ an agent that scopes its restore proof to the path it mutated — which is exactly what the discipline says to do, and what a
trapwritten around one file does — concludes the tree is clean and keeps measuring. The next thing it sees is either a build refusal it did not cause, or, if it skips the rebuild, a suite still running againstdist/that still carries the ablation. In this run the restore-leg rebuild failed andablation-dist-preflight --absentthen correctly reported "marker still present in 26 built files" — but that pairing only held because the rebuild's non-zero exit was captured. A restore leg that reads only the suite's colour would have reported a false red on an already-restored tree.packages/specis the instance becausegen:schemawritesauthorable-surface/,json-schema.manifest/andapi-surface/; AGENTS.md documents those artifacts and their merge driver, but says nothing about ablation, andscripts/ablation-dist-preflight.mjsproves presence/absence indist/and deliberately says nothing about the working tree.What would close it (⛔ not a proposal I acted on)
Any one of these; the first is the cheapest and needs no new code:
git checkout HEAD -- <package>(the whole package), and prove it with a WHOLE-TREEgit status --porcelain, not with a per-path diff. That is what recovered this run.ablation-dist-preflight.mjs --absent— it is already the restore leg's designated instrument, and it already refuses loudly rather than passing quietly. It would have named the leaked baseline line in step 5's place, before the confusing gate refusal.Reproduction
Any mutation of an authorable key on a
packages/specmetadata schema, followed by that package's build. The key does not have to be plausible — the baseline records whatever the Zod graph had at build time.