From c73a2aad6c8bc99745871b43d1c4aadd06995bfe Mon Sep 17 00:00:00 2001 From: DEls Date: Fri, 11 Sep 2026 18:02:18 +0200 Subject: [PATCH] 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 --- .../specs/2026-09-11-suppliers-design.md | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/superpowers/specs/2026-09-11-suppliers-design.md b/docs/superpowers/specs/2026-09-11-suppliers-design.md index 59702a1..05d421f 100644 --- a/docs/superpowers/specs/2026-09-11-suppliers-design.md +++ b/docs/superpowers/specs/2026-09-11-suppliers-design.md @@ -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. | ---