Commit d0b8ec2
feat(scripts): an anchor-verifying ablation replacement helper, and the census that shaped it (#18598)
Fixes #18226
Clause-②: no
`sed -i` / `perl -i` have failed silently twice in this repository, in
two
different modes, and neither was caught by an exit code. This adds
`scripts/ablation-replace.mjs`: an ablation mutation goes through an
anchor
that must HIT, and the tool produces its own on-disk evidence instead of
relying on the caller to remember to add some.
Delivered in the order triage set (`5707288884`) and the card's own
acceptance
criterion 1 set: **the census first**, and the helper's shape decided by
it.
---
## Deliverable 1 — the census
### Pathspec boundary, stated before the numbers
**Enumerated:** every one of the **8,766 tracked files** of
`objectstack-ai/objectstack` at
`e0d05538c0d0275728d25acfafbab259e23d0710`
(the merge base of this branch), via `git grep -nI` with no pathspec
restriction. `-I` skips binary files.
**NOT enumerated, and each for a reason that matters to the reading:**
1. **Untracked / ignored content** — `node_modules/`, `dist/`,
`.turbo/`. Not
ablation authorship surface.
2. **The sibling repositories** `../objectui`, `../cloud`. Different
trees;
`--repo` discipline forbids answering for them from here.
3. **Git history.** This checkout is shallow (`git rev-parse
--is-shallow-repository` = `true`), and the card asks about
「今天树上」 — today's tree. No windowed history question was asked, so
none was answered.
4. ⭐ **Agent transcripts — where BOTH recorded failures actually
happened.**
No tree query can reach them. This is not a gap in the method; it is the
census's main finding, below.
### The counts, and what classified them
Two probes, both over the whole tracked set:
| probe | regex | raw hits |
|---|---|---|
| A | `sed` followed by an `-i` switch cluster | **45** |
| B | `perl` followed by an `-i` switch cluster | **8** |
Every hit was then classified — markdown prose / shell or JS comment /
quoted
fixture string / actually-executed command — by stripping comments, then
stripping quoted segments, and re-testing:
| class | count | what they are |
|---|---|---|
| PROSE (markdown) | 3 | `AGENTS.md:224`, `.claude/agents/os-dev.md:257`
— the rules that name the hazard |
| COMMENT | 6 | hook headers, `scripts/check-bash32-floor.mjs:54` |
| QUOTED-FIXTURE | 42 |
`.claude/hooks/guard-main-checkout-bash.selftest.sh` — command STRINGS
fed to the guard hook as test input, never executed as `sed` |
| EXECUTED | 2 | adjudicated by hand, both negative — see below |
The two EXECUTED rows, read in context:
- `.claude/hooks/guard-main-checkout-bash.sh:571` — inside the refusal
message's `cat` heredoc. **Help text.** The classifier does not model
heredocs; this is a known false positive, named rather than hidden.
- `scripts/pm/os-verify-lock.sh:453` — a real `perl` invocation, but
`perl -MTime::HiRes -e 1`: a clock probe, no `-i`, no in-place write.
Probe
B's regex matched the `i` inside `-MTime`. **The probe OVER-selects; it
does
not under-select** — three of its eight raw hits are this same
over-match.
### ⇒ The reading
> **ZERO `-i`-family invocations are EXECUTED anywhere on the tracked
tree,
> and therefore zero are used for ablation.**
### Firing controls — because a zero without one is a dead probe, not a
reading
**Control 1 (the classifier can say EXECUTED).** The identical
classifier,
same code path, fed two synthetic rows:
```
fake/ablate.sh:12 sed -i "s/OLD/NEW/" packages/core/src/x.ts -> EXECUTED
fake/ablate.sh:13 perl -0pi -e "s/a/b/" packages/core/src/y.ts -> EXECUTED
TOTAL=2 EXECUTED=2
```
It fires. The in-tree zero is a property of the tree, not of a broken
probe.
**Control 2 (the regexes themselves reach the tree).** Probes A and B
are not
silent: 45 and 8 raw hits respectively. A regex that matched nothing at
all
would be indistinguishable from this result at the raw level, which is
exactly
why the classification pass is reported separately from the match pass.
**Control 3 (the second zero — no committed ablation script exists).**
Probing
for the ablation script shape — `trap` on `EXIT` / `INT` / `TERM` —
returns 29
hits, so the probe fires; every one of the 29 is a tempdir cleanup or a
process
kill, and **none restores a tracked source path**. Probing for the
restore leg
itself (`git checkout HEAD --` / `git restore --source=HEAD`) returns 13
hits:
2 inside `scripts/ablation-dist-preflight.mjs` (a verifier, not a
mutator), 4
in rules prose, and **7 inside test-file comments narrating an ablation
that
was run in a transcript**. The firing control for this zero is `git
checkout`
taken the same way, which hits 8+ files. ⇒ **Not one committed file in
this
repository performs an ablation mutation.**
### What the census actually decided — and it is not what the card
assumed
The card asked "how many ablations on the tree use the `-i` family". The
answer
is zero, and the reason is structural rather than fortunate:
- **212 tracked files carry an ablation narrative** (174 of them test
files).
Ablation is heavily, routinely used here.
- **Zero of them are ablation scripts.** Every one is a *record*, in a
comment,
of an ablation performed ad hoc in a dev's shell and never committed.
So the tree was never the population. Both recorded failures —
2026-08-20's NUL
byte and 2026-09-14's silent no-op — were transcript commands, and the
tree can
neither count them nor prevent the next one. Three consequences, and
each is a
shape decision rather than an opinion:
1. **The helper must be a standalone CLI a human or agent invokes from a
shell**, not a library some committed script imports. There are no
committed
scripts to import it.
2. **The "existing ones migrate when touched" half of the card's
adoption
boundary has an EMPTY migration set.** Nothing is being left
un-migrated,
and `⛔ 不许把既有消融脚本一次性全改` has nothing to forbid here.
3. **The census does NOT conclude the helper must be mandatory** — so
the
⛔ stop-and-report clause is not triggered. It points the other way: with
the
real population living outside the tree, a required gate would have
nothing
to read, and the cost would be a manual floor bought for no coverage.
---
## Deliverable 2 — the helper
`scripts/ablation-replace.mjs`, one new file, the shape the census
indicates.
```
node scripts/ablation-replace.mjs --file PATH --anchor TEXT --replacement TEXT -- CMD...
node scripts/ablation-replace.mjs --file PATH --anchor TEXT --delete -- CMD...
node scripts/ablation-replace.mjs --file PATH --anchor TEXT --replacement TEXT --hold
node scripts/ablation-replace.mjs --restore --file PATH
node scripts/ablation-replace.mjs --self-test
```
Against the card's five requirements:
1. **The anchor must hit, or it refuses and exits non-zero.** Zero hits
exits
`3` with `ANCHOR MISS`; a count other than the declared one (default
exactly
1) exits `3` with `ANCHOR AMBIGUOUS` and names `--expect N`.
2. **It produces its own on-disk evidence.** Occurrence counts of both
anchor
and replacement before and after, plus `git hash-object` before and
after,
printed on every leg — the caller does not have to remember.
3. **Restore is proven too.** Blob equal to HEAD's **and** an empty
`git diff HEAD`. An empty hash reads as FAILURE, never as a match
against
another empty. Restore is spelled `git checkout HEAD -- ABS`, never the
bare
form, which restores from the index.
4. **Restores on `EXIT` / `INT` / `TERM`** (and `HUP`, and an uncaught
exception) against an absolute path resolved once from
`git rev-parse --show-toplevel`, armed **before** the write.
5. **`--self-test`, both directions** — see the table below.
Three properties are structural rather than checks bolted on:
- **Anchor and replacement are LITERAL argv strings.** No regex, no
shell
word-splitting, no interpolation anywhere in the substitution path. That
is
the whole of the 2026-08-20 failure removed rather than detected: `$/`
is four
characters here and cannot become anything else. A replacement carrying
a
control byte is refused before any write.
- **The write is read back from DISK, not from the string just
written.** A
blob hash that did not move is `NOTHING LANDED` and a non-zero exit —
the
2026-09-14 failure, which exited 0.
- **Exit codes are a contract**: `0` ok · `1` the leg ran and its
evidence
refutes it · `2` usage · `3` anchor · `4` the restore could not be
proven.
In WRAP mode the wrapped command's own status **passes through
untouched** —
a red child is the ablation's reading, not the tool's failure — which is
why
the tool's own failure needs a code no child can mint.
### The boundary the card and triage drew, kept verbatim
⛔ **This helper is NOT a mandatory path.** No gate requires it, it is
not wired
into CI, and no existing ablation script was rewritten. New ablations
use it;
existing ones migrate when touched. Making it mandatory would be a new
required
gate — a manual floor — and that is the maintainer's call. The header
says all
of this in the file itself.
⛔ **`AGENTS.md` was not touched**, nor was anything under `.claude/`.
Option A
(a named trap in the byte-discipline clause) belongs to the skills seat.
### Scope, and what it deliberately does not do
The restore proof is **per-path**. The whole-tree half already exists
and is
better at it: `scripts/ablation-dist-preflight.mjs` reads
`git status --porcelain` over the entire tree precisely because a build
between
mutate and restore writes committed artifacts *beside* the file you
chose. The
two compose; ⛔ neither substitutes for the other, and `--hold` prints
the
preflight invocation alongside its restore command.
---
## Dogfood — the helper proving itself on a real ablation of its own
code
⭐ Both directions, from a committed state (`a3ec1cd30`, clean tree).
**Failure direction — anchor missing MUST exit non-zero and write
nothing:**
```
$ node scripts/ablation-replace.mjs --file scripts/ablation-replace.mjs \
--anchor 'NO_SUCH_ANCHOR_deadbeef' --replacement 'x' --hold
x ablation-replace: ANCHOR MISS — the anchor does not occur in the file. Refusing to run: ...
nothing was written; the file is untouched at 80b45ea
EXIT=3
blob before = 80b45ea
blob after = 80b45ea ⇒ nothing was written
```
**Landing direction — ablate this helper's own zero-hit refusal and
watch its
self-test go red:**
```
control (un-ablated): ok ablation-replace self-test: all 49 cases pass EXIT=0
$ node scripts/ablation-replace.mjs --file scripts/ablation-replace.mjs \
--anchor 'if (anchorBefore === 0) {' --replacement 'if (anchorBefore === -999) {' \
-- node scripts/ablation-replace.mjs --self-test
ablation-replace: anchor "if (anchorBefore === 0) {" x1 (before)
ablation-replace: anchor hits 1 time(s), as declared
ablation-replace: anchor x1 -> x0
ablation-replace: replace "if (anchorBefore === -999) {" x0 -> x1
ablation-replace: blob 96b8f64 -> 79dd2f798cd7863ba223db2a811d07e730617b83
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 96b8f64 -> 79dd2f798cd7
ablation-replace: running: node scripts/ablation-replace.mjs --self-test
x the refusal names the anchor miss
x ablation-replace self-test: 1 case(s) failed.
ablation-replace: command exited 1
ablation-replace: blob after restore 96b8f64
ablation-replace: blob at HEAD 96b8f64
ablation-replace: git diff HEAD empty
ablation-replace: ok restored: blob == HEAD (96b8f64) and `git diff HEAD` is empty
```
**Reported honestly: the ablation's reach was narrower than intended and
the
reading is stated as observed.** Disabling the zero-hit branch does not
make a
missing anchor pass — with `--expect 1`, a zero count then falls through
to the
`anchorBefore !== expect` branch and is still refused with the same exit
code.
So **1** case went red (the one asserting the refusal *names* the anchor
miss),
not five. That is a fact about the two refusals overlapping at `--expect
1`, and
it is the message assertion — not a bare non-zero check — that carried
the
discrimination. Worth knowing before anyone reads a single red case as a
weak
ablation.
### ⭐ The dogfood found a real defect, in the tool, before it shipped
Wrapping the helper's own `--self-test` is the obvious way to dogfood it
— and
the first attempt printed `ok ... all 42 cases pass` and `EXIT=0`,
having
performed **no ablation whatsoever**. Read off the *whole* argv, the
`--self-test` belonging to the WRAPPED command hijacked the dispatcher:
the
parent ran its own self-test instead of the mutation. **The exact false
green
this tool exists to refuse, arriving through its own front door.** The
flag is
now read only from the argv before the `--`, and a WRAP battery pins it
(with a
stand-in child, because wrapping the real self-test makes the self-test
spawn
itself unbounded — which is how the bug was found in the first place).
### A second live instance, in the same shift
Writing this file, a control-byte character class spelled with
backslash-u
escapes was **materialised into the raw bytes** by the editing tool.
`grep`
reported the new source file as binary; `check:nul-bytes` would have
refused
the push. That is the 2026-08-20 failure mode reproduced inside the tool
written to prevent it. The class is gone: the byte test is arithmetic on
code
points, with no escape left to materialise, and the header records why.
---
## `--self-test`, both directions
```
ok ablation-replace self-test: all 49 cases pass, in both directions. EXIT=0
```
| battery | cases | asserts |
|---|---|---|
| pure verdicts | 16 | counts, N-replacement, both anchor refusals,
`NOTHING LANDED`, null-hash-is-FAILURE, control-byte fatality, restore
verdict |
| argument parsing | 9 | `--` splitting, bad `--expect`, no-op
replacement, mutually exclusive flags |
| direction A — anchor missing refuses | 5 | non-zero, code `3`, names
the miss, **nothing on disk**, blob unmoved |
| direction B — anchor present lands | 8 | on disk (read back), anchor
gone, **blob moved**, both hashes printed, `--hold` prints its restore
command |
| restore is proven, never assumed | 4 | blob == HEAD, `git diff HEAD`
empty, original text back |
| WRAP holds the trap and restores | 7 | the child ran against the
MUTATED file, its status passes through, restore proven, `--self-test`
after `--` does not hijack |
Batteries carry a pinned floor and a roster floor, so a battery that
stops
running names itself rather than going quiet.
---
## Changeset — `skip-changeset`, measured with a firing control
1 parent d93400f commit d0b8ec2
1 file changed
Lines changed: 779 additions & 0 deletions
0 commit comments