docs(spec): correct the suppliers migration's expected merge count

19 -> ~16 was wrong. Ran the actual normalizeSupplierName() (trim +
whitespace-collapse + uppercase) against the three example pairs cited
as evidence: only the case-variant pair (Builders Warehouse) actually
collapses. The Ikhasi pair keeps a stray space after a hyphen that
differs between variants; the Global Precast pair differs by the
"(Pty) Ltd" suffix, not case or whitespace. Neither collapses under
exact-normalized matching.

This is not a defect -- it's the conservative-matching principle in
section 3 working as designed. The correct expectation against
production is 19 raw strings -> 18 groups, one merge. Both pairs that
don't collapse are exactly the kind of ambiguous case the spec already
says should stay separate for a human to resolve, not the kind that
should be forced together by loosening the normalizer.

Caught while grounding the implementation plan in real data, before
writing a single line of the plan itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DEls 2026-09-11 18:02:18 +02:00
parent 861cb25f0f
commit c73a2aad6c

View file

@ -21,7 +21,7 @@
`purchase_orders.supplier` is free text, written once from whatever OCR
extracted off the PO document. Production, checked live on 2026-09-11:
**24 purchase orders, 19 distinct raw supplier strings**, of which at
least three are the same company under a formatting variant:
least three *look* like the same company under a formatting variant:
```
"Builders Warehouse" / "BUILDERS WAREHOUSE"
@ -38,6 +38,24 @@ how much I owe each supplier."* A supplier split across two spellings
under-reports what's owed to them by however much sits on the
mis-spelled half.
**Correction, caught while writing the implementation plan:** running
the actual `normalizeSupplierName()` defined in §3 against these three
pairs shows only the first one — the case variant — actually collapses.
`"Ikhasi- Lo-Mkhulu (COD)"` keeps a space after its first hyphen that
`"Ikhasi-Lo-Mkhulu (COD)"` doesn't, so trim+whitespace-collapse+uppercase
produces two different strings. `"Global Precast"` and `"Global Precast
(Pty) Ltd"` differ by more than case or whitespace — the legal suffix —
so they don't collapse either. **The honest result against production
data is 19 raw strings → 18 groups, one merge, not the "~16" figure
originally written here.** This is not a defect in the normalization —
it is the normalization working exactly as designed in §3: conservative
matching that refuses to guess. The Ikhasi and Global Precast pairs stay
as separate `suppliers` records after migration, exactly like the Vula
pair, and the operator merges them by hand with full context if they
are in fact the same company. Do not "fix" this by adding
punctuation/suffix-stripping to the normalizer — that is the fuzzy
matching §3 already explains why to avoid.
---
## 2. Schema
@ -147,8 +165,9 @@ messages, idempotent).
Report the offending PO ids and stop; a human resolves them, same as
every guard pattern from the ingestion restructure.
Expected outcome against today's data: 19 raw strings → ~16 supplier
records (3 exact-normalized collapses), 24 POs all linked.
Expected outcome against today's data: 19 raw strings → 18 supplier
records (1 exact-normalized collapse — the "Builders Warehouse" case
variant; see the correction in §1), 24 POs all linked.
---
@ -246,7 +265,7 @@ required by this spec.
| `normalizeSupplierName` | Case, whitespace, and punctuation variants collapse to the same key; genuinely different names do not. |
| `matchSupplier` | Exact match links; no match returns `null` + flag; empty/whitespace input never creates a phantom match. |
| `computeSupplierLedger` | Full delivery → outstanding 0. Partial delivery → correct partial value. Over-delivery → negative, not clamped. Unmatched delivery line → counts toward nothing (mirrors `po-fulfilment.test.ts`'s equivalent case). Multiple POs for one supplier → totals sum correctly. |
| Migration | Dry-run reports the exact grouping (19 → ~16 given today's data) without writing. Guard fires and aborts on any empty-supplier PO. Idempotent: re-running after a successful apply is a no-op. |
| Migration | Dry-run reports the exact grouping (19 → 18 given today's data) without writing. Guard fires and aborts on any empty-supplier PO. Idempotent: re-running after a successful apply is a no-op. |
---