fix(pm): post-stamped names the platform's known read-back normalisations and locates a real mutation - #18298
Conversation
…ions and locates a real mutation `readBackVerdict` compared `stored === sent`, and GitHub stores no body byte-for-byte: every body refresh reads back one byte short (the trailing newline is stripped) and every comment 58 bytes long (the platform appends its footer block). The MUTATED warning therefore fired on nearly every write, and the one real mutation of a shift — a token the sanitizer ate out of a PR's provenance line — printed the same sentence as the noise. The stored body is now judged against a declared set of MEASURED normalisations, and the line names the one it saw: `identical`, `trailing-newline-stripped`, `footer-appended` (comment mode only — whether the platform synthesises a footer for a footer-less issue body is unmeasured, so that shape stays mutated), and `mutated` for everything else, which keeps the warning and adds the first differing BYTE offset with up to 60 bytes of context from each body, control characters escaped. - `PLATFORM_COMMENT_FOOTER`: the exact 58 bytes, declared once — not a regex and not a trim, so a footer the sanitizer has chewed is not forgiven. - `classifyReadBack` / `firstDifferingByte`: exported and pure, so the self-test and a probe drive the same rule the transcript line prints. - `readBackVerdict` takes the mode; it defaults to the strict `body` reading, so the comment-only footer rule must be asked for by name. - `--json` carries the class and the offset beside `body_mutated`. - 21 self-test cases in the read-back battery (12 -> 33, floor raised to 33), including the body-mode footer control and a mutation underneath an appended footer. Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Reviewing seat: ① Derived judgments
② Semver levelNot applicable — nothing published moves; ③ Boundary flags
Implemented-by: VERDICT: PASS Edit by the reviewing seat: the second seat flag above first read 「spells Generated by Claude Code |
|
Landing provenance — skills seat, session Generated by Claude Code |
Fixes #18296
The defect
readBackVerdictcomparedstored === sent, and GitHub stores no body byte-for-byte. Measured over one shift: every--bodyrefresh read back one byte short (the platform strips the trailing newline) and every comment 58 bytes long (the platform appends its footer block). So the warning meant for the sanitizer — "the platform MUTATED the body" — fired on nearly every write, and the one real mutation of that shift, the garbled token on a provenance line (carded as #18284), printed the same sentence as all the noise around it.What changed —
scripts/pm/post-stamped.mjsonlyPLATFORM_COMMENT_FOOTER: the platform's comment footer as the exact 58 bytes, declared once. Not a regex and not a trim: the question a read-back asks is whether the difference is exactly a normalisation somebody measured, and a pattern matching "a footer, roughly" also forgives a footer the sanitizer has chewed.classifyReadBack({ sent, stored, mode })(exported, pure):identical·trailing-newline-stripped(stored equals sent minus its trailing newlines) ·footer-appended(that body plus exactly the footer constant, with or without the strip) ·mutatedfor everything else, carrying the first differing BYTE offset and up to 60 bytes of context from each body, control characters escaped the way the refusals escape a span.mutatedwith its offset until somebody measures it.readBackVerdicttakes the mode, and defaults to the strictbodyreading, so the lenient rule has to be asked for by name.read-back: clean — the platform stripped the trailing newline (sent N, stored M)/read-back: clean — the platform appended its footer (sent N, stored M)— no warning sign,mutated: false. The MUTATED line keeps its warning and its sanitizer sentence and addsfirst difference at byte K: sent ... | stored ....--jsoncarriesread_back: { class, first_difference_byte }beside the existingbody_mutated.Nothing else moved: the stamp lines, the refusals, the unread-knock check and the write path are untouched.
The design choice: an exact declared set, not a tolerant comparison
Trimming both sides, or matching the footer with a pattern, buys the same quiet by making the tool agree with whatever it is shown — and what it would then agree with is a chewed footer or a whitespace edit nobody made. Declaring the measured set keeps the tool loud everywhere else and keeps the unmeasured body-footer cell honest, which is the contract-first shape: the leniency is declared, named in the output, and removable.
Reverse verification (ablation from the committed state)
Base blob
0e6d55abf5a895c285bdb20879fb2024915543cbrestored to disk and confirmed bygit hash-object; reading taken; HEAD blobf5fe528a9f2b5c0752efcb71f22226634e1b0393restored andgit diff HEADempty with a cleangit status --porcelain.sent 'x\n',stored 'x', bodymutated=true, "the platform MUTATED the body"mutated=false,trailing-newline-stripped, "clean — the platform stripped the trailing newline (sent 2, stored 1)"sent 'x',stored 'x' + footer, commentmutated=true, same sentencemutated=false,footer-appended, "clean — the platform appended its footer (sent 1, stored 59)"sent 'a [b] c',stored 'a b c'mutated=true, same sentence, no offsetmutated=true, offset at byte 2 withsent ...[b] cbesidestored ...b cAll three printed one indistinguishable warning before; two are now named clean and the third is located.
The smoke test is the act: this card's terminal report is posted with the tool itself, and its own read-back line is quoted in that report.
Gates
node scripts/pm/dispatch-gates.mjs --commandsderived 32 commands; all 32 ran, reconciled with--ran.node scripts/pm/post-stamped.mjs --self-test212 to 233 cases across 10 batteries. ESLint on the file clean,node scripts/check-self-test-wired.mjsclean.Acceptance notes
writeArtefactre-reads the artefact with a second GET in both modes, so a comment write costs two requests where the POST response already carries the stored body. Harmless, and a read-back that goes back to the platform is arguably the stronger proof.Generated by Claude Code