Skip to content

feat(scripts): an anchor-verifying ablation replacement helper, and the census that shaped it - #18598

Merged
os-try-charles merged 2 commits into
mainfrom
claude/issue-18226-ablation-replace-helper
Sep 17, 2026
Merged

os-try-charles merged 2 commits into
mainfrom
claude/issue-18226-ablation-replace-helper

Conversation

@os-try-charles

Copy link
Copy Markdown
Collaborator

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 80b45eadc4f9f995e6d5530cb1d6c7aa8f09d5c7
EXIT=3
blob before = 80b45eadc4f9f995e6d5530cb1d6c7aa8f09d5c7
blob after  = 80b45eadc4f9f995e6d5530cb1d6c7aa8f09d5c7    ⇒ 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     96b8f64283f8f18730eb26d4a3f145f0dfabd1c8 -> 79dd2f798cd7863ba223db2a811d07e730617b83
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 96b8f64283f8 -> 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  96b8f64283f8f18730eb26d4a3f145f0dfabd1c8
ablation-replace:   blob at HEAD        96b8f64283f8f18730eb26d4a3f145f0dfabd1c8
ablation-replace:   git diff HEAD       empty
ablation-replace: ok restored: blob == HEAD (96b8f64283f8) 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

⚠️ Not the "scripts are always exempt" reflex. The criterion is whether anything
published moves — each manifest's files[] actual shipped content — and the
seat named packages/lint shipping CHANGELOG.md precisely so this could not
rest on a reflex.

A reachability predicate was built over the 70 publishing manifests that
declare files[] (zero of which declare an entry escaping its own package
directory, so no files[] can reach repo-root scripts/), then run with
controls:

THE DIFF                              scripts/ablation-replace.mjs  -> NOT PUBLISHED
FIRING CONTROL A (the named trap)     packages/lint/CHANGELOG.md    -> @objectstack/lint files[]: CHANGELOG.md
FIRING CONTROL B                      packages/spec/dist/index.js   -> @objectstack/spec files[]: dist
FIRING CONTROL C (negative)           eslint.config.mjs             -> NOT PUBLISHED

The predicate fires on both positives, including the packages/lint trap.
Repo-root scripts/ is outside every pnpm-workspace.yaml glob. ⇒ nothing
published moves ⇒ skip-changeset.


Clause ②

Clause-②: no, re-judged from the actual diff rather than carried from the
dispatch: one new file under scripts/, no schema, no export, no accepted set
widened anywhere. The diff agrees with the declaration.


Acceptance notes

  • The helper ships with no pointer to it from the ablation procedure. The
    three documents that route agents to ablation-dist-preflight.mjs are
    .claude/agents/os-dev.md, .claude/skills/dogfood-verification/SKILL.md
    (both governed surfaces) and packages/qa/dogfood/README.md — and this
    seat's dispatched file surface is scripts/. Adding the pointer is a
    separate, differently-owned edit. Noted, not filed: an adoption-style tool
    nobody is routed to is adopted by whoever greps scripts/, and the two
    governed files are the ones that would fix that.
  • Noted, not filed: .claude/hooks/guard-main-checkout-bash.sh writes its
    refusal help text through a heredoc, which a comment-and-quote classifier
    reads as an executed command. Carrier: no PR touches it here; it cost one
    hand adjudication in this census and nothing else.

Gates

Derived, not guessed, and reconciled rather than recalled:

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack
  ⇒ derived from objectstack-ai/objectstack at a3ec1cd30; --repo checked against this
    checkout's origin remote — it holds
  ⇒ change set: 1 path vs merge base e0d05538c (three-dot), scripts/ablation-replace.mjs
  ⇒ 28 commands

All 28 were run to a captured exit code (redirect first, then $? — never
across a pipe), and the ran-list was reconciled back with --ran. The complete
per-command table, including anything the run could not measure, is in the
os-dev-report comment on #18226 — it is the authority, and a family the run
could not reach is recorded there as NOT MEASURED with its reason rather than
as a pass.

Families dispatch-gates itself declares outside that set and this run does
not claim to have covered: the 53 artifact-roster families (their silent
verdict is a fact about a list, never a clearance), the 11 wide-population
families, the 1 path-scheduled CI job, and the 14 changeset-derived families —
which do not apply, since the measured decision above is that this diff
publishes nothing.

Repo-wide scans (pnpm lint and the full check:* farm) are CI's run, not a
debt of this branch.


Generated by Claude Code

`sed -i` / `perl -i` have failed silently twice in this repository, in two
different modes, and neither was caught by an exit code: once a variable
interpolated into a NUL byte and turned the target binary, once the in-place
write swallowed its argument and wrote nothing at exit 0. Both were caught
only by on-disk evidence a dev remembered to add by hand.

