The default backend could not be scored by the repo's own evaluator, so the
number that justifies the architecture lived only in an ad-hoc script.
- eval_identity.py: `--lanes local` wraps identity_lanes.LocalLane and
TextLane behind the same four ceilings as every other lane. `Lane.decide`
is now the seam: the default runs the shared gate over the lane's ranking,
and the fused local+text lane overrides it, so the production decision is
what gets scored rather than a re-implementation of it.
- rebuild_catalog.py: one-directional image/CSV match (an enrolled image with
no row refuses; a row with no image is a text-only coverage gap), packshot
OCR against the catalog name, capture-session refusal from an evaluation
manifest, and the 160 px floor. The CSV is copied byte-for-byte so all 12
rows survive a rebuild, and unmanaged files are carried over.
Reproduced from the repo, live resolution: 59% end-to-end at 95% precision,
2 false accepts of 48 unknowns, 20.6 ms per crop.
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.