MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-3821

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

Done
Description

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:

"recordRevision to persist ONE audit row describing the mutation, INSIDE the same transaction as the mutation itself. The required tx parameter 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 NOTIFICATIONSsendEvent, 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:

writemust not…because
a notificationfire for a write that ROLLED BACKsomebody outside the system already believes it happened
an audit rowSURVIVE a write that rolled backthe 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

  1. lib/services/workItemTodosService.ts writes every structural revision inside the same transaction as the write it describes, through workItemRevisionsService.recordRevision. (Landed in this run — closing this is a read.)
  2. 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.)
  3. No product code beyond (1).