Skip to content

ADR-0004: Per-notification rule loading

  • Status: Accepted
  • Date: 2026-08-11
  • Deciders: maintainers
  • Related: ADR-0002

Context

Rules are editable in-app (Settings → Categories & automation → Parse rules). The parser must honor edits (enable/disable, pattern changes, deletes) immediately — cached rule sets would go stale.

Decision

Rules are loaded per notification from the rules table (RuleDao.enabledForPackage) inside the ingestion module. The parser is not cached.

A package with no enabled rules never parses — there is no fallback to the static BankRuleSets.

Consequences

  • Every in-app rule edit takes effect on the next notification. No cache invalidation problem.
  • Cost: one DB read per notification. Negligible for notification volume (a handful per day).
  • The static BankRuleSets are seeds only; they stop being authoritative once the table has data.

Alternatives considered

  • Loading rules once at startup and invalidating on edit — rejected: invalidation is error-prone and the volume doesn't justify the cache.
  • Falling back to BankRuleSets when a package has no rules — rejected: would silently resurrect rules the user deleted, contradicting the editor.

Amendment (2026-10-19): pattern validity is a per-row property

Rules stay DB-owned and loaded per notification (no static fallback), but a corrupt row is no longer fatal for the package:

  • parser/RuleCompiler is the single validation seam: it returns a typed result (valid + compiled regex, or invalid with a reason) and never throws. It verifies the required amount group from platform group metadata, not from pattern text, so quoted/classed/comment text cannot fake it.
  • Rule loading skips an invalid stored rule while other valid enabled rules for the same package still match; the editor shows the invalid row as needing repair. The editor keeps a draft with explicit Save/Discard, never persists incomplete typing, and reports invalid syntax from Test actions instead of throwing.

Amendment (2026-10-31): compile cache, DB still authoritative

enabledForPackage still reads the enabled rows from the DB on every notification, so edits, toggles and deletions take effect immediately. Only the compiled form is cached: a ConcurrentHashMap<Long, CompiledRule> keyed by row id and invalidated by a content signature (pattern, named groups, kinds, priority, label/source id). A rebind or 48 h replay no longer recompiles every rule (issue #325). updatePattern, setEnabled, upsert and delete clear the entry explicitly as well, so an edit can never serve a stale regex. This is a cache of a deterministic function of the row, not a second rule source; the static fallback remains forbidden.