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
BankRuleSetsare 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
BankRuleSetswhen 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/RuleCompileris the single validation seam: it returns a typed result (valid + compiled regex, or invalid with a reason) and never throws. It verifies the requiredamountgroup 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.