`scripts/ablation-replace.mjs` removes the construct rather than detecting
its failures: anchor and replacement are literal argv strings with no regex,
shell or interpolation surface at all; the anchor must hit a declared number
of times or the tool refuses and exits non-zero; and the write is verified
against the disk -- occurrence counts before/after plus `git hash-object` --
so a write that did not land is the tool's own non-zero verdict. Restore is
proven the same way (blob equal to HEAD's plus an empty `git diff HEAD`, with
an empty hash read as FAILURE, never as a match) and is armed on EXIT / INT /
TERM against an absolute path resolved from `git rev-parse --show-toplevel`.
`--self-test` runs both directions: anchor missing must exit non-zero and
write nothing, anchor present must land on disk and print the evidence.

Adoption-style by design: no gate requires it, it is not wired into CI, and
no existing ablation script is rewritten. New ablations use it; existing ones
migrate when touched.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
Dogfooding the helper on its own self-test found it: read off the WHOLE argv,
a `--self-test` belonging to the WRAPPED command hijacked the dispatcher. The
parent ran its own self-test, printed a green verdict and performed no
ablation at all — the exact false green the 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 the property along with the rest of wrap mode: the child runs against the
mutated file, its own exit status passes through untouched, and the file is
restored with the proof printed once the child exits.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
@os-try-charles os-try-charles added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 17, 2026 — with Claude

Copy link
Copy Markdown
Collaborator Author

席内复核(派发席 domain:devx):条款② PASS;普查结论我自己重取了一遍

读数时点 2026-09-17T07:45Z;PR 头 a3ec1cd30,我的对照树是 origin/main @ ab1d35925(⚠️ 比施工席当时的合并基 e0d05538c 要新 —— 所以下面的原始计数与报告里的不完全相同,这是树动了,不是谁读错了)。

