Skip to content

Commit 3d6f0aa

Browse files
os-steveclaude
andauthored
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

File tree

‎scripts/pm/post-stamped.mjs‎

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -424,9 +424,13 @@
424424
* which is why it is not the class above, whose
425425
* word is "plus".
426426
* footer-blank-collapsed stored is that body with the block's own blank
427-
* line collapsed to one newline — sent N, stored
428-
* N-1. The footer path DOES take a byte here, and
429-
* the byte is its own separator (#19048).
427+
* line collapsed to one newline, with or without
428+
* the strip above — sent N, stored N-1 on its
429+
* own and N-2 over a stripped trailing newline,
430+
* which is the shape a seat post's `--body`
431+
* write sends (#19048, #19312). The footer path
432+
* DOES take a byte here, and the byte is its own
433+
* separator.
430434
* mutated anything else — the warning, kept whole, plus
431435
* the FIRST DIFFERING BYTE and what stands at it
432436
* on each side.
@@ -511,7 +515,8 @@
511515
* gives up only newlines the platform does not keep, `footer-appended` adds
512516
* without removing, `footer-re-anchored` moves a newline the act itself sent,
513517
* and `footer-blank-collapsed` gives up one newline of the platform's OWN
514-
* footer separator. Every one of them exits 0.
518+
* footer separator, over any trailing newline it does not keep. Every one of
519+
* them exits 0.
515520
*
516521
* `mutated` is the only class that answers no, and since #18693 it is also the
517522
* only class that CAN: the two shapes the platform's own footer takes are
@@ -527,9 +532,10 @@
527532
* with those newlines removed and exactly one newline inserted
528533
* immediately before the block. Class `footer-re-anchored`,
529534
* exit 0.
530-
* COLLAPSED the sent body ended in the block with no trailing newline,
531-
* and the stored body is it with the block's blank line
532-
* collapsed. Class `footer-blank-collapsed`, exit 0 (#19048).
535+
* COLLAPSED the sent body ended in the block, with or without trailing
536+
* newline(s) of its own, and the stored body is it — those
537+
* newlines stripped — with the block's blank line collapsed.
538+
* Class `footer-blank-collapsed`, exit 0 (#19048, #19312).
533539
* NOT STORED `mutated`, and nothing else is: a byte this act sent is not
534540
* the byte the platform holds at that offset, or the stored
535541
* body stops before the sent one does. `EXIT_NOT_STORED`.
@@ -1724,7 +1730,7 @@ export const READ_BACK_CLASSES = Object.freeze({
17241730
'footer-re-anchored':
17251731
"the stored body is that body with the trailing newline(s) it sent moved to before the footer block's rule — nothing added, nothing lost",
17261732
'footer-blank-collapsed':
1727-
"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",
1733+
"the stored body is that body with the blank line immediately before the footer block's rule 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",
17281734
mutated: 'something nobody measured — the bytes disagree, and the offset says where',
17291735
});
17301736

@@ -1792,11 +1798,14 @@ function byteWindowFrom(text, from, limit = SPAN_BYTES) {
17921798
* the same bytes, a newline moved from after the footer to
17931799
* before its rule — at one trailing newline, equal length, one
17941800
* byte MOVED and zero lost.
1795-
* collapsed the sent body ENDS in the block exactly, with no trailing
1796-
* newline of its own, and the stored body is that same body
1797-
* with the block's leading blank line collapsed to one
1798-
* newline. Sent N, stored N-1: the platform really does take a
1799-
* byte away here, and the byte is its OWN separator.
1801+
* collapsed the sent body ENDS in the block — after its own trailing
1802+
* newline(s), if it sent any — and the stored body is that
1803+
* same body, those newlines stripped, with the block's leading
1804+
* blank line collapsed to one newline. The platform really
1805+
* does take a byte away here, and the byte is its OWN
1806+
* separator; over a stripped trailing newline it takes that
1807+
* one too, and THAT pair is the shape a seat post's `--body`
1808+
* write sends (#19312).
18001809
*
18011810
* ## THE CRITERION — which mutations are the footer's, and which the sanitizer's
18021811
*
@@ -1850,12 +1859,18 @@ export function footerReAnchoring(sent, stored) {
18501859
const head = trimmed.slice(0, trimmed.length - PLATFORM_COMMENT_FOOTER.length);
18511860
if (stored === `${head}\n${PLATFORM_COMMENT_FOOTER}`) return { strippedNewlines, shape: 're-anchored' };
18521861
}
1853-
// ⛔ `strippedNewlines === 0` is load-bearing, ⛔ not tidiness: a sent body
1854-
// that carried trailing newlines AND came back collapsed is a cell nobody
1855-
// has measured, and an unmeasured cell is not one this tool forgives.
1856-
if (strippedNewlines === 0 && sentText.endsWith(PLATFORM_COMMENT_FOOTER)) {
1857-
const head = sentText.slice(0, sentText.length - PLATFORM_COMMENT_FOOTER.length);
1858-
if (stored === `${head}${PLATFORM_COMMENT_FOOTER_COLLAPSED}`) return { strippedNewlines: 0, shape: 'collapsed' };
1862+
// The collapse reads the TRIMMED body, so ONE arm covers the collapse alone
1863+
// and the collapse OVER the strip — the cell this arm used to refuse as
1864+
// unmeasured, and the shape a seat post's `--body` write actually sends: a
1865+
// file ending in a newline whose last paragraph stands a blank line above the
1866+
// rule, so `\n\n\n---` goes out and the block's own `\n\n---` comes back.
1867+
// Measured 13 times in one shift (#19312), the content whole on a fresh read
1868+
// every one of them. ⛔ The head is still compared literally, so a whitespace
1869+
// truncation in the CONTENT — two newlines sent there, one stored — is still
1870+
// `null` and still exits 4.
1871+
if (trimmed.endsWith(PLATFORM_COMMENT_FOOTER)) {
1872+
const head = trimmed.slice(0, trimmed.length - PLATFORM_COMMENT_FOOTER.length);
1873+
if (stored === `${head}${PLATFORM_COMMENT_FOOTER_COLLAPSED}`) return { strippedNewlines, shape: 'collapsed' };
18591874
}
18601875
return null;
18611876
}
@@ -1869,8 +1884,9 @@ export function footerReAnchoring(sent, stored) {
18691884
* newlines the platform does not keep, `footer-appended` adds without
18701885
* removing, `footer-re-anchored` moves a newline the act itself sent, and
18711886
* `footer-blank-collapsed` drops one newline of the platform's own footer
1872-
* separator — the single byte the footer path has been measured taking, and
1873-
* never a byte of the body. So `mutated` — "something nobody
1887+
* separator, over any trailing newline the platform does not keep — the only
1888+
* bytes the footer path has been measured taking, and never a byte of the
1889+
* body. So `mutated` — "something nobody
18741890
* measured" — is the one class that can answer no, and since #18693 ONE
18751891
* measurement decides both what the status line says and what `$?` says.
18761892
* ⛔ `unreadable` answers YES on purpose: nothing was measured there, which is
@@ -2029,8 +2045,9 @@ export function readBackVerdict({ stamp, writtenAt, sent, stored, substituted =
20292045
} else if (readBack.class === 'footer-blank-collapsed') {
20302046
lines.push(
20312047
` read-back: clean — the platform re-anchored its own footer block: the blank line before its rule was` +
2032-
` COLLAPSED, so the one byte short is that separator newline and every CONTENT byte sent IS stored` +
2033-
` (sent ${sentBytes}, stored ${storedBytes})`,
2048+
` COLLAPSED${readBack.strippedNewlines > 0 ? `, over ${readBack.strippedNewlines} stripped trailing newline(s)` : ''}, so the byte(s)` +
2049+
` short are that separator newline${readBack.strippedNewlines > 0 ? ' and the newline(s) the platform does not keep' : ''}, and` +
2050+
` every CONTENT byte sent IS stored (sent ${sentBytes}, stored ${storedBytes})`,
20342051
);
20352052
} else {
20362053
// `mutated` means one thing now, so it says one thing: nobody measured this
@@ -3568,7 +3585,8 @@ export function selfTest() {
35683585
t('⛔ a loss INSIDE the block is the sanitizer\'s, ⛔ not the footer\'s, and exits 4', rb({ sent: C_SENT, stored: `${C_HEAD}${PLATFORM_COMMENT_FOOTER_COLLAPSED.replace('---', '--')}` }).exit === EXIT_NOT_STORED);
35693586
t('⛔ BOTH newlines gone is a cell nobody measured, ⛔ not a collapse this tool forgives', rb({ sent: C_SENT, stored: `${C_HEAD}${PLATFORM_COMMENT_FOOTER.slice(2)}` }).exit === EXIT_NOT_STORED);
35703587
t('⛔ …and the arm requires the act to have SENT the block: no footer, no collapse', footerReAnchoring(C_HEAD, collapsedAfter(C_HEAD)) === null);
3571-
t('⛔ …and requires it to have sent NO trailing newline: a strip AND a collapse is unmeasured', footerReAnchoring(`${C_SENT}\n`, collapsedAfter(C_HEAD)) === null);
3588+
t('⭐ …and the strip AND the collapse in ONE act is the MEASURED cell now, ⛔ no longer refused as unseen', footerReAnchoring(`${C_SENT}\n`, collapsedAfter(C_HEAD))?.shape === 'collapsed');
3589+
t('…with the strip RECORDED on the verdict, so the line can name the second byte it cost', footerReAnchoring(`${C_SENT}\n`, collapsedAfter(C_HEAD))?.strippedNewlines === 1);
35723590
t('⭐ SHAPE B IS NOT WHAT LANDED: a whitespace-only truncation in the CONTENT still exits 4', rb({ sent: `${C_HEAD}\n\n${PLATFORM_COMMENT_FOOTER}`, stored: collapsedAfter(C_HEAD) }).exit === EXIT_NOT_STORED);
35733591
const C_LOST = rb({ sent: C_SENT, stored: collapsedAfter(C_HEAD.replace('this', 'that')) });
35743592
// ⛔ The falsified claim is ASSEMBLED, ⛔ never written out: `git grep` for
@@ -3577,6 +3595,37 @@ export function selfTest() {
35773595
t('⭐ …and names all THREE measured shapes instead, the collapse included', notStoredText(C_LOST, 'o/n', 19048).includes('appends its block') && notStoredText(C_LOST, 'o/n', 19048).includes('moves a') && notStoredText(C_LOST, 'o/n', 19048).includes('COLLAPSES the blank line'));
35783596
t('…while still saying the thing that decides it: something this act sent is not there', notStoredText(C_LOST, 'o/n', 19048).includes('something this act sent is not there') && notStoredText(C_LOST, 'o/n', 19048).includes('READ THE ARTEFACT'));
35793597

3598+
// The collapse OVER the strip (#19312) — the shape a seat post's `--body`
3599+
// write actually sends: the file ends in a newline and its last paragraph
3600+
// stands a blank line above the rule, so three newlines go out before the
3601+
// rule and the block's own two come back. 13 writes in one shift exited 4 on
3602+
// it — 7 of 11 at 2026-09-20T21:40Z and 6 more at 22:31Z — every one with the
3603+
// content whole on a fresh `GET`, which is the false NOT-STORED that invites
3604+
// the duplicate re-post. The STORED byte counts are read off those cards; the
3605+
// sent side is that reading's own transformation, ⛔ not a count nobody wrote.
3606+
const liveCollapseOverStrip = (storedBytes) => {
3607+
const head = 'x'.repeat(storedBytes - FOOTER_BYTES);
3608+
return { sent: `${head}\n${PLATFORM_COMMENT_FOOTER}\n`, stored: `${head}${PLATFORM_COMMENT_FOOTER}` };
3609+
};
3610+
for (const live of [{ card: 19343, stored: 3585 }, { card: 19360, stored: 2892 }, { card: 19404, stored: 4623 }]) {
3611+
const { sent, stored } = liveCollapseOverStrip(live.stored);
3612+
const v = rb({ sent, stored });
3613+
t(`⭐ THE FILED READING — the body of objectstack#${live.card}: ${live.stored} bytes stored, and the run before the rule 3 newlines sent to 2 stored`,
3614+
Buffer.byteLength(stored, 'utf8') === live.stored && Buffer.byteLength(sent, 'utf8') === live.stored + 2, `stored=${Buffer.byteLength(stored, 'utf8')}`);
3615+
t('…and it LANDED: exit 0, ⛔ not the 4 that told a seat thirteen landed writes were lost', v.landed === true && v.exit === EXIT_OK);
3616+
t('…named by the collapse\'s own word, with the stripped newline RECORDED beside it', v.readBack.class === 'footer-blank-collapsed' && v.readBack.strippedNewlines === 1);
3617+
}
3618+
const OVER_STRIP = liveCollapseOverStrip(4096);
3619+
t('⭐ the line names the SECOND byte out loud — ⛔ never "the one byte short" about a body two short',
3620+
rb({ ...OVER_STRIP }).lines[1].includes('over 1 stripped trailing newline(s)') && rb({ ...OVER_STRIP }).lines[1].includes('every CONTENT byte sent IS stored'), rb({ ...OVER_STRIP }).lines[1]);
3621+
t('⛔ THE CONTROL — one CONTENT byte different under the very same strip-and-collapse still exits 4',
3622+
rb({ sent: `${C_HEAD}\n${PLATFORM_COMMENT_FOOTER}\n`, stored: `${C_HEAD.replace('this', 'that')}${PLATFORM_COMMENT_FOOTER}` }).exit === EXIT_NOT_STORED);
3623+
t('⛔ …and a footer RELOCATED — the SAME bytes in another position, the card\'s own first claim — exits 4',
3624+
rb({ sent: C_SENT, stored: `${PLATFORM_COMMENT_FOOTER}${C_HEAD}` }).exit === EXIT_NOT_STORED
3625+
&& Buffer.byteLength(C_SENT, 'utf8') === Buffer.byteLength(`${PLATFORM_COMMENT_FOOTER}${C_HEAD}`, 'utf8'));
3626+
t('⛔ …so a MULTISET compare is still refused: equal bytes in a different order is not a normalisation',
3627+
firstDifferingByte(C_SENT, `${PLATFORM_COMMENT_FOOTER}${C_HEAD}`) === 0);
3628+
35803629
battery('the CLI: the one decision a typo must never make');
35813630
t('a comment target parses', parseOptions(['--comment=17314']).options.mode === 'comment');
35823631
t('…with the number read as a number', parseOptions(['--comment=17314']).options.number === 17314);

0 commit comments

Comments
 (0)