Commit 8fa3fe6
pm(post-stamped): refuse a quoted stamp later than the clock the writing act holds (#17773)
Fixes #17763
`scripts/pm/post-stamped.mjs` validated only the SHAPE of a
`{{WAS:...}}` value, never its direction
in time, so a stamp no act can have read passed the refusal set and was
rendered verbatim. That is
the estimated stamp the tool exists to make unspellable, still spellable
through the one escape the
tool offers by name in its own refusal text — and the recorded failures
it was built against were
forward-skewed, which is exactly the half a shape check cannot see.
## The repro, before and after
Measured on this branch's base `aaacf1d5c` (the card measured it on
`813f8e9f`; it reproduces
unchanged):
```
$ node --input-type=module -e "
import { stampRefusals, renderBody } from './scripts/pm/post-stamped.mjs';
const NOW = Date.parse('2026-09-12T03:00Z');
const body = 'Verdict {{NOW}} — on the board read {{WAS:2099-01-01T00:00Z}}.';
console.log('refusals:', JSON.stringify(stampRefusals(body)));
console.log(renderBody(body, NOW).body);
"
refusals: []
Verdict 2026-09-12T03:00Z — on the board read 2099-01-01T00:00Z.
```
After, at `8c806fce4`, the same body against the same clock:
```
refusals: [{"kind":"quoted-in-the-future","detail":"`{{WAS:2099-01-01T00:00Z}}` declares an instant
LATER than the clock this act holds (`2026-09-12T03:00Z`). ... Write `{{NOW}}` if it is this act's
own clock, or correct the value to the instant that was actually read. There is no skew tolerance
here, forward: a stamp the clock has not reached is not nearly a reading."}]
ok: false kind: stamp-contract
```
End to end through the CLI, same tree: the future body exits `2`
(`EXIT_REFUSED`) and writes
nothing; the same body with a past quoted stamp exits `0` and still
renders it verbatim
(`Verdict 2026-09-12T03:44Z — on the board read 2026-09-08T14:00Z.`).
## What changed
- `stampRefusals(text, nowMs = Date.now())` now takes the clock the
WRITING act holds — the same one
`renderBody` substitutes, passed straight through, never a second read
taken later. The CLI
contract and the `{{NOW}}` route are untouched.
- A well-shaped quoted value later than that clock joins the existing
refusal register as
`quoted-in-the-future`. A shape failure is not additionally filed as a
direction failure: one typo,
one refusal.
- The positional and mixed refusals hand the seat
`{{WAS:THE-TYPED-STAMP}}` with the stamp filled
in. When that stamp is in the future they now say the quoted route is
not open to it, instead of
prescribing a remedy this same change refuses. Both texts keep the old
offer for a past stamp, and
both controls are pinned.
## The boundary, stated
A stamp names the SPAN of its own grain — `stampSpan`, imported from
`check-half-states.mjs` rather
than re-derived, because two spellings of "which minute is this" would
let the write side and the
read-side patrol disagree about the edge. A value is a possible reading
exactly while that span has
STARTED:
- **equal to the act's own minute: ACCEPTED.** A reading taken at any
instant inside `06:37` is
spelled `06:37Z`, and the minute's start is at or before any clock
reading within it.
- **the first minute the clock has not reached: REFUSED.** Acceptance
opens ON the tick — a clock
standing exactly at `06:37:00.000` accepts `06:37Z`, one millisecond
earlier does not. Both sides
of that tick are pinned.
- **no clock-skew tolerance, and `H56_STAMP_TOLERANCE_MIN` is not one.**
That number measures how
far a WRITE may land after the read it carries — a backward gap on the
read side. Spending it
forward here would reopen a quarter-hour window on the very direction
the recorded failures took.
A stamp 14 minutes ahead — inside H56's drift tolerance — is refused,
and that case is pinned
beside the constant.
## Tests
`--self-test` gains a declared battery, `the direction check: a stamp no
act can have read`, floored
at 22 in `SELF_TEST_BATTERIES`; `SELF_TEST_BATTERY_FLOOR` moves 6 to 7
so a dropped section still
names itself. `pnpm check:pm-post-stamped` at `8c806fce4`:
`✓ post-stamped self-test: 90 cases pass across 7 batteries — offline,
no network, no token.`
The battery contributes 22 of those 90.
Reverse verification — three ablations, each mutating the committed
file, each proving the bytes
reached disk (`git hash-object` against the HEAD blob) and each restored
with
`git checkout HEAD -- PATH` proven by an empty `git diff HEAD` and a
matching blob hash:
| mutation | expected | observed |
|:---|:---|:---|
| `span.from > nowMs` to `false && …` (delete the predicate) | RED |
exit 1, 8 named cases fail, incl. the filed repro |
| `>` to `>=` (move the edge off the tick) | RED | exit 1, the
on-the-tick acceptance fails |
| `span.from` to `span.to` (require the whole span to be past) | RED |
exit 1, 4 named cases fail, incl. the equal-to-now boundary |
The second and third legs are why the tick cases exist: the first
version of the battery pinned the
minute but not the instant, and the `>=` ablation stayed green through
it. Restored self-test after
each leg: `exit 0`, 90 cases.
Gate sweep — `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`
derives 32 families for the one path in this diff; all 32 run, all `exit
0`, reconciled with
`--ran`: `✓ dispatch-gates --ran: 32 derived famil(ies) accounted for —
32 run, 0 NOT-MEASURED (a
DERIVED zero — all 32 recorded an exit code and none of them is 3)`.
Exit codes were captured before
any pipe. The derivation's own caveats stand and are not claimed as
cleared here: the 50
artifact-roster families, the 11 wide-population families, the 14
changeset-derived families, the
one path-scheduled CI job and the always-runs tail are each outside that
total and belong to CI.
## Scope and acceptance notes
- One path: `scripts/pm/post-stamped.mjs`. `skip-changeset` —
`scripts/pm/**` publishes nothing from
any released package (it is on the fast-track list, not in any package's
`files[]`).
- The mixed refusal's text was corrected alongside the positional one
because THIS change makes its
offer false: it hands back `{{WAS:A-FUTURE-STAMP}}` as a remedy the same
commit refuses. Leaving
it would have landed a tool arguing with itself. Both of its branches
are pinned, future and past.
- The new battery's assertions are optional-chained and compared against
an explicit `true`/`false`.
The first ablation showed the original spelling dying on a `TypeError`
before the battery floor
ever ran — exit 1 either way, but a stack trace names no case and
silences every section after it,
which is what the floor exists to prevent.
- `check-half-states.mjs`'s H56 read side was NOT touched, per the
dispatch. Noted, not filed: H56
has no direction rule of its own, but it does not need the same one — it
measures a stamp against
the platform's own `created_at`..`updated_at` write window (ground
truth, not the writer's clock),
and `stampDriftMinutes` is unsigned, so a forward-skewed stamp beyond
the 15-minute tolerance is
already reported. What it tolerates is 15 minutes in either direction,
deliberately and documented
as such. Carrier: the next card touching
`scripts/pm/check-half-states.mjs`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a24eaee commit 8fa3fe6
1 file changed
Lines changed: 133 additions & 22 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
50 | 53 | | |
51 | 54 | | |
52 | 55 | | |
| |||
56 | 59 | | |
57 | 60 | | |
58 | 61 | | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
59 | 91 | | |
60 | 92 | | |
61 | 93 | | |
| |||
98 | 130 | | |
99 | 131 | | |
100 | 132 | | |
| 133 | + | |
101 | 134 | | |
102 | 135 | | |
103 | 136 | | |
| |||
158 | 191 | | |
159 | 192 | | |
160 | 193 | | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
161 | 211 | | |
162 | 212 | | |
163 | 213 | | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
164 | 218 | | |
165 | | - | |
| 219 | + | |
166 | 220 | | |
167 | 221 | | |
168 | 222 | | |
| 223 | + | |
169 | 224 | | |
170 | 225 | | |
171 | | - | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
172 | 237 | | |
173 | | - | |
| 238 | + | |
174 | 239 | | |
175 | | - | |
176 | | - | |
177 | | - | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
178 | 246 | | |
179 | 247 | | |
180 | 248 | | |
181 | 249 | | |
182 | 250 | | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
183 | 255 | | |
184 | 256 | | |
185 | | - | |
186 | | - | |
187 | | - | |
188 | | - | |
189 | | - | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
190 | 263 | | |
191 | 264 | | |
192 | 265 | | |
| |||
195 | 268 | | |
196 | 269 | | |
197 | 270 | | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
198 | 274 | | |
199 | 275 | | |
200 | | - | |
201 | | - | |
202 | | - | |
203 | | - | |
204 | | - | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
205 | 283 | | |
206 | 284 | | |
207 | 285 | | |
| |||
216 | 294 | | |
217 | 295 | | |
218 | 296 | | |
219 | | - | |
220 | | - | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
221 | 300 | | |
222 | 301 | | |
223 | 302 | | |
| |||
236 | 315 | | |
237 | 316 | | |
238 | 317 | | |
239 | | - | |
| 318 | + | |
240 | 319 | | |
241 | 320 | | |
242 | 321 | | |
| |||
438 | 517 | | |
439 | 518 | | |
440 | 519 | | |
441 | | - | |
| 520 | + | |
| 521 | + | |
442 | 522 | | |
443 | 523 | | |
444 | 524 | | |
| |||
550 | 630 | | |
551 | 631 | | |
552 | 632 | | |
| 633 | + | |
553 | 634 | | |
554 | 635 | | |
555 | 636 | | |
556 | 637 | | |
557 | 638 | | |
558 | | - | |
| 639 | + | |
559 | 640 | | |
560 | 641 | | |
561 | 642 | | |
| |||
574 | 655 | | |
575 | 656 | | |
576 | 657 | | |
577 | | - | |
| 658 | + | |
578 | 659 | | |
579 | 660 | | |
580 | 661 | | |
| |||
610 | 691 | | |
611 | 692 | | |
612 | 693 | | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
613 | 724 | | |
614 | 725 | | |
615 | 726 | | |
| |||
0 commit comments