Do not stamp last_update_at on the shared-payload bulk path - #92
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Reviewed — merging. The guard is the thing, and it is built the right way round.I asked for a guard pinned against the real
Requiring the Two ablation legs, both from the committed state so the restore was a real recovery point: injecting a bulk Red-first on the hook itself was the card's own batch, and the failure message is the card's own sentence: Both boundaries checked rather than assumed, which is what I asked for and is easy to skip: Gates, re-run by me on the head: Generated by Claude Code |
Fixes #78
Implements the decision in the triage comment: on the per-row dispatch path
(
ctx.dispatch.mode === 'per-row'),task.hook.tsdoes not stamplast_update_atat all. Same detection as #39's guard, opposite response —#39 refuses because a wrong
completed_atcorrupts a historical fact, whilehere the honest answer is to write nothing rather than write one row's truth
onto all of them.
The defect, reproduced before the change
The batch the card describes — one row that genuinely changes its note, one
row that already holds that exact note — measured against the booted engine on
0557a57, hook unchanged:Four of the six new assertions were red before the three-line change and all
six are green after.
Why writing nothing is safe, not merely convenient
Stated in the hook's module header, because it is only safe while it stays
true. Stagnation is defined over open work:
duly_stagnationfiltersstatus IN ('open','in_progress')on every measure, and the "Not moving" lensdoes the same. The two bulk actions this product ships —
complete(
status: 'done') andskip(status: 'skipped') — move every row they touchout of that set, so a row leaving a bulk write with a stale clock is one no
stagnation query will evaluate again. "Bulk completion would look like
stagnation" describes a state that cannot occur.
The real deliverable:
test/bulk-stagnation-premise.test.tsThat premise is a fact about
bulkActionDefs, not about the hook, and a bulk"set note" or bulk reassign would falsify it silently — the symptom is a frozen
clock, not an error. The guard reads the real metadata on both sides rather
than restating it:
duly_stagnation's own measure filtersand out of the lens's own filter, found structurally (any
duly_taskviewfiltering on
last_update_at), and the two readings are required to agree. Aset hand-copied into the test would have kept agreeing with itself after
someone widened the real one. The guard then uses their union, which is the
fail-safe direction if they ever diverge.
dulyViews, filtered to views bound toduly_task, so a def added on any view is inspected without editing thisfile. Each must be an
operation: 'update'with a literalstatusin itspatch, that status must be a declaredduly_taskoption, it must beoutside the stagnation set, and no
parammay putstatusback in thecaller's hands (params merge over the static patch). A def with no
statusfails: its rows keep the status they had.
src/views/task.view.ts— a rename that madetaskBulkDefs()return nothingwould otherwise leave the guard passing on an empty list.
Ablation — the guard was proven able to fail, both legs mutation-confirmed
on disk and restored by an
EXIT INT TERMtrap:set notedef, nostatusinpatchstatus: 'in_progress'status: 'done'×1→0,status: 'in_progress'×0→1Five failures per leg because the shared
bulkActionsarray is offered on fiveviews. Tree restored byte-identically afterwards (
git diff HEADempty).Both boundaries held, checked rather than assumed
dispatch.mode === 'per-row', not on the event, somode: 'record'keepsthe row-conditional stamp. Pinned by a new assertion that a by-id note edit
still advances the clock — it is what would go red if the skip were written
as "never stamp on update".
mode: 'update'backdating pass still works.test/seed-history.test.tsrun explicitly: 45 passed alongsidetask-actions. It depends onbeforeUpdaterefusing to stamp, and thischange can only make the hook stamp less, never more.
No changeset: this repo has no changeset mechanism. The four gates are the
whole contract.
Gates
All four green on
69c3c8e, the final commit, in one run:The one warning is the documented, expected
hierarchy-securitycapabilitynotice that AGENTS.md says not to silence.
Generated by Claude Code
Generated by Claude Code