Commit 3d6f0aa
fix(scripts/pm): the read-back names the collapse OVER the strip instead of calling the write lost (#19456)
Fixes #19312
Clause-②: no
`post-stamped --body` answered exit 4 — `WRITTEN BUT NOT STORED … the
write did NOT land` — on thirteen writes that had landed whole. The
response a false NOT-STORED invites is the one that duplicates a card
body: a re-post.
## What was measured
Thirteen `--body` writes in one shift exited 4 on the same one-line
difference: 7 of 11 at 2026-09-20T21:40Z (#19343 · #19360 · #19390 ·
#19392 · #19395 · #19396 · #19404) and 6 more at 22:31Z (#19179 · #19221
· #19309 · #19336 · #19439 · #19440). The body sent `\n\n\n---` before
its trailing footer rule and the platform stored `\n\n---`, content
byte-identical on a fresh `GET`.
Reproduced offline on this branch's base (57ceb9d), with the stored
tails read back from the cards themselves:
| sent | stored | class at 57ceb9d | class here |
|:---|:---|:---|:---|
| `H` + `\n` + block | `H` + block | `footer-blank-collapsed`, exit 0 |
unchanged |
| `H` + `\n` + block + `\n` | `H` + block | **`mutated`, exit 4** |
**`footer-blank-collapsed`, exit 0** |
| `H` + block + `\n` | `H` + `\n` + block | `footer-re-anchored`, exit 0
| unchanged |
| `H` + `\n\n` + block + `\n` | `H` + block | `mutated`, exit 4 |
unchanged — exit 4 |
The failing row is the shape a seat post's `--body` write actually
sends: the file ends in a newline **and** its last paragraph stands a
blank line above the rule, so three newlines go out before the rule and
one after the block. The platform strips the trailing newline and
collapses the run back to the block's own two. Two acts, both already
declared on their own; the classifier had no arm for the pair.
## The one predicate that decided it
`footerReAnchoring`'s collapse arm required `strippedNewlines === 0` and
read `sentText`. It now reads the **trimmed** body, so ONE arm covers
the collapse alone and the collapse over the strip:
```js
- if (strippedNewlines === 0 && sentText.endsWith(PLATFORM_COMMENT_FOOTER)) {
- const head = sentText.slice(0, sentText.length - PLATFORM_COMMENT_FOOTER.length);
- if (stored === `${head}${PLATFORM_COMMENT_FOOTER_COLLAPSED}`) return { strippedNewlines: 0, shape: 'collapsed' };
+ if (trimmed.endsWith(PLATFORM_COMMENT_FOOTER)) {
+ const head = trimmed.slice(0, trimmed.length - PLATFORM_COMMENT_FOOTER.length);
+ if (stored === `${head}${PLATFORM_COMMENT_FOOTER_COLLAPSED}`) return { strippedNewlines, shape: 'collapsed' };
```
Still an exact `===` against a candidate BUILT from the sent bytes, and
the head is still compared literally. ⛔ No multiset compare, ⛔ nothing
position-insensitive, ⛔ no pattern, ⛔ no length test.
## The DECLARED set, before and after
Only one row moved, and it moved by naming a cell that is now measured
rather than by widening a comparison.
- **before** — `footer-blank-collapsed`: *the stored body is that body
with the blank line immediately before the footer block's rule collapsed
— one newline of the block's own separator gone, and no content byte
touched.* The arm refused the pair in prose too: *a sent body that
carried trailing newlines AND came back collapsed is a cell nobody has
measured, and an unmeasured cell is not one this tool forgives.*
- **after** — the same row, *…collapsed, over any trailing newline(s)
the platform does not keep — one newline of the block's own separator
gone, and no content byte touched.*
`identical`, `trailing-newline-stripped`, `footer-appended`,
`footer-re-anchored`, `mutated` are untouched, and no class was added:
the vocabulary is still six words.
The rendered line had to move with it, because it said *the one byte
short is that separator newline* about a body that is now two short. It
names the second byte when there is one: `COLLAPSED, over 1 stripped
trailing newline(s), so the byte(s) short are that separator newline and
the newline(s) the platform does not keep, and every CONTENT byte sent
IS stored`.
## The three pinned fixtures
1. **Today's pair**, carried by the three cards whose stored byte counts
were read back off the platform (#19343 · 3585, #19360 · 2892, #19404 ·
4623): classifies `footer-blank-collapsed` with `strippedNewlines: 1`,
lands, exit 0.
2. **One CONTENT byte different** under the very same
strip-and-collapse: exit 4.
3. **The footer RELOCATED** — the same bytes in another position, equal
length, identical multiset: exit 4, with `firstDifferingByte` pinned at
0.
Plus the pin this change had to rewrite rather than delete: the case
that asserted `footerReAnchoring(sent + newline, collapsed) === null`
pinned exactly the branch being removed, so it now asserts the measured
shape and its recorded `strippedNewlines`. Every other exit-4 control in
both footer batteries is untouched and still green — a byte lost before
the rule, a byte changed, a chewed footer, a rewritten link, a
truncation ending in the block, a newline from nowhere, both separator
newlines gone, and the whitespace-only truncation in the CONTENT (row 4
of the table above).
## The relocation decision, on the four axes
The card's original claim was a pure RELOCATION of the footer — sent and
stored differing by position while the multisets are identical — and
suggested a multiset compare. That shape stays exit 4, pinned as fixture
3.
- **实际业务需求** — no relocation has ever been measured on this channel. The
card's claim was inherited and explicitly not re-measured; when the seat
did re-measure it today, what it found was the blank-line collapse, with
the footer exactly where it was sent. A declared class for a cell nobody
has read serves no write that exists.
- **项目长远合理性** — the tool's entire value is an exact compare against a
named list of measured transformations. `no workarounds`: a positional
exemption with no reading behind it is tolerance, not a contract.
- **防 AI 写错** — this is the decisive axis. Forgiving "same bytes,
different order" is precisely what makes
#19350 worse: that card is the read-back going
SILENT on a body the platform rewrote in 482 places, and a rewrite that
happens to preserve the byte multiset would pass a multiset compare.
Strictness on the loud side must not be bought with silence on the quiet
one.
- **创业阶段不扩散** — zero measured instances, zero pull, so
implementation-first: it is not declared. If the platform is ever
measured relocating a footer, that reading adds its own arm with its own
word.
## The boundary this change holds
#19350 (closed) is the OPPOSITE direction of
the same comparison and the reason widening is refused. The only newly
forgiven shape here is `stored === (sent minus its own trailing
newlines, with the footer block's leading blank line collapsed)`. Any
content rewrite — including a 482-place one — makes that `===` fail, so
nothing in this change moves the read-back toward silence. The boundary
is re-stated in prose in the classifier docblock; #19350 is not
addressed here.
## Verification
- `node scripts/pm/post-stamped.mjs --self-test` :: exit 0 — 544 cases
across 18 batteries, up 14.
- Reverse verification (ablation): re-adding the single predicate
`strippedNewlines === 0 &&` — through `scripts/ablation-replace.mjs`,
anchor hit 1 time, blob `46734b107fcd` to `b24615b0ab9a` on disk — turns
the self-test red in exactly the expected direction: **9 of 544 cases
fail, all of them the new ones, 0 floor problems**. Restore leg verified
against HEAD's blob, not an exit code: `blob == HEAD (46734b1)` and
`git diff HEAD` empty.
- The derived gate union for this diff (`node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, no paths): 30 families, each run with its
exit captured BEFORE any pipe. Reconciled with `--ran`: 30 derived, 30
run, 0 UNRUN.
- NOT governed: the `GOVERNED_SURFACES` register in
`scripts/pm/check-governed-merges.mjs` carries `docs/adr/`, `.claude/`,
`skills/`, `AGENTS.md`, `CLAUDE.md` and `docs/NORTH-STAR.md` —
`scripts/pm/**` is on none of them. The `--pair` reading is in the
report on the card.
## Acceptance notes
- `.claude/skills/pm-dispatch/references/platform-readings.md` :360
records the tool's pre-fix behaviour as a fact — 「送全块即触发该归一 ⇒
`post-stamped` 的 `body` 档把这点空白判 `mutated`,净零字节良性告警。」 — and is stale once
this lands. That file is a governed fact table and outside this card's
declared file surface; carrier: the `domain:skills` seat, alongside this
PR. Line :359 (「平台在尾部 `---` 前后正反两向归一空行」) is unaffected and is in fact
the reading this change acts on.
- `\n\n\n---` reaches the platform because the body FILE carries the
extra blank line, not because the tool adds one. Whether the seat's body
composition should normalise its own tail before the write is a separate
question from whether the read-back should name what the platform does
with it; noted, not filed — this PR changes only the instrument's
reading.
- Net +49 lines in one file, inside the PM's +60 budget.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 59cf244 commit 3d6f0aa
1 file changed
Lines changed: 73 additions & 24 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
424 | 424 | | |
425 | 425 | | |
426 | 426 | | |
427 | | - | |
428 | | - | |
429 | | - | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
430 | 434 | | |
431 | 435 | | |
432 | 436 | | |
| |||
511 | 515 | | |
512 | 516 | | |
513 | 517 | | |
514 | | - | |
| 518 | + | |
| 519 | + | |
515 | 520 | | |
516 | 521 | | |
517 | 522 | | |
| |||
527 | 532 | | |
528 | 533 | | |
529 | 534 | | |
530 | | - | |
531 | | - | |
532 | | - | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
533 | 539 | | |
534 | 540 | | |
535 | 541 | | |
| |||
1724 | 1730 | | |
1725 | 1731 | | |
1726 | 1732 | | |
1727 | | - | |
| 1733 | + | |
1728 | 1734 | | |
1729 | 1735 | | |
1730 | 1736 | | |
| |||
1792 | 1798 | | |
1793 | 1799 | | |
1794 | 1800 | | |
1795 | | - | |
1796 | | - | |
1797 | | - | |
1798 | | - | |
1799 | | - | |
| 1801 | + | |
| 1802 | + | |
| 1803 | + | |
| 1804 | + | |
| 1805 | + | |
| 1806 | + | |
| 1807 | + | |
| 1808 | + | |
1800 | 1809 | | |
1801 | 1810 | | |
1802 | 1811 | | |
| |||
1850 | 1859 | | |
1851 | 1860 | | |
1852 | 1861 | | |
1853 | | - | |
1854 | | - | |
1855 | | - | |
1856 | | - | |
1857 | | - | |
1858 | | - | |
| 1862 | + | |
| 1863 | + | |
| 1864 | + | |
| 1865 | + | |
| 1866 | + | |
| 1867 | + | |
| 1868 | + | |
| 1869 | + | |
| 1870 | + | |
| 1871 | + | |
| 1872 | + | |
| 1873 | + | |
1859 | 1874 | | |
1860 | 1875 | | |
1861 | 1876 | | |
| |||
1869 | 1884 | | |
1870 | 1885 | | |
1871 | 1886 | | |
1872 | | - | |
1873 | | - | |
| 1887 | + | |
| 1888 | + | |
| 1889 | + | |
1874 | 1890 | | |
1875 | 1891 | | |
1876 | 1892 | | |
| |||
2029 | 2045 | | |
2030 | 2046 | | |
2031 | 2047 | | |
2032 | | - | |
2033 | | - | |
| 2048 | + | |
| 2049 | + | |
| 2050 | + | |
2034 | 2051 | | |
2035 | 2052 | | |
2036 | 2053 | | |
| |||
3568 | 3585 | | |
3569 | 3586 | | |
3570 | 3587 | | |
3571 | | - | |
| 3588 | + | |
| 3589 | + | |
3572 | 3590 | | |
3573 | 3591 | | |
3574 | 3592 | | |
| |||
3577 | 3595 | | |
3578 | 3596 | | |
3579 | 3597 | | |
| 3598 | + | |
| 3599 | + | |
| 3600 | + | |
| 3601 | + | |
| 3602 | + | |
| 3603 | + | |
| 3604 | + | |
| 3605 | + | |
| 3606 | + | |
| 3607 | + | |
| 3608 | + | |
| 3609 | + | |
| 3610 | + | |
| 3611 | + | |
| 3612 | + | |
| 3613 | + | |
| 3614 | + | |
| 3615 | + | |
| 3616 | + | |
| 3617 | + | |
| 3618 | + | |
| 3619 | + | |
| 3620 | + | |
| 3621 | + | |
| 3622 | + | |
| 3623 | + | |
| 3624 | + | |
| 3625 | + | |
| 3626 | + | |
| 3627 | + | |
| 3628 | + | |
3580 | 3629 | | |
3581 | 3630 | | |
3582 | 3631 | | |
| |||
0 commit comments