[object Object]

← back to Dw Photo Capture

observability: add /api/stats; backlog O/0 OCR fallback with evidence (cycle 19)

e3483ef8aacb60e838b56393d707fc3fa8d6f6a7 · 2026-06-26 08:20:13 -0700 · Steve Abrams

Investigated the OCR O-vs-0 / I-vs-1 normalization idea empirically: the ambiguity is REAL
(of 1033 sample catalog codes, 122 contain a letter-O and 47 contain both O and 0, e.g.
SNPBOBJML11I3, BMGI006). But a recovery fallback touches the hot resolve path and carries a
false-match risk that can't be tested headlessly (resolve hits live endpoints), so I
BACKLOGGED it with the evidence + a safe design (exact-first, variant-only-on-miss, capped)
rather than ship speculative hot-path complexity.

Shipped the clean additive win instead: GET /api/stats — a read-only observability snapshot
for the self-teaching scanner (version, vendor count, seeded, fingerprinted, total learned
scans, last-learn timestamp, recents, catalog indexed, vision model). Auth-gated, in-memory,
zero hot-path risk. Verified: 200 with creds / 401 without; lint + 51 tests green. $0 (local).

Files touched

Diff

commit e3483ef8aacb60e838b56393d707fc3fa8d6f6a7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jun 26 08:20:13 2026 -0700

    observability: add /api/stats; backlog O/0 OCR fallback with evidence (cycle 19)
    
    Investigated the OCR O-vs-0 / I-vs-1 normalization idea empirically: the ambiguity is REAL
    (of 1033 sample catalog codes, 122 contain a letter-O and 47 contain both O and 0, e.g.
    SNPBOBJML11I3, BMGI006). But a recovery fallback touches the hot resolve path and carries a
    false-match risk that can't be tested headlessly (resolve hits live endpoints), so I
    BACKLOGGED it with the evidence + a safe design (exact-first, variant-only-on-miss, capped)
    rather than ship speculative hot-path complexity.
    
    Shipped the clean additive win instead: GET /api/stats — a read-only observability snapshot
    for the self-teaching scanner (version, vendor count, seeded, fingerprinted, total learned
    scans, last-learn timestamp, recents, catalog indexed, vision model). Auth-gated, in-memory,
    zero hot-path risk. Verified: 200 with creds / 401 without; lint + 51 tests green. $0 (local).
---
 OVERNIGHT_LEDGER.md |  2 ++
 server.js           | 20 ++++++++++++++++++++
 2 files changed, 22 insertions(+)

diff --git a/OVERNIGHT_LEDGER.md b/OVERNIGHT_LEDGER.md
index 75d3dc3..1473050 100644
--- a/OVERNIGHT_LEDGER.md
+++ b/OVERNIGHT_LEDGER.md
@@ -13,6 +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.
 - [ ] 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)
@@ -64,3 +65,4 @@ the single highest-value safe one, the officer executes it, and the loop resched
 ### Deferred known-minor (cycle 17, low value)
 - 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 |
diff --git a/server.js b/server.js
index c608e88..42cb92a 100644
--- a/server.js
+++ b/server.js
@@ -586,6 +586,26 @@ const appHandler = (req, res) => {
       .sort((a, b) => (b.learned_scans || 0) - (a.learned_scans || 0) || (b.n || 0) - (a.n || 0));
     return send(res, 200, { total: vendors.length, seeded: VENDOR_LEX.map(v => v.name), vendors });
   }
+  // Observability snapshot for the self-teaching scanner — read-only, in-memory. $0.
+  if (u.pathname === '/api/stats' && req.method === 'GET') {
+    const profs = Object.values(VENDOR_PROFILES);
+    let lastLearn = null, learnTotal = 0;
+    for (const p of profs) {
+      learnTotal += (p.learned_scans || 0);
+      if (p.learned_scans > 0 && p.updated_at && (!lastLearn || p.updated_at > lastLearn)) lastLearn = p.updated_at;
+    }
+    return send(res, 200, {
+      version: buildLabel(),
+      vendors: profs.length,
+      seeded: VENDOR_LEX.length,
+      fingerprinted: profs.filter(p => p.logo_valid === true).length,
+      learned_scans_total: learnTotal,
+      last_learn: lastLearn,
+      recents: Object.keys(recents).length,
+      catalog_indexed: CATALOG.length,
+      vision_model: OLLAMA_VISION_MODEL,
+    });
+  }
   // Refine a vendor's profile from a scan that resolved to a real product. Conservative:
   // we only fold in structured signal (the on-swatch code prefix), never raw OCR prose.
   if (u.pathname === '/api/learn' && req.method === 'POST') {

← c3b1b21 auto-save: 2026-06-26T08:13:39 (2 files) — data/build.json d  ·  back to Dw Photo Capture  ·  test: reject O/0 OCR fallback (real collisions) + lock verba eb1e546 →