Try the most specific rule first, not the topmost
File order decided precedence, so a narrow rule had to be written above the broad one it carves an exception out of -- an ordering constraint the file cannot show and the user has to remember. *NIKOLA* below *NIK* silently matched nothing, and a catch-all * could only ever be the last line. Engine.New now sorts once and MatchIndex walks that order: most literal characters first, then fewest *, then account-scoped over unscoped. Literals are what a rule commits to and a * is what it gives up, so a bare * is tried last wherever it sits. The sort is stable, so equally specific rules keep file order and the earlier one wins -- which is all position decides now, and why AppendRule can keep appending without displacing a rule written by hand. The two orders must not be confused: Rules(), Usage and MatchIndex still speak in file positions, because that is what the rules screen numbers and what DeleteRules deletes by. A shadowed rule still reports zero usage, but a zero no longer says anything about where the rule sits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -124,10 +124,23 @@ milliseconds. That is the checksum skip working, not a failure.
|
||||
**Money is `int64` minor units**, never a float. Per-account currency, no
|
||||
conversion, and totals are never summed across currencies.
|
||||
|
||||
**First matching rule wins**, so specific rules belong above general ones.
|
||||
A rule setting both `match` and `type` requires both of them.
|
||||
`config.AppendRule` therefore appends — never prepends — so saving from the
|
||||
rule builder cannot shadow a rule the user wrote by hand.
|
||||
**The most specific rule wins, not the topmost.** `rules.Engine` sorts the
|
||||
rules once in `New` and matches in that order: most literal characters first,
|
||||
then fewest `*`, then account-scoped over unscoped, with a *stable* sort so
|
||||
equally specific rules keep file order and the earlier one still wins. That is
|
||||
what lets `*NIKOLA*` carve an exception out of `*NIK*` from anywhere in the
|
||||
file, and a catch-all `*` sit wherever it reads best. A rule setting both
|
||||
`match` and `type` requires both of them, and both count towards its literals.
|
||||
|
||||
The two orders must not be confused. `Engine.rules` stays in file order and
|
||||
`Rules()`, `Usage` and `MatchIndex` all speak in file positions, because that
|
||||
is what the rules screen numbers, what `config.DeleteRules` deletes by, and
|
||||
what the user can point at in rules.toml; only `Engine.order` is sorted.
|
||||
Anything new that reports a rule must report its file position too.
|
||||
|
||||
`config.AppendRule` still appends rather than prepends, but that now only
|
||||
settles ties: a saved rule cannot displace an equally specific one written by
|
||||
hand, while a narrower one is meant to take precedence and does.
|
||||
|
||||
**The builders are forms, so the global keymap must not apply there.**
|
||||
`Update` routes to `updateRules` / `updateTransfers` before `updateNormal`
|
||||
@@ -145,8 +158,8 @@ field navigation, and lets `ctrl+n` / `ctrl+p` through to the input for cycling.
|
||||
it afterwards or `ctrl+n` would offer candidates that no longer fit the value.
|
||||
|
||||
**A rule's usage count is how many transactions it wins, not how many its glob
|
||||
could match** — `Engine.Usage` counts by `MatchIndex`, so a rule shadowed by an
|
||||
earlier one correctly reports zero. That is what makes the rules screen able to
|
||||
could match** — `Engine.Usage` counts by `MatchIndex`, so a rule shadowed by a
|
||||
more specific one correctly reports zero. That is what makes the rules screen able to
|
||||
find dead rules at all.
|
||||
|
||||
**A rule's `note` is documentation that round-trips.** It is a TOML key rather
|
||||
|
||||
Reference in New Issue
Block a user