条款②:no 成立(PASS)

  • 改动 1 个路径:新增 scripts/ablation-replace.mjs(+779/−0),⛔ 未触 packages/spec/src/**。
  • PR 正文行首 Clause-②: no(逐字读回)。
  • node scripts/pm/check-clause2-carriers.mjs --pair 18598 退出 0:「both carriers agree … no widening tell」。
  • 已发布面:83 份 manifest 中发运 scripts/** 的 0 份;发火对照 70 份 files[] 发运 CHANGELOG.md。

普查:我用另一条正则在另一棵(更新的)树上重取,结论同向(2026-09-17T07:45Z)

树 objectstack-ai/objectstack,revision origin/main @ ab1d35925,tracked files 8766

git grep -nI  'sed -i'                 → 55 行,分布在 4 个文件:
    .claude/hooks/guard-main-checkout-bash.sh          (拒绝消息的 help 文本 + 注释)
    .claude/hooks/guard-main-checkout-bash.selftest.sh (喂给钩子的命令字符串,不执行)
    AGENTS.md                                          (散文,点名这个陷阱)
    scripts/check-bash32-floor.mjs                     (注释)
git grep -nIE 'perl [^|]*-[0-9]*p?i'   → 6 行,逐行打印,全部是散文/注释/夹具

发火对照(同一读法、同一棵树):git grep -nI 'git checkout HEAD --' → 12 行 ⇒ 读法有效,上面的零是读数

⇒ 树上没有一处在执行 -i 家族做消融 —— 这条我认。它也把卡面的隐含人口证伪了:两次被记录的失败都发生在会话记录里,不在树上。施工席把这一点写在普查的边界里而不是藏起来,是对的。

⭐ 值得记一笔的两件

  1. 狗粮吃出了真缺陷:把工具包住自己的 --self-test 做消融时,被包命令里的 --self-test 劫持了派发器,父进程跑了自己的自检并打出 all 42 cases pass / exit 0,而一次消融都没做 —— 正是这个工具存在的理由那种假绿,从它自己的前门进来。已修(只读 -- 之前的 argv)并用 7 例 WRAP 电池钉住。
  2. 卡面那个失败模式在本班又活体复现了一次:写这个文件时,一个用反斜杠-u 写的控制字符类被编辑工具物化成了原始字节,grep 直接把新文件报成 binary。⇒ 2026-08-20 那次的形态,在为防它而写的工具里重演。已改成对码点做算术,check:nul-bytes 退 0。

放行路径

绿判据 2026-09-17T07:45Z 读为 NOT GREEN(5 条 in_progress) ⇒ 等它跑完;绿后翻 ready → 重读(要求 total_count 真的移动过) → 武装。⛔ 不拿这次的读数去武装。


Generated by Claude Code

@os-try-charles
os-try-charles marked this pull request as ready for review September 17, 2026 08:32
@os-try-charles
os-try-charles added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit d0b8ec2 Sep 17, 2026
37 checks passed
@os-try-charles
os-try-charles deleted the claude/issue-18226-ablation-replace-helper branch September 17, 2026 08:54
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ipts/ablation-replace.mjs beside its sibling preflight (objectstack-ai#18725)

Fixes objectstack-ai#18599

Clause-②: no

Governed surface (`.claude/**`): draft, `skip-changeset`, an authorized
approval lands it and the `domain:skills` seat lands it from there. File
surface: `.claude/agents/os-dev.md` + `packages/qa/dogfood/README.md`
only. Head `fa55c9d0ad`, base `f6189a43f9` (`origin/main` at worktree
creation; the card's readings were taken on `ab1d35925` and re-read here
on the tip). Line-neutral on the governed file: **403 / 403**, widest
line 120 B before and after, zero lines over the ratchet's 120-byte cap.

## The defect

PR objectstack-ai#18598 (card objectstack-ai#18226) landed `scripts/ablation-replace.mjs` — the
on-disk mutation step of an ablation: replace through an anchor that
must hit, with the write and the restore verified against the disk
rather than an exit code, in place of the `sed -i` / `perl -i` family
that silently failed in two recorded ablations (2026-08-20, a `-0` run
whose `$/` interpolated into a NUL byte; 2026-09-14, an argument
swallowed and nothing written, exit 0). No instruction surface pointed
at it, while the two that route an agent to its sibling
`scripts/ablation-dist-preflight.mjs` do exist — so the helper could
only be found by grepping `scripts/`, which is the memory-dependent
shape objectstack-ai#18226 was filed against. This PR adds the pointer to exactly
those two surfaces, in the order an ablation runs the two tools: mutate
on disk through the replace helper, then prove the mutation reached
`dist/` with the preflight, then read the run. Adoption-style only: not
a mandatory path, no gate, no migration of existing ablation scripts
(objectstack-ai#18226's triage 5707288884 set that floor; the migration set measured
empty in objectstack-ai#18598's census).

## `.claude/agents/os-dev.md` — one line in, one true duplicate out

Inserted, new :254 (119 B), directly under the mutate-then-build line of
the ablation step (:253 「每一腿(变异与还原)都是:改动 → 证明它真落到了磁盘 → …」, 108 B) and
above the preflight line (old :254, now :255, 113 B):

> - 落盘走 `node scripts/ablation-replace.mjs`,不走 `-i`
家族:锚点必须命中,写入与还原按磁盘核验。

「`-i` 家族」 is the name the file's existing :258 already gives the trap
(`sed`、`perl -i`、`str.replace`、`re.sub` 零命中也 exit 0). The line is a
routing line in the register of the preflight line beside it: one
instrument, the reason in the same breath, no 「必须」, no gate.

Paid by deleting old :319 (72 B), the opening bullet of 「干净收尾」:

> - 报告落两次,GitHub 优先:卡片评论在前,终报消息在后。

What it lost: nothing the file does not state elsewhere, twice. Its
three claims — two deliveries, GitHub first, comment before terminal
message — are the header's :17 (「报告交付两次,GitHub 优先:先作 issue 评论,…」) and
:18 (「…再作为终报消息…」), and the very next bullet of the same section (now
:320, 「终报消息之前,把同一段 JSON 发成 issue 评论,…」) restates the ordering together
with the marker. The section now opens on that bullet.

Why a payment and not a fold: measured on this tree, of the file's 284
adjacent bullet pairs ZERO merge under the 120-byte cap (smallest merged
133 B), and in the ablation block :253–:264 ZERO of 11 (smallest 162 B);
the block's total slack is 249 B spread over lines holding 0–61 B each,
none of which fits a 35-byte script path plus its reason. Re-wrap
funding is refused by the ratchet rule in any case.

Whole file: 403 → 403 lines; widest line 120 B before and after;
control-byte scan (`grep -naP` over the C0/DEL range) on both files:
clean.

## `packages/qa/dogfood/README.md` — one sentence

Step 4 of 「Adding a golden test」, placed before the existing preflight
sentence (mutate, then prove `dist/`); the helper's usage placeholders
are written as words here because the platform sanitizer eats tag-shaped
fragments in a PR body — in the file they are the helper's own
spellings:

> Make the revert itself through `node scripts/ablation-replace.mjs
--file PATH --anchor TEXT --replacement TEXT -- CMD` rather than `sed
-i` / `perl -i`: the anchor must hit, the write and the restore are
verified against the disk, and a mutation that did not land exits
non-zero instead of handing you a green run.

Non-governed, no line budget, the README's English register. Net +4
lines in that paragraph.

## Verification by the card's own instrument (tree
`objectstack-ai/objectstack`)

| reading | card @ `ab1d35925` | base `f6189a43f9` | head `fa55c9d0ad` |
|---|---|---|---|
| `git grep -lI 'ablation-replace'` | 1 as reported (that sha itself
answers 0 — the helper landed in `d0b8ec2aaf` after it) | 1 (the helper
itself) | **3** — `.claude/agents/os-dev.md`,
`packages/qa/dogfood/README.md`, `scripts/ablation-replace.mjs` |
| `git grep -lI 'ablation-dist-preflight'` | 7 | 8 (the card's 7 plus
the helper's own header, which names its sibling) | 8, unchanged |
| control `git grep -cI 'dogfood'` os-dev.md / README.md | 1 / 6 | 1 / 6
| 1 / 6, unchanged |
| `git grep -cI 'ablation'` on
`.claude/skills/dogfood-verification/SKILL.md` | 0 | 0 | 0 — the third
routing document named in objectstack-ai#18598's report does not exist; not touched |

Per-file hits at head: `ablation-replace` — os-dev.md 1, README.md 1,
the helper 14; `ablation-dist-preflight` — os-dev.md 1, README.md 1.

## objectstack-ai#18226 option A — a reading, not implemented here

Option A (「在字节纪律里加一条具名陷阱」: name the `perl -0` + `$/`-interpolates-to-NUL
trap in the byte-discipline rules) was **routed, not ruled, and has not
landed**. objectstack-ai#18226's triage 5707288884 routed it to the skills seat (「选项
A(在字节纪律里加一条具名陷阱)要走 skills 席,⛔ 不在本卡内落地」), the claim 5710594253 repeated
the boundary, and PR objectstack-ai#18598 landed option C (the helper) only. On this
tree: `AGENTS.md` carries no control-character or `check:nul-bytes`
sentence at all (its only `perl -i` mention is :224, the Bash-guard
write shapes); `os-dev.md` :387–:390 (字节与 sanitizer 纪律) covers writing
control characters as escapes and the harm of a raw NUL, and :258 covers
mode ② (nothing written, exit 0) — neither names mode ① (`-0` + `$/`).
Left untouched here (this card is the pointer only) and reported in
`open_questions` for the seat to queue.

## Gates (worktree at `fa55c9d0ad`; every exit code captured by
redirect-then-`$?`, never through a pipe)

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from the worktree, no hand-fed paths (change
set vs merge base `f6189a43f`: the two files; committed 2, working tree
0, untracked 0). 53 commands; reconciled with `--ran` carrying `:: exit
N` per line: **53 derived, 48 run, 5 NOT-MEASURED, 0 UNRUN**.

Named by the dispatch:

- `pnpm check:pm-skill-ratchet` :: exit 0 — 「.claude/agents/os-dev.md is
403 lines (ceiling 403; headroom 0)」 · 「widest table row is 0 bytes (pin
0; headroom 0)」
- `pnpm check:nul-bytes` :: exit 0 — 「OK (scanned 8814 text file(s) … no
raw ASCII control bytes)」
- `pnpm check:agent-model-declared` :: exit 0 — 「1 agent definition(s)
under .claude/agents/ all declare a model」
- `pnpm check:pm-governed-merges` :: exit 0 (the post-merge audit).
`node scripts/pm/check-governed-merges.mjs --pr NUMBER` needs this PR's
number, so it runs after creation; its verdict (expected GOVERNED, exit
3, which is a verdict and not a failure) is recorded in the report
comment on the card.
- `pnpm lint` (repo-wide, `eslint . --no-inline-config`, as PR objectstack-ai#18654
ran it) :: exit 0, 100 s, on `fa55c9d0ad`

The other 43 that ran, all exit 0: `check-ci-filter-parity`,
`check-closing-keyword-parity` (+ `--self-test`),
`check-comment-mask-adoption` (+ `--self-test`),
`check-comment-mask-corpus`, `check-keyed-text-bounds` (+
`--self-test`), `check-platform-object-tenancy-census` (+
`--self-test`), `check-plugin-teardown-shape` (+ `--self-test`),
`check-registry-log-declared` (+ `--self-test`),
`check-rest-log-spy-declared` (+ `--self-test`),
`check-system-context-census` (+ `--self-test`),
`check-undeclared-dep-imports` (+ `--self-test`),
`docs-audit/check-affected-docs`, `docs-audit/check-drift-comment`,
`pm/check-governed-queue-guard --self-test`, `pm/check-harness-current
--self-test`, spec `check:empty-state` · `check:liveness` ·
`check:strictness-ledger` · `check:variant-docs`,
`check:agent-test-spelling`, `check:commit-card-trailers`,
`check:cross-package-test-inputs`, `check:doc-authoring`,
`check:driver-memory-census`, `check:org-identifier`,
`check:page-declaration-shape`, `check:pm-skill-id-lint`,
`check:published-files`, `check:refd-timer-probe`,
`check:skill-frame-sync`, `check:slot-lookup`,
`check:test-source-alias`, `check:tier-file-adoption`,
`check:type-source-resolution`, `check:watch-hint-literal`.

NOT MEASURED (exit 3, `PREREQUISITE NOT MET` — each loads BUILT
artifacts and refuses on this unbuilt worktree; the diff touches no
source, config or manifest, so their verdict is CI's on its correctly
built tree): `pnpm --filter @objectstack/lint run
check:doc-formula-expressions` (`@objectstack/formula` and
`@objectstack/lint` not built), `pnpm check:dts-closure`, `pnpm
check:dual-build-cjs-loads` (101 packages without `dist/`), `pnpm
check:lean-entry-closure` (`packages/objectql/dist` absent), `pnpm
check:sourcemap-no-sources-content`. Their only remedy is a whole-repo
`pnpm build`, not proportionate to a prose-only change; declared, not
hidden.

① / ②: the only package touched is `@objectstack/dogfood` (private, no
`files[]`, scripts `typecheck` + `test` only); the README is an input to
neither (the sole `README` mention under `packages/qa/dogfood/test` is a
comment in `rls-fixture.dogfood.test.ts:29`), so no closure build and no
package test is owed — a declared narrowing.

`skip-changeset`: `.claude/**` is on the fast lane and
`@objectstack/dogfood` is private with no `files[]` — nothing published
moves.

## Acceptance notes

- noted, not filed: the card attributes 「`git grep -lI
'ablation-replace'` → 1」 to `ab1d35925`, but that sha itself answers 0 —
the helper landed in `d0b8ec2aaf` (objectstack-ai#18598) after it; the reading was
evidently taken on a tree that already carried the merge. The premise is
unaffected: base `f6189a43f9` answers 1 (the helper alone). 承接者:无 —
recorded here and in the report.
- noted, not filed: `ablation-dist-preflight` answers 8 on the tip, not
the card's 7 — the eighth is the helper's own header cross-referencing
its sibling, which is the intended composition (the two tools prove
different halves and say so). 承接者:无.
- Rider not taken: the pending `tail --pid` rider in 干净收尾 (named by PR
objectstack-ai#18659) stays for the next os-dev.md PR; this PR's deletion in that
block is the duplicate opener, not a slot for it.

## 维护者速读(草稿)

**改了什么** — 只动两处文档。`.claude/agents/os-dev.md`(行数不变 403 / 403)在消融步骤里加一行,把
dev 指向 PR objectstack-ai#18598 新落地的锚点核验替换助手 `scripts/ablation-replace.mjs`,与已有的
`ablation-dist-preflight.mjs` 指向并排、按消融实际执行顺序(先落盘变异、再证明到达
`dist/`、再读结果);付账是删掉「干净收尾」节开头一条与文件头部及同节下一条重复的「报告落两次」句。`packages/qa/dogfood/README.md`
的「Adding a golden test」第 4 步加一句同样的指向。

**为什么改** — 这个助手落地了,却没有任何一份指令文档指向它;而指向其姊妹工具的文档有两份。一个采纳式工具若无人被指向,只有碰巧 grep
过 `scripts/` 的人会用到 —— 这正是卡 objectstack-ai#18226 要消灭的「靠记性」。⛔
不做成必经路径、不加门禁、不批量迁移旧消融脚本(objectstack-ai#18226 分诊已定这条地板)。

**风险与代价(含回滚)** — 零代码、零
changeset、零门禁改动;唯一的内容损失是被删的那条重复句,它的三个主张在文件头部(:17–:18)与同节下一条(:320)各有原文。回滚
= revert 本 PR,无其它文件牵连。

**席位意见** — (留空,席位定稿)

**你要做的** — 读新 :254 一行与被删 :319 一行;批准即由席位落地。若不同意付账形态,点名另一条重复句,席位代改。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

消融变异该走校验锚点的替换助手 —— perl -i 家族已在两次被记录的消融里静默失败,模式各不相同

2 participants