docs(plan): per-sale expiry tracking design — batch registry + candidate matching

Grilled 2026-07-16 with the user; full decision record in
docs/expiry-tracking-plan.md. Core reframe: expiry is captured once per
batch at DO intake (staff-typed on the stock-entry confirmation page, from
the physical packs), so the cashier scan only MATCHES OCR fragments against
the 1-3 known in-stock batch dates instead of free-reading damaged
dot-matrix prints (proven model-capability ceiling, 2026-07-15). Fallback:
auto-FEFO + 'inferred' flag, zero cashier interaction. No cloud, ever.

- docs/expiry-tracking-plan.md: architecture, matching algorithm spec
  (resolveExpiryFromEvidence), schema/API deltas, phases 1-3, testing plan
- backend plans §13 (13.1-13.4): matcher util + offline tuning, route
  wiring + expiry_source provenance, multi-frame union, dot-matrix
  recognizer fine-tune
- root plans §10 (10.1-10.3): cashier fast path, inferred badge +
  end-of-day review, burst capture for mounted camera
- stock-feature-plan.md: extension note (batch dropdown becomes the
  manual-override path)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8TumxFDnyVnfsR3mxPXfX
This commit is contained in:
Rafhan Mazaya FathurrahmanandClaude Fable 5 committed 2026-07-16 17:00:36 +07:00
1 parent 721dea41dc
commit e6daa9b053
7 files changed
+350 -333

No files matched your search

+55
View File
@@ -450,6 +450,61 @@ implemented** — no code for this feature exists in the codebase yet.
*Suggested order: 12.1 → 12.2 (needs 12.1's tables/movement helper) and 12.3
(needs 12.1's summary route) — 12.2/12.3 are independent of each other.*
## 13. Backend — Per-Sale Expiry Resolution (Candidate Matching)
`src/utils/expiry-matcher.ts`, `src/app/api/parse/route.ts`,
`src/app/api/v1/scan-product/route.ts`, `src/app/api/v1/documents/`,
`config/classify_ocr_server.py`
Added 2026-07-16 from a user-directed grilling session (ad-hoc feature per
`AGENTS.md` Part B7, like §12). **Read
[../../docs/expiry-tracking-plan.md](../../docs/expiry-tracking-plan.md) first**
— full design, confirmed decisions (full automation at cashier, no cloud ever,
auto-FEFO + `inferred` flag as the only fallback), matching algorithm spec, and
phase plan. Core idea: expiry is captured once per batch at DO intake
(staff-typed on the stock-entry page, §12/root §9), so the cashier scan only has
to **match** OCR fragments against 1–3 known candidate dates — never free-read a
damaged dot-matrix print under time pressure. **Blocked on 12.1 + 12.2** (needs
`stock_batches` + the in-stock candidate filter). Flutter counterpart: root
`plans/next-enhancements.md` §10.
- **13.1** [TODO] **`resolveExpiryFromEvidence()` matcher util + offline tuning
harness.** Pure TS util implementing the 4-stage resolution
(`single_batch` → `matched_exact` → `matched_fragment` → `inferred_fefo`)
with digit-confusion-aware fuzzy scoring of candidate date print-forms (and
batch codes) against the scan's OCR `text_lines`; thresholds
(`SCORE_MIN`/`MARGIN_MIN`) tuned offline by replaying the 79 frozen-benchmark
line-sets against synthetic candidate sets built from ground-truth labels —
tune for **zero wrong-candidate picks** (flagged FEFO beats a confident wrong
match). Includes verifying the classify server response actually carries
`text_lines` to the gateway (add to payload if not — small
`classify_ocr_server.py` change). Unit tests + harness script committed.
- **13.2** [TODO] **Wire resolution into both scan routes + persist
provenance.** `documents.expiry_source VARCHAR(20)` CHECK
(`single_batch|matched_exact|matched_fragment|inferred_fefo|manual`) +
`expiry_match_score REAL NULL`; `parse/route.ts` Product branch and
`v1/scan-product/route.ts` call the matcher after the §12.2 in-stock filter
and include `resolvedBatch`/`source`/`score` in `metadata.productScan` and
the response (no second GPU call — same pattern as 11.1); PUT persists
`expiry_source` (client override ⇒ `'manual'`); documents list gains an
`?expiry_source=` filter for the end-of-day review of `inferred_fefo` sales.
Blocked on 13.1.
- **13.3** [TODO] **Phase 2 — multi-frame evidence union.** Accept burst
uploads (N frames per scan) on the product-scan path; classify on the best
frame, union OCR `text_lines` across all frames before matching (glare moves
between frames — fragments accumulate). Pairs with a mounted camera at the
cashier (root §10.3). Blocked on 13.2.
- **13.4** [TODO] **Phase 3 — on-prem dot-matrix recognizer.** Synthetic
dot-matrix/inkjet date-crop generator (dot dropout, scratch, fade, curvature,
glare augmentation) + real-data flywheel (harvest scan crops weakly labeled
by the batch registry's staff-typed expiry values); fine-tune a small rec
model on the RTX 2060; deploy as an additional reader in
`classify_ocr_server.py` feeding the same matcher. Goal: shrink the
`inferred_fefo` residue. **No cloud — hard constraint.** Blocked on 13.2;
independent of 13.3.
*Suggested order: 13.1 → 13.2 → (13.3 and/or 13.4 as needed once Phase-1
accuracy is measured in the field).*
---
*Sections 1-4 migrated 2026-07-08 from root `plans/next-enhancements.md` sections