Three claims were stale: it said l12 has no facing on the shelf (facings #46-48
now carry it), that the file uses sku_l01-sku_l11, and that the set is 61
in-catalog / 48 unknown across 11 SKUs. It is 64 / 45 across 12.
The #46-48 note it pointed at as an open question is now answered, so the note
reference goes with it.
Facings #46-48 carried the note 'possibly what stock.csv calls l12'. The l12
reconciliation settled it: the shelf tag under them reads ユーラス 野菜を
おいしく食べる旨タレ (380g) at ¥299, the price the l12 row carries, and the row
has been renamed to match. Leaving them unknown understated the catalog and
overstated precision.
Counts move from 61 in-catalog / 48 unknown to 64 / 45. The enrollment ceiling
falls 87% -> 83% because l12's three facings now count as in-catalog with no
enrolled view - an honest catalog gap, not a recognition regression.
Also records the Tier-3 source search: two reusable clips (CC BY 3.0 Wikimedia
1920x1080 supermarket pan; CC BY YouTube 360p mayonnaise-bay pan), 11 rejected
candidates, and the LOPIA walk that carries the standard YouTube licence and
was therefore not downloaded. Both clips are labelled tier3_proxy_candidate,
never Tier 3: that still needs the operator's own recorded walks.
Replaces the DINO gallery as the recognition path with the measured design
from docs/research/2026-09-28-recognition-architecture/DECISION.md: RootSIFT
over a per-SKU reference pyramid, similarity-constrained RANSAC and a hue
check, fused with an OCR text lane that vetoes a match whose package text
names a different catalog row and answers on its own when geometry finds
nothing.
- identity_lanes.py: LocalLane, TextLane, DecisionPolicy. The text lane
builds each row's signature from the catalog name plus its packshot OCR, so
a row with no enrolled image is still answerable.
- engine.py: every backend groups scores by canonical SKU, so no
view-suffixed id reaches the wire and a name and price always resolve.
decide_crop fuses the lanes; score_crop_lanes feeds the live path. Both
lanes are built in warm_up (non-fatally) so the first frame is not the slow
one, and the text lane shares the engine's OCR lock.
- live.py: decides per crop on the lane's own rank order. The lane's order
comes from its rank key and its value is only the gate, so re-sorting by
value named a different SKU than the lane chose; averaging in an empty
observation also diluted a good confidence below the accept line.
- identity.py: a `local` score scale whose gate is one absolute line, because
the value is already a margin over the runner-up.
- app.py, run-demo-server.sh: `--identity local` is the default; the demo
server no longer starts on the leaked `stock-lopia` catalog.
- eval_live.py: the replay vocabulary accepts `local`, so a recording from
the shipped server can be scored.
- README.md: documents the three backends and what each is for.
Measured on the corrected image6 labels at live resolution: 36/61 in-catalog
facings named at 95% precision, 0 of 48 unknowns accepted, 11 labels sourced
from the text lane alone. Through the real server: /v1/recognize 90%
precision, /v1/stream 100% precision, no view-suffixed ids on either.