← back to Dw Photo Capture
test: reject O/0 OCR fallback (real collisions) + lock verbatim-code contract (cycle 20)
eb1e5463019308000a13f6e62f37f5c62452e6a3 · 2026-06-26 08:43:02 -0700 · Steve Abrams
Ran the collision gate the backlog required before building the O/0+I/1 variant fallback:
checked 225,846 real catalog codes (mfr_index + profile examples) and found 14 collision
groups where genuinely DIFFERENT products are pure O/0 or I/1 variants of each other —
RI5112 vs R15112, MI725 vs M1725, etc. A blind variant fallback could therefore resolve a
scan to a real-but-WRONG product. REJECTED with evidence (saved shipping a subtly-wrong
feature).
The correct, safe behavior is to search exactly what OCR read — analyzeOcr preserves codes
verbatim with no silent O↔0 / I↔1 normalization. Locked that contract with 3 regression
tests (RI5112 and R15112 stay distinct; letter-O+I code unchanged). 51→54 tests, lint clean.
$0 (local).
Files touched
M OVERNIGHT_LEDGER.mdM test-analyze-ocr.js
Diff
commit eb1e5463019308000a13f6e62f37f5c62452e6a3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 26 08:43:02 2026 -0700
test: reject O/0 OCR fallback (real collisions) + lock verbatim-code contract (cycle 20)
Ran the collision gate the backlog required before building the O/0+I/1 variant fallback:
checked 225,846 real catalog codes (mfr_index + profile examples) and found 14 collision
groups where genuinely DIFFERENT products are pure O/0 or I/1 variants of each other —
RI5112 vs R15112, MI725 vs M1725, etc. A blind variant fallback could therefore resolve a
scan to a real-but-WRONG product. REJECTED with evidence (saved shipping a subtly-wrong
feature).
The correct, safe behavior is to search exactly what OCR read — analyzeOcr preserves codes
verbatim with no silent O↔0 / I↔1 normalization. Locked that contract with 3 regression
tests (RI5112 and R15112 stay distinct; letter-O+I code unchanged). 51→54 tests, lint clean.
$0 (local).
---
OVERNIGHT_LEDGER.md | 3 ++-
test-analyze-ocr.js | 11 +++++++++++
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/OVERNIGHT_LEDGER.md b/OVERNIGHT_LEDGER.md
index 1473050..1b28c33 100644
--- a/OVERNIGHT_LEDGER.md
+++ b/OVERNIGHT_LEDGER.md
@@ -13,7 +13,7 @@ explicit backlog empties, each cycle BRAINSTORMS fresh refinement ideas (local L
the single highest-value safe one, the officer executes it, and the loop reschedules — it does NOT stop.
## Backlog (safe / reversible unless flagged GATED)
-- [ ] OCR O/0 + I/1 variant fallback (cycle-19 evidence: 122/1033 codes contain letter-O, 47 contain both O+0 — ambiguity is REAL). SAFE design only: generate O↔0/I↔1 variants and try them ONLY after the exact code fails to resolve, never replacing the exact match; cap variants (≤8) to bound lookup calls; first check no two catalog codes are pure O/0-variants of each other (false-match risk). Touches scanResolve (hot path) — pure variant-generator is unit-testable; wiring needs care. Possibly /dtd the approach.
+- [REJECTED w/ evidence, cycle 20] OCR O/0+I/1 variant fallback: collision gate FAILED — 14 groups of DIFFERENT real catalog codes are pure O/0 or I/1 variants of each other (RI5112 vs R15112, MI725 vs M1725) across 225,846 codes. A variant fallback could resolve to a real-but-WRONG product. The safe contract (search verbatim, never normalize) is now locked by tests.
- [ ] GATED/Steve: Data volume ~100% full — review big consumers: ~/.ollama 45G (16 models, prune unused?), ~/Projects 83G (node_modules dupes?), ~250G in sudo-gated system/var areas needs Steve to inspect (`sudo du -xh -d1 /System/Volumes/Data | sort -rh | head`). Loop kept tripping ENOSPC.
- [x] A-verdict: tighten logo matcher + validate fingerprints + re-harvest (DTD 3/3)
- [x] Wire logo fingerprints into /api/identify as a TIEBREAKER (DTD-A, name-match primary)
@@ -66,3 +66,4 @@ the single highest-value safe one, the officer executes it, and the loop resched
- scanResolve runs ALL candidates through TWIL (pass 1) before ANY through Shopify (pass 2), so a weak candidate that hits TWIL beats a barcode that only resolves in Shopify. Narrow edge (barcode present + fails TWIL + weak code hits TWIL); the TWIL-first source preference is deliberate. Revisit only if it surfaces in real use.
| 18 | brainstorm #2 (ROBUSTNESS) | atomic saveJSON (temp-file + rename) — a crash/ENOSPC mid-write can no longer truncate vendor_profiles.json (180 learned profiles) or progress/recents/favorites; single shared-primitive fix protects all persistence | df 29G; proved via REAL saveJSON: forced write-failure left original intact, 0 stale temps; learn persists 200; lint+51 tests; selfcheck 200 | $0 |
| 19 | investigate O/0 (real, backlogged) + ship /api/stats | O/0/I/1 ambiguity CONFIRMED real (122 of 1033 codes have letter-O, 47 have both O+0) but touches hot resolve path w/ untestable false-match risk → BACKLOGGED w/ evidence; shipped additive /api/stats (version, vendors, fingerprinted, learn total, last-learn, model) — read-only, auth-gated, zero hot-path risk | df 27G; stats 200 + 401 no-creds; lint+51 tests; selfcheck implied | $0 |
+| 20 | collision gate → REJECT O/0 fallback + lock verbatim contract | checked 225,846 real codes: 14 collision groups where different products are pure O/0 or I/1 variants (RI5112≠R15112) → O/0 auto-fallback would mis-resolve → REJECTED w/ evidence; locked the safe behavior (analyzeOcr preserves codes verbatim, no silent O↔0/I↔1) with 3 tests | df 34G; 51→54 tests; lint clean | $0 |
diff --git a/test-analyze-ocr.js b/test-analyze-ocr.js
index 7955329..106e530 100644
--- a/test-analyze-ocr.js
+++ b/test-analyze-ocr.js
@@ -179,6 +179,17 @@ const bc = (payload, sym = 'Code128') => `BC\t${payload}\t${sym}`;
eq(fontWin.candidates[0], 'CD2222', 'order: larger font wins among generic codes');
}
+// ── codes are preserved VERBATIM — no silent O↔0 / I↔1 normalization (cycle 20) ──
+// The real catalog contains O/0- and I/1-distinct codes (e.g. RI5112 vs R15112 are DIFFERENT
+// products — 14 such collisions found across 225k codes), so the scanner must search exactly
+// what OCR read. An O/0 auto-normalization would resolve to the wrong product; this locks that
+// analyzeOcr never transforms a code's letters/digits.
+{
+ eq(analyzeOcr(parseOcrRows(line(100, 'RI5112'))).candidates, ['RI5112'], 'verbatim: letter-I code kept (not R15112)');
+ eq(analyzeOcr(parseOcrRows(line(100, 'R15112'))).candidates, ['R15112'], 'verbatim: digit-1 code kept (distinct from RI5112)');
+ eq(analyzeOcr(parseOcrRows(line(100, 'SNPBOBJML11I3'))).candidates, ['SNPBOBJML11I3'], 'verbatim: letter-O+I code unchanged');
+}
+
// ── fingerprintVendor edge cases (cycle 12 brainstorm: harden the tiebreaker contract) ──
{
eq(fingerprintVendor('', 'serif'), null, 'fp edge: empty read → null');
← e3483ef observability: add /api/stats; backlog O/0 OCR fallback with
·
back to Dw Photo Capture
·
auto-save: 2026-06-26T08:43:59 (1 files) — data/build.json 93cd3e4 →