ci / lint-test (push) Successful in 57s
`transaction_audit` records every INSERT/UPDATE/DELETE on transactions, transaction_splits, transaction_tags, transaction_overrides and expense_metadata, via AFTER row triggers. WHY: on 2026-08-23 the n8n CC Statement Processor stamped `statement_id` onto seven statementless wallet/pantry rows without setting `reconciled_with_id`, double-counting $245.90 and hiding those rows from the reconcile queue (`WHERE statement_id IS NULL`). Answering "was anything lost?" required restoring the 02:30 pg_dump into a scratch database and diffing it column by column, because nothing in the estate recorded what changed. Trigger rather than application code because the ledger has at least five independent writers -- this app (Prisma), n8n (raw SQL), pantry-app, the ingestion runner (psycopg) and ad-hoc `docker exec psql`. That bug was an n8n raw-SQL UPDATE; an app-level audit would not have seen it. Not Postgres' own logs: wal_level=replica, wal_keep_size=0, archive_mode=off, log_statement=none, track_commit_timestamp=off, no pgaudit -- verified live. Postgres was recording nothing usable, and has no built-in audit tables. UPDATEs store only the columns that changed (a whole-row copy buries a one-column edit, and line_items can be kilobytes); no-op UPDATEs write nothing; txid groups the rows of one logical edit, which matters because applyReconcileMatch moves four children in a single transaction. No FK to transactions -- the log must outlive the row. TRUNCATE does not fire row triggers, so the test suite's wipes are silent. Rehearsed against a scratch restore first: changed-column-only diffing, no-op suppression, composite PK (transaction_tags), and the matched_transaction_id fallback (a card-settled order has only that) all verified before applying. Applied to personal and personal_test. Board #259.