You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 499f7f9
Browse filesBrowse the repository at this point in the historyBrowse files
fix(pm): post-stamped refuses a token it cannot render (#18286)
Fixes#18284
## The defect
`scripts/pm/post-stamped.mjs` recognised its two tokens with
`QUOTED_TOKEN_RE`
(`{{WAS:([^{}]*)}}`) and caught the rest with a post-substitution
leftover scan
over `ANY_TOKEN_RE` (`{{[^{}]*}}`). Both spell their payload `[^{}]*`,
so a
token-shaped opener whose payload carries a **brace**, or one that never
closes
at all, matched neither: it was not substituted, because nothing
recognised it,
and it was not refused, because the leftover scan could not see it. It
was
copied to the board verbatim.
The filed reading is the seat's own artefact: a quoted slot was filled
from a
shell variable, the read failed, and the variable held a multi-line Node
dump —
which carries braces. Reproduced here on the unpatched tool at
`fe0ae5c1`:
```
$ node scripts/pm/post-stamped.mjs --dry-run --comment=18284 --file=dump.md ; echo "EXIT=$?"
post-stamped: DRY RUN — nothing was written. 0 token(s) substituted with
`2026-09-15T08:18Z`, 0 quoted stamp(s) rendered verbatim. Target would be
#18284 (comment).
EXIT=0
```
…and its stdout was the body it would have written, opener, dump and
closer
intact. Same fixture after the patch, `EXIT=2`, nothing on stdout:
```
post-stamped: REFUSED — 1 double-brace opener(s) in this body are not tokens this tool
can render. Nothing was written.
1. [brace-in-payload] a brace inside the payload, so no token ends here — `{{WAS:[eval]:1\nJSON.parse(process.env.LANDED).mergedAt\n …`
```
## What changed
The check moved **ahead of substitution** and changed what it walks: not
the
tokens a regex happens to match, but every `{{` in the body,
positionally —
because the defect is precisely an opener no regex in the file matches,
so a
scan built out of those same regexes would walk past it again. Each
opener must
be `{{NOW}}` or a `{{WAS:…}}` whose payload is brace-free and closed.
Four
shapes are refused and named: `unknown-token-name`, `unclosed-opener`,
`brace-in-payload`, `nested-opener`. The refusal prints the offending
span
clipped to 60 bytes with control characters escaped, so a payload that
arrived
from a shell read gone wrong stays readable; the same renderer is now
applied to
the existing `quoted-not-a-stamp` detail, which used to inline a raw
multi-line
dump into a numbered row.
The scan judges **shape and stops**. Whether an admitted payload is an
instant,
and one the clock has reached, stays with `stampRefusals` — two places
deciding
"what is a quoted stamp" is two spellings of one decision. What the scan
gives
that rule is the thing it could not previously assume: that
`maskQuotedStamps`
consumed every opener, so the bare-stamp scan underneath is reading
prose and
not the inside of a token nobody could parse. That ordering is pinned:
an
unclosed opener on the opening line is reported as the opener it is, not
as the
positional refusal the unmasked payload would otherwise raise.
The post-substitution leftover check is **gone** rather than kept beside
the
scan: after the scan every opener is consumed and the two substituted
stamps
carry no braces, so it could never fire, and a check that cannot fire is
a check
nobody maintains. Its `kind` (`unknown-token`), its exit code and the
two
sentences callers read are preserved on the new refusal.
**No accepted form is narrowed.** The forms the tool took before —
`{{NOW}}`
exactly; `{{WAS:…}}` with a payload that is one `protocolStamps` match
and
nothing else, minute- or seconds-grained, surrounding whitespace
tolerated —
all still clear the scan, each pinned by a case. A bare stamp in prose
is still
not an opener's business, and a token inside backticks is **still
substituted**:
there has never been an escape form and the scan does not invent one.
`--dry-run` and the live write take the same refusal, because both run
through
`renderBody`. The DRY RUN line, the read-back delta note, the
`--ack-through`
rule and the footer policy are untouched.
## Verification
- Self-test **117 → 152** cases, **8 → 9** batteries;
`SELF_TEST_BATTERY_FLOOR`
raised 8 → 9 so a deleted battery is still caught. New battery: *the
opener
scan: every `{{` is a token this tool renders, or the body is refused*
(35
cases) — the filed repro, a multi-line payload, an unknown token name,
an
unclosed opener, nested and doubled openers, the valid-pair control, the
no-narrowing controls, the no-escape-hatch controls, and the span
renderer.
- Reverse verification by ablation from the committed state: the base
blob was
checked out over the file, its presence proved by `git hash-object`
against
`fe0ae5c1:scripts/pm/post-stamped.mjs` (and `grep -c
unrecognisedOpeners` → 0),
the fixture run, then `git checkout HEAD --` restored and `git diff
HEAD`
proved empty. Both readings are quoted above.
- All 32 commands from `node scripts/pm/dispatch-gates.mjs --commands`
exit 0,
plus `node scripts/pm/post-stamped.mjs --self-test`,
`node scripts/check-self-test-wired.mjs` and ESLint on the file.
- No changeset: nothing published moves. `scripts/pm/**` is not in any
package's
`files[]`.
## Acceptance notes
Noted, not filed — nothing in this PR's path touches them and no queued
card
covers the file:
- The tool's usage text and header describe the quoted payload as
`YYYY-MM-DDThh:mmZ`, while the shape actually accepted is
`PROTOCOL_STAMP_RE`, whose seconds group is optional. The seconds grain
is
deliberate and pinned by an existing case; only the prose is narrower
than the
contract. Documentation nit, left alone rather than widened in a fix PR.
One reproducible defect found while reading the payload rules is
reported in the
terminal report for the seat to file, with dedupe words: a quoted
payload that
matches the stamp *shape* but names an impossible date is accepted and
rendered
verbatim, because `stampSpan` returns `null` for it and `stampIsFuture`
reads
`null` as "not future". It is a different failure mode from this card's
—
rendered-but-unvalidated rather than neither-rendered-nor-refused — so
it is not
fixed here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr)_
Co-authored-by: Claude <noreply@anthropic.com>
'the token contract: the two spellings, and nothing else': 9,
822
988
'the refusals: every route that must not reach the board': 20,
989
+
'the opener scan: every `{{` is a token this tool renders, or the body is refused': 35,
823
990
'the direction check: a stamp no act can have read': 22,
824
991
'the substitution: one clock, read once, written everywhere': 9,
825
992
'the read-back: what the transcript can actually prove': 11,
826
993
'the CLI: the one decision a typo must never make': 16,
827
994
'the unread-knock check: a refresh cannot void what nobody read': 23,
828
995
'the shared rule: this tool and H56 cannot come to disagree': 6,
829
996
});
830
-
constSELF_TEST_BATTERY_FLOOR=8;
997
+
constSELF_TEST_BATTERY_FLOOR=9;
831
998
constUNATTRIBUTED_BATTERY='(unattributed)';
832
999
833
1000
letselfTestReachedVerdict=false;
@@ -882,6 +1049,61 @@ export function selfTest() {
882
1049
t('…and states that no flag turns the contract off',refusalText(stampRefusals(OPENING)).includes('There is no flag'));
883
1050
t('…and that nothing was written',refusalText(stampRefusals(OPENING)).includes('Nothing was written'));
884
1051
1052
+
// The filed artefact's shape, in kind: a shell read failed and the quoted
1053
+
// slot took a multi-line Node dump. Dumps carry braces, and both payload
1054
+
// classes in this file are `[^{}]*`, so nothing matched and the opener was
1055
+
// copied to the board — neither rendered nor refused.
1056
+
battery('the opener scan: every `{{` is a token this tool renders, or the body is refused');
1057
+
constCARD_DUMP=
1058
+
'Merged at {{WAS:[eval]:1\n'+
1059
+
'JSON.parse(process.env.LANDED).mergedAt\n'+
1060
+
' ^\n'+
1061
+
"SyntaxError: Unexpected token '}' in JSON at position 0}} — read from the merge commit.";
1062
+
t('⭐ the filed repro: a quoted slot holding a Node dump is REFUSED, not copied to the board',renderBody(CARD_DUMP,NOW_MS).ok===false);
1063
+
t('…and nothing is rendered from it',renderBody(CARD_DUMP,NOW_MS).body===undefined);
1064
+
t('⛔ WHY it used to pass: neither payload class can cross a brace, so nothing matched at all',QUOTED_TOKEN_RE.test(CARD_DUMP)===false&&ANY_TOKEN_RE.test(CARD_DUMP)===false);
1065
+
t('…so the scan walks openers positionally and names the brace',unrecognisedOpeners(CARD_DUMP)[0].kind==='brace-in-payload');
1066
+
t('…and the refusal prints the span with its newlines ESCAPED',renderBody(CARD_DUMP,NOW_MS).error.split('\n')[2].includes('[eval]:1\\n'));
1067
+
t('⛔ …so the dump never reaches the refusal text raw, at any length',renderBody(CARD_DUMP,NOW_MS).error.includes('\nJSON.parse(process.env')===false);
1068
+
1069
+
constMULTI_LINE_PAYLOAD='Merged at {{WAS:[eval]:1\nSyntaxError: Unexpected end of JSON input}} — read from the log.';
1070
+
t('a quoted payload spanning LINES is refused',renderBody(MULTI_LINE_PAYLOAD,NOW_MS).ok===false);
1071
+
t('…by the shape rule, which owns what a quoted payload may say',kinds(MULTI_LINE_PAYLOAD,NOW_MS).join()==='quoted-not-a-stamp');
1072
+
t('…and its span is escaped and clipped too — one spelling of "show me the offender"',stampRefusals(MULTI_LINE_PAYLOAD,NOW_MS)[0].detail.includes('[eval]:1\\nSyntaxError'));
1073
+
1074
+
t('an unknown token NAME is refused',renderBody('Claim: {{THEN}} — dispatched.',NOW_MS).ok===false);
1075
+
t('…named as a well-formed token this tool does not know',unrecognisedOpeners('{{THEN}}')[0].kind==='unknown-token-name');
1076
+
t('…and the recorded `{{now}}` typo is that same kind',unrecognisedOpeners('Claim: {{now}}')[0].kind==='unknown-token-name');
1077
+
1078
+
constUNCLOSED='Landing provenance\n\nMerged at {{WAS:2026-09-08T14:00Z — read from the merge commit.';
1079
+
t('an opener with NO closer is refused',renderBody(UNCLOSED,NOW_MS).ok===false);
1080
+
t('…named as exactly that, rather than guessed at',unrecognisedOpeners(UNCLOSED)[0].kind==='unclosed-opener');
1081
+
t('…and the act-clock token is not exempt: an unclosed `{{NOW` is refused too',unrecognisedOpeners('Claim {{NOW — dispatched.')[0].kind==='unclosed-opener');
t('⭐ ONE opener, ONE refusal: an unclosed opener is reported as the opener it is',renderBody(UNCLOSED_OPENING,NOW_MS).kind==='unknown-token');
1084
+
t('⛔ …and NOT as the positional refusal the unmasked payload would otherwise raise',kinds(UNCLOSED_OPENING,NOW_MS).includes('positional')&&renderBody(UNCLOSED_OPENING,NOW_MS).error.includes('positional')===false);
1085
+
t('a DOUBLED opener is refused',unrecognisedOpeners('Claim {{{{NOW}}}} — dispatched.')[0].kind==='nested-opener');
1086
+
t('…and a NESTED one, a quoted slot wrapped round the act-clock token',unrecognisedOpeners('Board read {{WAS:{{NOW}}}}.')[0].kind==='nested-opener');
1087
+
t('every opener is walked, not only the first',unrecognisedOpeners('Claim {{now}} and board read {{WAS:{}}}.').length===2);
1088
+
1089
+
constSCAN_CONTROL='Verdict {{NOW}} — on the board read {{WAS:2026-09-08T14:00Z}}.';
1090
+
t('⭐ THE CONTROL: one valid `{{NOW}}` and one valid quoted stamp still render',renderBody(SCAN_CONTROL,NOW_MS).ok===true);
1091
+
t('…with the scan finding nothing to refuse',unrecognisedOpeners(SCAN_CONTROL).length===0);
1092
+
t('…and the rendered body carrying no opener at all',renderBody(SCAN_CONTROL,NOW_MS).body.includes('{{')===false);
1093
+
t('⛔ NO accepted form narrowed: whitespace inside the declaration still clears the scan',unrecognisedOpeners('read {{WAS: 2026-09-08T14:00Z }}').length===0&&renderBody('read {{WAS: 2026-09-08T14:00Z }}',NOW_MS).ok===true);
1094
+
t('⛔ …and the seconds grain still clears it',unrecognisedOpeners('read {{WAS:2026-09-08T14:00:30Z}}').length===0);
1095
+
t('⛔ …and a bare stamp in prose is no opener\'s business',unrecognisedOpeners('The 2026-09-08T14:00Z ruling stands.').length===0);
1096
+
t('⛔ the scan opens NO escape hatch: the entity spelling is not an opener, so it is prose',unrecognisedOpeners('the token {{NOW}}').length===0);
1097
+
t('⛔ …and a token inside backticks is STILL substituted — a fence is not an escape',renderBody('Write `{{NOW}}` there.',NOW_MS).body==='Write `2026-09-10T06:37Z` there.');
1098
+
1099
+
t('the span renderer escapes a newline',offendingSpan('a\nb')==='a\\nb');
1100
+
t('…a carriage return and a tab too',offendingSpan('a\r\tb')==='a\\r\\tb');
1101
+
t('…and any other control byte as an `\\xNN` escape, never as the byte itself',offendingSpan('a\u0007b')==='a\\x07b');
1102
+
t('a span inside the budget is printed whole, with no ellipsis',offendingSpan('{{THEN}}')==='{{THEN}}');
1103
+
t(`a longer one is clipped to ${SPAN_BYTES} bytes and says so`,offendingSpan('x'.repeat(100))===`${'x'.repeat(SPAN_BYTES)}…`);
1104
+
t('⛔ …and never cuts a multi-byte character in half',offendingSpan(`${'x'.repeat(SPAN_BYTES-1)}€€`)===`${'x'.repeat(SPAN_BYTES-1)}…`);
1105
+
t('the budget is counted in BYTES, which is the unit a dump arrives in',SPAN_BYTES===60&&Buffer.byteLength(offendingSpan('€'.repeat(40)),'utf8')<=SPAN_BYTES+3);
1106
+
885
1107
// The clock this act holds is 06:37:48 — so 06:37Z is the minute it is IN,
886
1108
// 06:38Z the first minute it has not reached, and 06:51Z sits 14 minutes
887
1109
// ahead, inside H56's drift tolerance and still not a thing anyone read.
0 commit comments