← back to Dw Photo Capture
test: harden analyzeOcr against adversarial OCR inputs (cycle 10, brainstorm)
2c6e05260d419d08471fa4d3b16c683acc98ac44 · 2026-06-26 02:04:25 -0700 · Steve Abrams
First brainstorm-mode cycle. A local qwen3:8b proposed 5 refinements; triaged — 2 were
already shipped (prefix-boost, fast-lock), 2 deferred to backlog (UX confidence overlay,
Swift grayscale pre-pass), and the test-coverage one was the safe high-value pick.
Added 11 regression guards locking the ranking contract under degraded/adversarial OCR:
3-char + no-digit tokens excluded; dashed code is a candidate but not strong; <4-char
barcode ignored; two brand names → first lexicon match wins (deterministic); lowercase
brand still detected; duplicate code de-duped; two barcodes → both candidates, barcode
forces topStrong. All behaviors pre-verified correct first (no bug found — pure hardening).
21→32 tests, lint clean. $0 (local LLM brainstorm + local tests).
Files touched
M OVERNIGHT_LEDGER.mdM test-analyze-ocr.js
Diff
commit 2c6e05260d419d08471fa4d3b16c683acc98ac44
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 26 02:04:25 2026 -0700
test: harden analyzeOcr against adversarial OCR inputs (cycle 10, brainstorm)
First brainstorm-mode cycle. A local qwen3:8b proposed 5 refinements; triaged — 2 were
already shipped (prefix-boost, fast-lock), 2 deferred to backlog (UX confidence overlay,
Swift grayscale pre-pass), and the test-coverage one was the safe high-value pick.
Added 11 regression guards locking the ranking contract under degraded/adversarial OCR:
3-char + no-digit tokens excluded; dashed code is a candidate but not strong; <4-char
barcode ignored; two brand names → first lexicon match wins (deterministic); lowercase
brand still detected; duplicate code de-duped; two barcodes → both candidates, barcode
forces topStrong. All behaviors pre-verified correct first (no bug found — pure hardening).
21→32 tests, lint clean. $0 (local LLM brainstorm + local tests).
---
OVERNIGHT_LEDGER.md | 7 +++++++
test-analyze-ocr.js | 29 +++++++++++++++++++++++++++++
2 files changed, 36 insertions(+)
diff --git a/OVERNIGHT_LEDGER.md b/OVERNIGHT_LEDGER.md
index 8dd0373..0d6881b 100644
--- a/OVERNIGHT_LEDGER.md
+++ b/OVERNIGHT_LEDGER.md
@@ -23,6 +23,12 @@ the single highest-value safe one, the officer executes it, and the loop resched
- [x] /code-review pass — NO critical; 3 robustness/security findings fixed (learn cap, fp len-guard, auth colon)
- [x] GATED (drafted to pending-approval): weekly launchd refresh — plist staged
+### Brainstorm pool (cycle 10, qwen3:8b $0) — triaged
+- [done-already] SKU prefix boost (180 vendor profiles) · early-exit on confident OCR (topStrong fast-lock)
+- [ ] UX: surface OCR confidence on the live overlay (topStrong vs tentative) — marginal, no per-token score yet
+- [ ] Robustness: grayscale/contrast pre-pass in bin/ocr.swift for low-light labels — needs Swift recompile + A/B test
+- [x] Test-coverage: adversarial/degraded OCR inputs against analyzeOcr (cycle 10)
+
## Cycle log
| # | decision (DTD?) | action | result | cost |
|---|-----------------|--------|--------|------|
@@ -40,3 +46,4 @@ the single highest-value safe one, the officer executes it, and the loop resched
### Code-review MINORs deferred (latent, low-risk, not fixed this cycle)
- OLLAMA_URL declared after ollamaVision def (works — only called in handlers; moving it risks colliding with the concurrent voice-feature session that owns that const).
- bgIdentify may teach a prefix from an unresolved OCR code (intentional fire-and-forget; vendor must already be real via lexicon/fuzzy; low impact).
+| 10 | brainstorm (qwen3:8b $0) → pick #5 (no fork) | harden analyzeOcr ranking contract with 11 adversarial/degraded-input tests (3-char/no-digit/dashed codes, <4 barcode, two-brand order, lowercase brand, dup dedup, two barcodes) | 21→32 tests pass; lint clean; no behavior change (all pre-verified correct) | $0 |
diff --git a/test-analyze-ocr.js b/test-analyze-ocr.js
index 800537b..25e7713 100644
--- a/test-analyze-ocr.js
+++ b/test-analyze-ocr.js
@@ -119,5 +119,34 @@ const bc = (payload, sym = 'Code128') => `BC\t${payload}\t${sym}`;
eq(fingerprintVendor('totally unknown brand xyz', 'serif'), null, 'fingerprintVendor: unknown → null');
}
+// ── adversarial / degraded OCR inputs (cycle 10 brainstorm: harden the ranking contract) ──
+{
+ // a 3-char code is below the min length → not a candidate
+ eq(analyzeOcr(parseOcrRows(line(100, 'AB1'))).top, null, 'edge: 3-char code excluded');
+ // a token with no digit (marketing word) is never a candidate
+ eq(analyzeOcr(parseOcrRows(line(100, 'HELLO'))).top, null, 'edge: no-digit token excluded');
+ // a dashed alnum code is a valid candidate (but not strong without brand/known prefix)
+ const dash = analyzeOcr(parseOcrRows(line(100, '5255-16')));
+ eq(dash.top, '5255-16', 'edge: dashed code is a candidate');
+ ok(dash.topStrong === false, 'edge: dashed code with no brand/prefix is not strong');
+ // a barcode payload shorter than 4 chars is ignored (not ground truth)
+ const shortBc = analyzeOcr(parseOcrRows(bc('AB')));
+ eq(shortBc.barcode, null, 'edge: <4-char barcode ignored');
+ eq(shortBc.top, null, 'edge: <4-char barcode is not a candidate');
+ // two brand names on the swatch → the first in lexicon order wins (deterministic)
+ eq(analyzeOcr(parseOcrRows([line(40, 'THIBAUT'), line(40, 'SCHUMACHER'), line(120, '9010')].join('\n'))).vendor,
+ 'Thibaut', 'edge: two brands → first lexicon match wins');
+ // brand text in lowercase still detected (matching is case-insensitive)
+ eq(analyzeOcr(parseOcrRows([line(40, 'thibaut'), line(120, '9010')].join('\n'))).vendor,
+ 'Thibaut', 'edge: lowercase brand detected');
+ // the same code seen on two rows is de-duplicated to one candidate
+ eq(analyzeOcr(parseOcrRows([line(100, 'AB1234'), line(50, 'AB1234')].join('\n'))).candidates,
+ ['AB1234'], 'edge: duplicate code de-duped');
+ // two barcodes → both are candidates, the first is top, and a barcode forces topStrong
+ const twoBc = analyzeOcr(parseOcrRows([bc('GRS-111'), bc('GRS-222')].join('\n')));
+ eq(twoBc.candidates, ['GRS-111', 'GRS-222'], 'edge: both barcodes are candidates');
+ ok(twoBc.topStrong === true, 'edge: a barcode forces topStrong even with two present');
+}
+
console.log(`\nanalyzeOcr/vendor tests: ${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);
← f6b8dec review: fix 3 findings from session code-review (cycle 9)
·
back to Dw Photo Capture
·
auto-save: 2026-06-26T02:08:48 (2 files) — data/build.json d 367d125 →