fix(devx): the ablation preflight reads the WHOLE tree, not only dist/ - #13288
Merged
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw
This was referenced Aug 29, 2026
os-elon
marked this pull request as ready for review
August 30, 2026 06:15
os-elon
enabled auto-merge
August 30, 2026 06:15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #13265
scripts/ablation-dist-preflight.mjsproved an ablation reacheddist/. It said nothing about the working tree — so on a package whose build writes CHECKED-IN artifacts, an ablation is a two-file mutation and the restore leg puts back only one of them. This adds the whole-tree half to the instrument that already runs at exactly that moment.The defect, reproduced end to end on this branch
One run,
packages/spec, plant ablation (osAblationLeakProbedeclared onFieldSchema), each leg confirmed on disk by blob hash and marker count before any reading was taken:0 -> 1; blobd6c5b1a6 -> 34c45842packages/spec/authorable-surface/data.jsonwith+ "data/Field:osAblationLeakProbe"git diff HEAD -- THE_MUTATED_PATH0 lines ·git hash-object== HEAD ·grep -c marker0 · whole-treegit status --porcelain1 dirty pathexit 1—1 authorable key(s) disappeared from the contract+ the ADR-0049 enforce-or-remove prescription, i.e. the false red, about a key that never existedStep 4 is the whole finding: all three per-path proofs the ablation discipline names pass on a tree that is not restored. The silence is directional, so a stricter per-path proof cannot close it — only a whole-tree read can.
What changed
readTreeStatus+markerPresence(impure) feedclassifyTree/treeVerdict(pure, self-tested), and both modes now print atree:verdict beside the existingdist:one.Which leg you are on is DERIVED from the marker, not declared. A dirty tree is correct on a mutate leg and wrong on a restore leg, and the two share a command line (
--absentis both the delete-ablation mutate leg and the plant-ablation restore leg). Per dirty path, against HEAD:On a MUTATE leg every dirty path is listed and nothing is fatal (the build was supposed to write them; this is the earliest moment the restore leg's true size is knowable — it is two files, and here they are). On a RESTORE leg the tree must be clean and every dirty path is fatal and named.
No CLI change, deliberately. Both documented spellings keep their meaning, so the three documents that state this script's invocation stay accurate and untouched — two of them (
.claude/agents/os-dev.md,.claude/skills/dogfood-verification/SKILL.md) are governed surfaces no code PR may edit. The fullest statement of the new half lives in the script's own header, which all three already point at.Whole-tree, not a path list.
packages/specis the instance (gen:schemawritesauthorable-surface/,json-schema.manifest/,api-surface/), not the class. Measured blast radius: of every workspacebuildscript, two invoke a generator —@objectstack/specand@objectstack/docs, the latter delegating to spec'sgen:schema/gen:docs, which writescontent/docs/references/. A fix enumerating known-dirty paths would leave every other generated-artifact package with the same hole; agit status --porcelainread has no list to fall out of date. Untracked paths count too: a sharded artifact gains FILES, andgit add -Atakes them.The differential, measured on the same tree
At the restore leg of the run above, both scripts on the same unrestored tree:
HEAD~) —marker still present in 26 built files ... Rebuild the package, exit 1. True, and it sends the agent into a rebuild loop against a build that is refusing for a different reason.dist:line, plustree: restore leg ... 1 path still differs from HEADnamingpackages/spec/authorable-surface/data.json, exit 1.Following the new refusal's prescription (
git checkout HEAD -- packages/spec/authorable-surface/data.json) took the tree to 0 dirty paths, the rebuild to exit 0, and the preflight todist: green+tree: green. Both verdicts are computed and printed before either exits, deliberately: in the measured incident both were true at once, and reporting only the first is what hides the second.Local checks — union re-run on the final commit
518eb917dfnode scripts/ablation-dist-preflight.mjs --self-test— exit 0,all cases pass(36 cases; 13 pre-existing, 23 new). The new ones include a real-git leg that replays this incident: it asserts the per-path diff is EMPTY on the unrestored tree, that the restore leg reds, and that the red NAMES the leaked baseline.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(12 families, derived from the merge base, not hand-listed) —check:agent-test-spelling,check:bash32-floor,check:cli-command-ids,check:cross-package-test-inputs,check:entry-guard,check:parse-guard,check:pnpm-filter-targets,check:watch-hint-literal,check-ci-filter-parity.mjs,check-shard-attestation.mjs— all exit 0, each quoting its own verdict line.pnpm check:nul-bytes— exit 0,75 assertionsself-test plusscanned 7359 text file(s) ... no raw ASCII control bytes.node scripts/check-test-completeness.mjs— exit 3,PREREQUISITE NOT MET: it grades a saved turbo test log and none was named. Recorded as NOT MEASURED, which its own text prescribes; not a red.eslint --no-inline-config --format jsonover the one changed file — 1 file linted, 0 errors, 0 warnings, exit 0. The narrowing is a measurement, not a skip: this repo runs oneeslint.config.mjswhich never enables type-aware linting for any file (noparserOptions.project, no typed rules — its own comment at line 328 states this, and a grep confirms onlyecmaVersion/sourceTypeparser options), so no untouched file's verdict can depend on this diff, and the diff adds no config, no inline disable and no new file. The repo-wide sweep remains CI's.Declarations
scripts/ships in no package; nothing underskills/orpackages/is touched.skip-changeset: this PR releases nothing.What it still does not cover
An agent that runs no preflight at all on the restore leg. The mechanism is reachable today by the documented procedure (
.claude/agents/os-dev.mdalready prescribes--absenton every restore leg), so no prose change is required for it to fire — but a belt-and-braces sentence in that file would be a governed-surface card, not this one.Generated by Claude Code