{"verdict":"needs-attention","summary":"Do not ship: failure recovery can duplicate accepted emails or permanently block all remaining reminders.","findings":[{"severity":"high","title":"Make email delivery idempotent across persistence failures","body":"Email acceptance precedes the transaction recording delivery. If that transaction fails, the queue retries the still-queued milestone and sends it again. Fault injection reproduced two accepted emails with reminders_sent equal to 1, violating R3, R8 and R10. The database primary key cannot deduplicate the external send.","file":"src/jobs/reminders.ts","line_start":111,"line_end":122,"confidence":1,"recommendation":"Persist a stable delivery key per invoice and milestone, pass it to a mailer supporting provider-side idempotency, and reuse it on every retry. Add a test where acceptance succeeds but the subsequent database transaction fails."},{"severity":"high","title":"Recover abandoned running jobs before deduplicating reminders","body":"If the process exits after a reminder job is claimed, that job remains running indefinitely: JobQueue has no lease or restart recovery. This enqueue call then always deduplicates against it, blocking both the interrupted milestone and every later milestone for that invoice. Simulating an abandoned running job left milestones 7 and 14 queued with zero runnable jobs after another sweep.","file":"src/jobs/reminders.ts","line_start":24,"line_end":25,"confidence":1,"recommendation":"Add recoverable job leases or safe startup recovery for abandoned running jobs, coordinated with delivery idempotency. Test restarting after a claim and verify that remaining milestones eventually send."}],"next_steps":["Implement delivery idempotency and abandoned-job recovery, then test both failure scenarios."]}