wallet: the deterministic parse must beat the model's location
ci / lint-test (push) Successful in 46s

Found by reading the backfilled rows rather than by a test. The precedence
was subtitle -> MODEL -> descriptor, so the model's answer won wherever it
had one, which is always. That defeats the entire point of parsing the
descriptor positionally: the ledger got "SAN FRANCISOUS" for the only
foreign merchant in the capture window, plus the padded "MELBOURNE   AU",
and the bare "Sydney"/"Perth" with their country codes dropped.

Correct order is subtitle -> deterministic descriptor -> model. After the
fix those rows read SAN FRANCISO US, MELBOURNE AU, Sydney AU, Perth AU,
and a subtitle-derived place still wins where one exists.

The test that should have caught this passed because its fixture omitted
`location`, so the fallback chain was never exercised — every real
interpretation carries one. Fixture now includes the model's actual
mangled answer for that row, so the test fails if the order regresses.

Re-ran the backfill on the corrected code: 21 transactions, $6,284.37
unchanged, 13 with a location. The 21 previously-written rows were deleted
first rather than superseded, which is normally forbidden on transactions
— checked before doing it that they carried zero splits, tags and
overrides, being machine-written minutes earlier by this same lane with
nothing yet attached.
This commit is contained in:
2026-08-22 16:33:56 +10:00
parent 8d1875225f
commit 3d0d75519a
2 changed files with 14 additions and 3 deletions
@@ -223,6 +223,11 @@ describe("location", () => {
await ingest(req({ await ingest(req({
...breadworldBank, event_id: 75, amount: 42.44, ...breadworldBank, event_id: 75, amount: 42.44,
merchant: "SP PEAKDESIGN", merchant: "SP PEAKDESIGN",
// The model's own answer for this row, included because every real
// interpretation carries one -- omitting it is what let a wrong
// precedence order pass this test the first time, and ship
// "SAN FRANCISOUS" into the ledger.
location: "SAN FRANCISOUS",
notif_body: "$42.44 paid from Mastercard ending...8032. SP PEAKDESIGN SAN FRANCISOUS", notif_body: "$42.44 paid from Mastercard ending...8032. SP PEAKDESIGN SAN FRANCISOUS",
})); }));
const [t] = await txns(); const [t] = await txns();
+9 -3
View File
@@ -139,9 +139,15 @@ export async function processWalletIngestion(ev: WalletEvent): Promise<WalletIng
// second must be able to REPLACE a descriptor-derived place, or the result // second must be able to REPLACE a descriptor-derived place, or the result
// depends on arrival order. // depends on arrival order.
const subtitleLocation = sub.location ?? null; const subtitleLocation = sub.location ?? null;
const location = subtitleLocation // Precedence: the Wallet subtitle, then the DETERMINISTIC descriptor parse,
?? ev.location // and only then the model's own answer. Getting this order wrong defeats the
?? (desc.city && desc.country ? `${desc.city} ${desc.country}` : desc.city); // point of parsing positionally at all -- the model returned the mangled
// "SAN FRANCISOUS" and the padded "MELBOURNE AU" for exactly these rows,
// which is what wallet-descriptor.ts exists to correct.
const descriptorLocation = desc.city && desc.country
? `${desc.city} ${desc.country}`
: desc.city;
const location = subtitleLocation ?? descriptorLocation ?? ev.location ?? null;
// Currency: only ever what the notification actually states. A bare "$" is // Currency: only ever what the notification actually states. A bare "$" is
// recorded as unknown, never assumed AUD -- harmless domestically, a 23x // recorded as unknown, never assumed AUD -- harmless domestically, a 23x