ADR-0019: Starter-rule re-seeding for existing installs
- Status: Accepted
- Date: 2026-08-13
- Deciders: maintainers
- Related: ADR-0004
Context
Starter parse rules live in BankRuleSets and are seeded into the rules
table on first launch (ADR-0004). Rules shipped in a later release therefore
never reached existing installs — the table was only populated when empty, and
there is deliberately no static fallback. When mae_payment_of and
tng_transferred_to_you were added (issue #20) they had to reach users who
already had a populated rules table.
Decision
RuleRepository.seedDefaults() seeds on first launch, then reconciles once
per seed version:
- Each
ParserRulecarries a stableid(e.g.mae_payment_of) that is now persisted asrules.sourceId(nullable, unique index, added in schema v5). - On an existing install with a stored
rules_seed_versionbelowRULE_SEED_VERSION, the repository: - backfills
sourceIdonto legacy rows by matchinglabel(a one-time migration of seed identity), and - inserts any starter rule whose
sourceIdis absent — this is how new rules reach existing installs. - The reconcile writes only with in-place
UPDATE(backfill) andINSERT(new rules) — neverREPLACE— so thetransactions.ruleId → rules.id ON DELETE SET NULLFK is never triggered and rule provenance (ADR-0002) survives. - The version stamp is stored in the
settingstable (rules_seed_version). Deleting a starter rule in the editor records itssourceIdin a durable tombstone list (deleted_rule_source_ids); the reconcile skips every tombstoned id, so a later version bump cannot revive a rule the user deleted (issue #316). A missing starter with no tombstone (for example a legacy row that never got asourceId) is still restored.
Amendment (seed version 8, ADR-0039)
The currency alternation inside BankRuleSets patterns is a shipped string in
the rules table, so an expanded currency catalog (ADR-0039) would not reach
existing installs. On the once-per-version reconcile, RuleRepository also
performs a targeted substring swap: any seeded row (sourceId non-null)
whose pattern contains the exact prior alternation has that substring replaced
with Currency.amountAlternation(), then is written with UPDATE. A pattern
without that exact substring — a user's own edits, or a user-created rule — is
left untouched. Future catalog expansions repeat this by adding the previous
alternation literal and bumping RULE_SEED_VERSION.
Consequences
- New starter rules reach existing installs on the next launch after upgrade.
- User-edited rules are never overwritten; user-deleted starter rules are never re-added (once the version is stamped).
toParserRule()now derives its id fromsourceId ?: label, so provenance and the seed identity stay stable across the label-vs-id distinction.- Adding rules requires bumping
RULE_SEED_VERSION; forgetting to bump means the new rules only reach fresh installs (the old behavior).
Alternatives considered
- Reconcile by
labelonly, no schema change — rejected: labels are human-readable and editable-adjacent;sourceIdis the stable key and also fixes the label-as-id smell. - Reconcile unconditionally on every launch — rejected: would re-add user-deleted starter rules; the version stamp bounds it to actual releases.