fix(audit): alias learning is not a rule change, stop logging it (BFNAR noise) (#2134)
Production falsified 20260901103000's exclusion list within 30 minutes of deploy: 15 of the first 16 UPDATE audit rows on categorization_templates changed only the learning columns plus counterparty_aliases, because the learning path (lib/bookkeeping/counterparty-templates.ts) merges new aliases in the same write that bumps occurrence_count. Projected ~800 noise rows/day against ~50/day of real rule changes, each one rendered into the legally-facing behandlingshistorik as "Konteringsmall aendrad: Alias". counterparty_aliases joins the trigger's strip list. The trade-off is explicit: a human editing ONLY aliases is no longer logged. Accepted because alias growth is overwhelmingly automatic, and a change that also touches accounts, VAT, pattern or the active flag still logs: the first real such row (2026-09-01 19:02:17Z, debit/credit/vat accounts changed by the learning loop, BFN's automatkontering case exactly) was captured correctly and stays captured under the new WHEN clause. The pre-fix noise rows stay in audit_log (append-only). The read model stops labelling the column, so alias-only diffs, historical ones included, render as no-ops rather than rule changes; a diff that also carries a real change shows only the real change. pg-test extended: alias+learning update writes no audit row, alias+account update still does. Read-model test pins the pre-fix noise row shape to null. Claude-Session: https://claude.ai/code/session_01L3P2hr19PhQuCoTSGoegcY Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Opus 5
parent
53c9c0c4f9
commit
191fb2cfce
@@ -708,15 +708,18 @@ describe('auditRowToEvent: behandlingsregler', () => {
|
||||
})
|
||||
|
||||
it('categorization_templates: the learning columns never reach the report', () => {
|
||||
// The DB trigger filters these already (migration 20260901103000); the read
|
||||
// model must not resurrect them if a row slips through, or every booking
|
||||
// would appear as a system change.
|
||||
// The DB trigger filters these already (20260901103000 + 20260901200000);
|
||||
// the read model must not resurrect them if a row slips through, or every
|
||||
// booking would appear as a system change. counterparty_aliases counts as
|
||||
// learning: prod's first 30 minutes of trigger rows were 15/16 alias
|
||||
// noise, and those pre-fix rows are still in audit_log and must render as
|
||||
// no-ops.
|
||||
const learning = auditRowToEvent(
|
||||
auditRow({
|
||||
table_name: 'categorization_templates',
|
||||
action: 'UPDATE',
|
||||
old_state: { counterparty_name: 'Spotify AB', debit_account: '6540', occurrence_count: 4, confidence: 0.7 },
|
||||
new_state: { counterparty_name: 'Spotify AB', debit_account: '6540', occurrence_count: 5, confidence: 0.9 },
|
||||
old_state: { counterparty_name: 'Spotify AB', debit_account: '6540', occurrence_count: 4, confidence: 0.7, counterparty_aliases: ['SPOTIFY'] },
|
||||
new_state: { counterparty_name: 'Spotify AB', debit_account: '6540', occurrence_count: 5, confidence: 0.9, counterparty_aliases: ['SPOTIFY', 'SPOTIFY STOCKHOLM 4711'] },
|
||||
}),
|
||||
)
|
||||
expect(learning).toBeNull()
|
||||
|
||||
@@ -413,7 +413,10 @@ const MAPPING_RULE_FIELDS: Record<string, string> = {
|
||||
|
||||
const CATEGORIZATION_TEMPLATE_FIELDS: Record<string, string> = {
|
||||
counterparty_name: 'Motpart',
|
||||
counterparty_aliases: 'Alias',
|
||||
// counterparty_aliases deliberately absent: aliases grow in the same
|
||||
// learning write as occurrence_count (migration 20260901200000 stopped
|
||||
// logging them), and the pre-fix audit rows already in prod are alias-only
|
||||
// noise that must render as no-ops, not as rule changes.
|
||||
debit_account: 'Debetkonto',
|
||||
credit_account: 'Kreditkonto',
|
||||
vat_treatment: 'Momshantering',
|
||||
|
||||
Reference in New Issue
Block a user