fix(pm): post-stamped refuses a token it cannot render - #18286
Conversation
Every double-brace opener in a body must now be `{{NOW}}` or a `{{WAS:...}}`
whose payload is brace-free and closed; anything else is REFUSED with the
offending span, on `--dry-run` as well as on a live write.
The UNKNOWN refusal used to run AFTER substitution, over whatever the two
payload regexes had left behind. Both spell their payload `[^{}]*`, so a
token-shaped opener carrying a BRACE, or one that never closes, matched
neither: not substituted, because nothing recognised it, and not refused,
because the leftover scan could not see it. A seat filled a quoted slot from
a shell variable, the read failed and the variable held a multi-line Node
dump; `--dry-run` reported "0 token(s) substituted, 0 quoted stamp(s)
rendered verbatim" and the stored comment carried the opener, the dump and
the closer verbatim.
So the check moved ahead of substitution and walks every `{{` positionally
rather than whatever a regex matches -- the defect is precisely an opener no
regex here matches. Four shapes are named: an unknown token NAME, an opener
with no closer, a brace inside the payload, a second opener before the first
closes. Spans print 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 applied to the existing quoted-shape refusal.
The scan judges shape and stops -- whether an admitted payload is an instant
the clock has reached stays with `stampRefusals`, and what the scan gives
that rule is the thing it could not previously assume: that masking consumed
every opener. The post-substitution leftover check is gone rather than kept
beside it; after the scan it could never fire.
No accepted form is narrowed: the whitespace-tolerant declaration, the
seconds grain, a bare stamp in prose and a token inside backticks all read
exactly as before, each pinned. Self-test 117 -> 152 cases, 8 -> 9 batteries.
Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr
Co-authored-by: Claude <noreply@anthropic.com>
…st-stamped-refuses-malformed-tokens
Contract reviewServed-tier: Reviewing seat: ① Derived judgments
② Semver levelNot applicable — nothing published moves; ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Landing provenance — skills seat, session Generated by Claude Code |
Fixes #18284
The defect
scripts/pm/post-stamped.mjsrecognised its two tokens withQUOTED_TOKEN_RE(
{{WAS:([^{}]*)}}) and caught the rest with a post-substitution leftover scanover
ANY_TOKEN_RE({{[^{}]*}}). Both spell their payload[^{}]*, so atoken-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:…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: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. Fourshapes are refused and named:
unknown-token-name,unclosed-opener,brace-in-payload,nested-opener. The refusal prints the offending spanclipped 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-stampdetail, which used to inline a raw multi-linedump 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
maskQuotedStampsconsumed 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 twosentences 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 oneprotocolStampsmatch andnothing 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-runand the live write take the same refusal, because both run throughrenderBody. The DRY RUN line, the read-back delta note, the--ack-throughrule and the footer policy are untouched.
Verification
SELF_TEST_BATTERY_FLOORraised 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 (35cases) — 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.
checked out over the file, its presence proved by
git hash-objectagainstfe0ae5c1:scripts/pm/post-stamped.mjs(andgrep -c unrecognisedOpeners→ 0),the fixture run, then
git checkout HEAD --restored andgit diff HEADproved empty. Both readings are quoted above.
node scripts/pm/dispatch-gates.mjs --commandsexit 0,plus
node scripts/pm/post-stamped.mjs --self-test,node scripts/check-self-test-wired.mjsand ESLint on the file.scripts/pm/**is not in any package'sfiles[].Acceptance notes
Noted, not filed — nothing in this PR's path touches them and no queued card
covers the file:
YYYY-MM-DDThh:mmZ, while the shape actually accepted isPROTOCOL_STAMP_RE, whose seconds group is optional. The seconds grain isdeliberate 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
stampSpanreturnsnullfor it andstampIsFuturereadsnullas "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