[object Object]

← 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

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 →