Two related gaps found by the 2026-09-07 review. Accepted on 2026-09-08, unchanged since, no issue until now.
1. An operator action is two writes with no transaction
backend-ts/src/case/case-actions.ts records the event (:138) and then applies the patch (:150). A failure between them leaves an action recorded but not applied.
The direction is deliberate and should stay: it can never produce an unaudited state change, which is the hard rule in CLAUDE.md. What is missing is the primitive — there is no applyCaseAction({ patch, action, audit }) that makes the three one unit.
2. The run pipeline is the other way round, and CAN lose the event
This is the part the review's framing understated and the follow-up assessment corrected. run-pipeline.ts upserts the case and then best-effort audits it, catching an audit-insert failure and logging a run WARN rather than aborting. That ordering is also deliberate — it exists so a transient audit_events failure cannot strand an otherwise-complete run as RUNNING after the case was already mutated.
But it means the system-wide claim is false as stated. Carry the narrower true statement:
"Every state change writes an audit_event" holds for operator actions. It does not hold for the run-created case transition, which can mutate and lose the event.
That belongs in DATA_MODEL_CONTRACTS §4 next to the best-effort note, rather than being discoverable only by reading the pipeline.
What was decided about the fix
A reconciliation job was proposed and qualified: it is not magic — it needs durable operation identity, an expected version, a deadline, and a visible failure state, or it is a second unreliable thing checking the first. Before any real operational reliance, the state change, the action record and the audit row should be one transaction, including the run-created case transition.
Partial progress already exists and is worth building on rather than around: recordCaseEvents is a batch atomic dual-write, added for bulk assign and reused for panel backfill. Bulk assign deliberately audits before it mutates — the same side of the trade as the single-action path.
Related: ADR-076, ADR-080, CLAUDE.md hard rules.
Two related gaps found by the 2026-09-07 review. Accepted on 2026-09-08, unchanged since, no issue until now.
1. An operator action is two writes with no transaction
backend-ts/src/case/case-actions.tsrecords the event (:138) and then applies the patch (:150). A failure between them leaves an action recorded but not applied.The direction is deliberate and should stay: it can never produce an unaudited state change, which is the hard rule in CLAUDE.md. What is missing is the primitive — there is no
applyCaseAction({ patch, action, audit })that makes the three one unit.2. The run pipeline is the other way round, and CAN lose the event
This is the part the review's framing understated and the follow-up assessment corrected.
run-pipeline.tsupserts the case and then best-effort audits it, catching an audit-insert failure and logging a runWARNrather than aborting. That ordering is also deliberate — it exists so a transientaudit_eventsfailure cannot strand an otherwise-complete run as RUNNING after the case was already mutated.But it means the system-wide claim is false as stated. Carry the narrower true statement:
That belongs in
DATA_MODEL_CONTRACTS§4 next to the best-effort note, rather than being discoverable only by reading the pipeline.What was decided about the fix
A reconciliation job was proposed and qualified: it is not magic — it needs durable operation identity, an expected version, a deadline, and a visible failure state, or it is a second unreliable thing checking the first. Before any real operational reliance, the state change, the action record and the audit row should be one transaction, including the run-created case transition.
Partial progress already exists and is worth building on rather than around:
recordCaseEventsis a batch atomic dual-write, added for bulk assign and reused for panel backfill. Bulk assign deliberately audits before it mutates — the same side of the trade as the single-action path.Related: ADR-076, ADR-080, CLAUDE.md hard rules.