Planning bug: a card generalised the side-effects-outside-the-transaction rule onto the AUDIT ROW, which is the one write that must be inside it
Found by: the motir run MOTIR-3808 parent run, 2026-08-28, while executing child MOTIR-3813 (the to-do store) on branch parent/MOTIR-3808-work-item-todo-list, base origin/main @ ae490d52e.
Sibling instance in the same story: MOTIR-3820. Two cards of one seven-card plan asserted a rung-2 fact that shipped code contradicts. That recurrence is why this is a second card rather than a comment on the first.
The defect
MOTIR-3813's body instructs, under Service — where the two contended writes are locked:
"Side effects run OUTSIDE the transaction. The revision entry for a structural change (add / edit / delete — per the ADR, NOT for a tick) is written after the DB write commits; a failure there is logged and degrades, and must not roll back or fail the to-do write."
That is not implementable against the shipped API, and it inverts a documented contract.
Evidence
lib/services/workItemRevisionsService.ts takes a REQUIRED Prisma.TransactionClient, and its own header says the parameter IS the contract:
"
recordRevisionto persist ONE audit row describing the mutation, INSIDE the same transaction as the mutation itself. The requiredtxparameter is the contract that enforces this: a revision commits atomically with the mutation it describes, or neither does. … The atomicity integration test (tests/integration/work-items/revisions.test.ts) exercises exactly this rollback."
Every call site agrees — git grep -n "recordRevision(" -- 'lib/**' returns ~20 hits across attachmentsService, backlogService, commentsService, componentsService, customFieldValuesService, estimationService, labelsService and plansService, and every one is inside a $transaction. There is no post-commit form to call.
The root cause — a real rule applied to the wrong noun
motir-core/CLAUDE.md's side-effects-outside-the-transaction rule is about NOTIFICATIONS — sendEvent, email, anything with an outside observer. commentsService.addComment is the worked example: the work-item/comment.created event fires after the transaction commits, commented "Post-commit, never inside the tx — a rollback must not have notified."
The card generalised that onto a revision row, and the two cases are opposite:
| write | must not… | because |
|---|---|---|
| a notification | fire for a write that ROLLED BACK | somebody outside the system already believes it happened |
| an audit row | SURVIVE a write that rolled back | the trail would describe a change that never landed |
So "side effect" was doing the work of a category, and the two members of that category want opposite placements. A run following the card literally would have had to widen recordRevision's signature or skip the revision entirely.
Disposition applied in this run
The revision is written inside the transaction, per the shipped contract. lib/services/workItemTodosService.ts carries the divergence and its reasoning in a header block so the next reader of the card is not left to re-derive it, and MOTIR-3813 is amended on the record.
Acceptance criteria
lib/services/workItemTodosService.tswrites every structural revision inside the same transaction as the write it describes, throughworkItemRevisionsService.recordRevision. (Landed in this run — closing this is a read.)- MOTIR-3813's "Side effects run OUTSIDE the transaction" paragraph is corrected on the record and cites this bug. (Landed in this run — closing this is a read.)
- No product code beyond (1).
Comments (0)
No comments yet — be the first to weigh in.