Let manual corrections outrank the model's own precedent
History rows now carry a source label: manual edits and merchant rules are the user's decisions, ranked ahead of equally similar rows the model classified itself and guaranteed slots in a full history window. Without the distinction, precedent fed the model its own uncorrected answers as majority evidence, so a correction never won against the rows it was meant to fix. Both system prompts state that user entries outrank ai entries. Alias write-back on manual merchant links and the per-merchant usual category already learned locally; this closes the loop for categories and tags.
This commit is contained in:
@@ -2,6 +2,7 @@ package classification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -156,3 +157,49 @@ func TestBICRedactionKeepsPayeeVocabulary(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One manual correction must outrank any number of the model's own past
|
||||
// answers for the same payee: without source ranking, precedent feeds the
|
||||
// model its uncorrected output as majority evidence and corrections never
|
||||
// stick.
|
||||
func TestManualCorrectionsOutrankAIPrecedent(t *testing.T) {
|
||||
d, _ := ledgerFixture()
|
||||
groceries, events := "", ""
|
||||
for _, c := range d.Categories {
|
||||
if c.Name == "groceries" {
|
||||
groceries = c.ID
|
||||
}
|
||||
if c.Name == "events" {
|
||||
events = c.ID
|
||||
}
|
||||
}
|
||||
add := func(id, date, category, source string) {
|
||||
f := domain.Facts{ID: id, Source: "test", AccountID: "acct_kontist", BookingDate: date,
|
||||
Amount: "-13.00", Currency: "EUR", Counterparty: "LVR Landesmuseum Bonn", Fingerprint: id}
|
||||
d.Transactions = append(d.Transactions, domain.Transaction{Facts: f, Enrichment: domain.Enrichment{
|
||||
Kind: "expense", CategoryID: category, TagIDs: []string{},
|
||||
Classification: domain.Provenance{Source: source},
|
||||
}})
|
||||
}
|
||||
// Many uncorrected AI answers, one older manual correction.
|
||||
for i := range 30 {
|
||||
add(fmt.Sprintf("tx_ai_%02d", i), "2026-08-20", groceries, "openrouter")
|
||||
}
|
||||
add("tx_corrected", "2026-08-01", events, "manual")
|
||||
target := domain.Facts{ID: "tx_new", AccountID: "acct_kontist", BookingDate: "2026-08-30",
|
||||
Amount: "-13.00", Currency: "EUR", Counterparty: "LVR Landesmuseum Bonn"}
|
||||
rows := history(target, d, func(s string) string { return normalize(s) }, 20)
|
||||
if len(rows) == 0 || rows[0].Source != "user" || rows[0].CategoryID != events {
|
||||
t.Fatalf("manual correction did not lead precedent: %+v", rows[0])
|
||||
}
|
||||
// The correction keeps its slot even in a window the AI rows could fill.
|
||||
users := 0
|
||||
for _, row := range rows {
|
||||
if row.Source == "user" {
|
||||
users++
|
||||
}
|
||||
}
|
||||
if users == 0 {
|
||||
t.Fatal("correction crowded out of the history window")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user