The F row's veto numbers were measured on v1 labels, which the audit itself
found to be 23% wrong. They are kept as the record of what was found, marked as
audit-time, and pointed at the current measurement rather than left to read as
present-day results. The OCR cost figure was re-measured: 18.0 ms/crop on 40
production-shaped crops, inside the recorded band.
The two clips were staged but never exercised. One frame per second through the
production detector and the shipped local lane:
- The 1080p supermarket pan: 227 crops, 0 accepted. Correct open-set behaviour -
the frame is German pickle jars, not catalog products.
- The 360p mayonnaise bay: 1623 crops, 24 accepted (1.5%), across seven SKUs.
The clip visibly contains Kewpie products, so those are not automatically
false accepts, but at 640x360 the labels are too soft for per-SKU ground
truth.
Recorded as an accept-rate probe with false_accepts and accuracy left null
rather than estimated. Tier 3 still needs an operator-recorded walk.
A set total hides which products the lane can and cannot name. Every SKU in
these sets has images from two independent source domains, so a SKU that scores
the same on both is not one lucky photograph.
The breakdown is what makes the two interesting results visible: l01, l02, l03,
l07 and l09 are 2/2 correct on both their domains, l07 with no enrolled image
at all, while l11 abstains on both and l04 abstains on all four of its images.
Six enrolled views predate ATTRIBUTION.json and their source URLs were never
recorded. They are not wrong - each is independent of the evaluation photo
(highest template match 0.166 against add/image6.jpg) and each is above the
enrollment floor - but the provenance is gone, and provenance is what lets a
later audit tell a re-sourced packshot from a leaked shelf crop.
rebuild_catalog now names them instead of leaving the gap to be rediscovered,
and ATTRIBUTION.json records both the l06 replacement and the gap itself, with
what is verifiable about those six.
Reported, not refused: refusing would drop six working references to punish a
missing field.
The ceilings table, the stale-reference line and the metadata-integrity
paragraph all described the audit-time state as current. Each now says what was
found and what changed: the catalog ceiling is 53/64 = 83% (l10 and l06
enrolled, l12's facings labelled), the l06 package and l09 view are replaced,
and every stock.csv defect named there is corrected.
The OCR cost figure was re-measured rather than assumed: 18.0 ms/crop on 40
production-shaped crops, inside the recorded 18-26 ms band.
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.
E-3 asks what the text lane is worth, and the record answered it with numbers
that no committed command could produce - the evaluator had no way to run the
local lane without the text lane. --no-text is that way, and it keeps the
comparison to one variable by building the lane either way, so the OCR cost is
paid in both arms.
Measured: full resolution 77% end-to-end at 91% precision with text against 61%
at 66% without (5 false accepts against 20); live 62% at 93% against 56% at 84%
(3 against 7). The lane adds 16 pp and 6 pp of coverage while raising precision,
against E-3's +5 pp bar.
Also refreshes the re-measured block above it, which still printed the
pre-correction 61/48 label counts and a stray 'HUB!' marker.
19 scored rather than 20: sku_l06_1.jpg is now the enrolled l06 view, so it is
reported as leaked instead of as a held-out correct accept. 12 correct,
7 abstain, 0 false accepts.
Both leak checks are named in the record and in the external README, since the
enrollment that closed F-12 is exactly the kind of change that silently
converts a held-out row into training data.
Enrolling a packshot from an external set is normal - it is how F-12 was
closed - and it silently turns one of those rows into training data. The
external sets were scored by an ad-hoc script that never checked, so
sku_l06_1.jpg was being reported as a held-out correct accept while being
byte-identical to the enrolled view.
code/eval_external.py now scores those sets with the same leaked() check the
shelf evaluator runs, and refuses a row that has since been enrolled.
That check needed fixing first. Its 64x64 signature fires on two different
photographs of the same product: the l09 rear label from a retailer page sits
19.5 from the enrolled front packshot, under the 20.0 line, while being a
different photograph (resized MAD 26.3). A signature hit is now confirmed by
correlation - a genuine re-cut, re-encode or rescale correlates at 0.99+, that
pair at 0.28 - so the two signals together are decisive where either alone is
not. Verified against both directions: identical, q55 re-encode and 700px
rescale are caught; the different-photo pair is not.
Also adds the van backend (always-argmax, no gate) so the stream comparison in
DECISION.md is reproducible rather than asserted, with two tests pinning that
it answers where the shipped gate abstains and still refuses an empty score set.
External result on the corrected basis: 19 scored, 12 correct, 7 abstain,
0 false accepts, 1 leak excluded. Suite 307 -> 309.
Twenty independent-source images across eight SKUs, leak-tested (max template
match against add/image6.jpg is 0.675) and scored through the shipped lane:
13 correct, 7 abstain, 0 false accepts. The seven abstentions are all
unenrolled or weakly-read SKUs.
The result worth keeping is sku_l07: no enrolled image, both images accepted on
text alone. That is the text lane's coverage claim holding on data it had no
part in building.
F-12 is marked fixed with its measured effect, and the packshot-OCR check that
would have caught it is recorded as implemented but not yet trustworthy - it is
a false negative on l04 and l07.
The enrolled sku_l06.jpg showed the superseded 内臓脂肪ケア wrapper. The shelf
and the current retail package both read 内臓脂肪を減らす キューピーフィッテ
210g, so the reference image was of a design the store no longer sells - the
catalog was pointing at the wrong package.
F-12 was recorded as blocked on a capture because Kewpie's own site serves the
old design for that JAN. An independent retailer listing carries the current
one, and it matches the shelf facings exactly (verified by eye against a crop
of add/image6.jpg before enrolling).
Measured effect on the corrected labels: live decision 60% -> 64%, end-to-end
59% -> 62%, full-resolution decision 81% -> 83%. Suite still 307.
Also lands 12 more external-proxy images (six SKUs, two source domains each)
and their scored result. Across both external sets - 20 images - the lane is
13 correct, 7 abstain, 0 false accepts.
Two visually verified non-enrollment images per SKU for sku_l01, l02, l03,
l05, l06 and l11, sourced from Amazon retailer galleries and Yahoo
retailer/customer listings. sku_l06 uses the current 内臓脂肪を減らす package
(F-12); three customer listings showing the superseded wrapper were rejected.
Collection scratch files are removed.
Five blocks still printed pre-correction figures. Each was re-run through the
evaluator and replaced with what it actually outputs now:
- The lane comparison: local 75% at 91% against Van 64% at 38% (full), and
59% at 93% against 66% at 39% (live). At live resolution Van now answers more
facings correctly and is wrong on 67 of them to the local lane's 3 - stated
plainly rather than left as a win on both axes.
- The accept sweep: 75/67/67/62% end-to-end at 91/91/93/95% precision, with the
evaluator's own column definitions spelled out, since my first re-derivation
used the wrong denominator.
- The G1 stream claim: four pan replays give 63-70% coverage at 76-84%
precision, not 75% at 100%.
- The upscale paragraph: the guard is intact, but sku_l09.jpg is now a
1000x1000 packshot, so the current run no longer exercises it.
- catalog_hash and the pyramid floor: the hash covers view bytes, not just
names, and every view is now above the 800 px floor.
Also refreshes the server smoke against the renamed catalog: 53 named in 2.58 s,
no view-suffixed ids, no missing product info. l12 is not among them - it has
no enrolled view, which is the documented gap.
Eight images for sku_l04, sku_l07 and sku_l09 from six source domains, scored
through the shipped LocalLaneAdapter and DecisionPolicy against
stock-lopia-clean: 4 correct accepts, 4 abstains, no false accepts.
Leak-tested first: the highest template match of any image against
add/image6.jpg is 0.675, so none is a crop of the evaluation photo.
The result that matters is sku_l07. It has no enrolled image, and both its
images accept through the text lane alone - one reads ラッキー フヨネーズ, the
other the rear label's 製造者:丸和油脂株式会社 with 内容量:200g, which
together name the row 丸和油脂 ラッキーマヨネーズ 200g. The text lane's
coverage claim holds on data that was not used to build it.
The four l04 abstentions are correct behaviour: l04 is also unenrolled and its
reads are weak (one reads only コと旨 タ 味, another scores 0.379 against a
0.45 accept line).
Also records the l12 reconciliation, the Kewpie 450 g research, and two
reusable pan clips with 11 rejected candidates for a future Tier-3 replay.
Nothing here is Tier 2 or Tier 3 and every manifest says so.
The reproduction block still printed the pre-enrollment numbers (catalog 69%,
decision 86%); it now prints what the command actually outputs today, with the
catalog figure's fall from 87% to 83% explained rather than left to look like a
regression.
Adds the three row corrections with their evidence, the near-unknown fix and
its measured effect, and the state of the four unenrolled SKUs - including that
l08's row name and size are themselves unverified, since no retailer listing
for a 30g スパイシーストーム ペリペリミックス exists.
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.
cat_full's *_shelf.jpg views are crops of add/image6.jpg itself - template
matching puts each one back at 0.998-0.999 confidence - so committing them
reintroduced the evaluation photo as a catalog, which is the leak the audit
removed from stock-lopia. cat_pack is the same views without the shelf copies.
Both are regenerated by lfo_setup.py from stock-lopia and the labels, so the
evidence stays reproducible without the repository carrying the leak. Verified
after removal: lfo_setup.py rebuilds all 16 views.
The row named a product that is not on the shelf. The tag under facings #46-48
reads ユーラス 野菜をおいしく食べる旨タレ (380g) at ¥299 - the exact price the
l12 row already carried - and the package reads お子さまOK 甘めテイスト, the
opposite of 中辛. Category, heat level and brand were all wrong.
This one is worse than a wrong size: the text lane scores a crop against the
row's printed name, so a correct read of the real bottle would have been
vetoed by a row describing a different product.
The 2023 green-label predecessor (380ml, JAN 4571233473887, checksum verified)
is recorded in the manifest as predecessor-only evidence; no current JAN is
claimed, because the current white package's rear label has not been
photographed. One in-store rear-label photo would close that.
/v1/recognize on add/image6.jpg against stock-lopia-clean: 111 detections,
53 named, every one carrying product_name and price, no view-suffixed ids.
sku_l10 answers 9 facings, which it could not do before it was enrolled.
The live WebSocket path was exercised too: 217 frames, 77 tracks labelled,
0 unexplained label changes, l09 and l10 in the final summary.
The documented near-unknown case could not be rejected. Crops 82-97 of
add/image6.jpg are Kewpie 450g bags the catalog does not hold, and they read
'マヨネーズ 450g'. Every mayonnaise row explains 'マヨネーズ' equally, so the
text ranking ties at 1.0 and the strict-greater veto in DecisionPolicy never
fires - the local lane's answer stood and the bag was labelled as a catalog
row. No amount of IDF tuning fixes this: the veto compares two catalog rows,
and the crop's product is not one of them.
The size is the part that differs and the part printed plainly, so the policy
now checks it. Only a mass in PACKAGE_MASS_RANGE counts: below it are
fragments of a longer number ('20' out of '200g') and above it are two numbers
run together ('5210' out of '210g'), and vetoing on either would cost correct
answers. Percentages are excluded for the same reason - 'カロリー 50% カット'
is a claim about the contents, not a 50 g package.
The veto also blocks the text lane's fill-in, or it would only move the
answer: the local row is blocked and the text lane then names that same row.
Measured on the corrected image6 labels, local lane at full resolution:
precision 86% -> 91%, false accepts 8 -> 5, coverage unchanged at 79%. At live
resolution 95% and 2 false accepts, unchanged. 307 tests pass.
Three enrollment views were refused by the name/OCR check at 0.70, and the
research says all three refusals are false negatives:
- sku_l04: the packshot is byte-identical (MD5 510db1965bc483899c0cce17c72ba10e)
to Amazon media 61yWDtDjDuL for ASIN B0197VWHRG, やまや めんたいマヨネーズタイプ
200g, JAN 4903085062863. 明太子屋さんがつくった is a package slogan, not a
competing brand. Caveat recorded: 200g is superseded by the current 300g.
- sku_l07: 丸和油脂 ラッキーマヨネーズ 200g, JAN 4978931401615, corroborated by
Amazon, Rakuten, Yahoo Shopping and Kakaku. The front prints the product name
but never the maker or size, so full-name overlap cannot reach 0.70.
- sku_l10: exact 400g front, JAN 4901001114030, with the 450g rejection.
Also adds the missing sku_l10 OCR read, generated with the current engine, and
l10's attribution entry. Manifests are tracked; the retailer images they cite
are restricted-research and stay untracked, matching the existing precedent.
Both CSVs called sku_l11 キューピー ハーフマヨネーズ 450g. The shelf tag under
the l11 row in add/image6.jpg reads キユーピー ハーフマヨネーズ〈400g〉, and the
enrolled packshot's own OCR reads 400g, so the catalog shipped a size the
product does not have - the same defect class as sku_l10.
Measured on the corrected image6 labels, local lane: full 86% precision (was
84%), live 95% (was 91%), false accepts 8 and 2. 303 tests pass.
The previous commit corrected stock-lopia-clean/stock.csv to reference
sku_l10.jpg but left the view itself untracked, so a fresh checkout would
have loaded a catalog whose CSV names an image that is not there.
Ajinomoto ピュアセレクト マヨネーズ has no 450g size: the official lineup is
250g/400g/600g/1kg, the shelf tag in add/image6.jpg reads 400g, and DECISION.md
already recorded the mismatch. Both CSVs carried 450g, so every l10 answer
shipped a wrong size.
The packshot is the exact 400g front (JAN 4901001114030, 1000x1000) with its
source URL and the 450g rejection recorded in
data/packshots/research/sku_l10/manifest.json.
Enrolling it moves the catalog ceiling from 69% to 87% of in-catalog facings
answerable by image (l10 was 11 unenrolled facings). Measured on the corrected
image6 labels: local lane 92% identity at 84% precision full, 85% at 91% live.
Also points external-eval defaults at the repository-local data path; the
licensed Wikimedia corpus remains uncommitted pending manifest review.
Two enrollment defects, both found by trying to enroll real packshots.
`rebuild_catalog.py` crashed on every successful run: the report referenced
`small` but the call to `below_pyramid_floor` was missing, so the pyramid-floor
check the enrollment protocol asks for never ran and the tool died with a
NameError after writing the catalog.
The name/OCR check read one orientation. A packshot whose identifying text runs
vertically - the やまや めんたいマヨネーズ image reads 明太手屋さんが only when
rotated - was refused as a mismatch, which is the worst failure this check has:
it rejects good data while claiming to validate it. `best_orientation_text`
tries all four right-angle rotations and picks the one that explains the most of
the catalog name, not the one that reads the most characters, because the
upright pass returns stray kana that concatenate longer than the real read.
The regression test goes through `audit`, not the helper: the bug was that
`audit` called a single-orientation read, and a helper unit test would have
passed on the broken code. Verified to fail before the fix and pass after.
Data now lives in the repo, not /tmp:
data/packshots/source/ packshots as downloaded, with ATTRIBUTION.json
data/evaluations/ per-session run artifacts, so a number in
DECISION.md traces to the run that produced it
sku_l09 is re-sourced at 1000x1000 from Matsukiyo's Rakuten image, replacing
the 305px view that the README documents as the reason l09 wins facings it
should not.
E-3: the text lane adds 31 pp coverage at full resolution and 23 pp at live
while raising precision, against a +5 pp bar. E-2: LocalLane is RootSIFT-only,
so ORB/BRISK have nothing left to drop from the shipped path.
The text lane can answer an unenrolled sibling correctly, and those answers
were counted in a ratio whose denominator is the enrolled count, so the
ceiling read 117%. It now reads 86% for the local lane, which is the number
the docstring always described.
The text lane now requires shared bigrams between the read and the winning
row's name before it will answer, and the local lane passes the catalog
signatures through so the decision can weigh text evidence. On the labeled
shelf this moves the local lane from 66% to 80% end-to-end at full
resolution (91% precision) and 66% at live resolution (95% precision, 2
false accepts), against 57%/35% for the matcher alone.
Live fixes:
- `_plan_classification` counts `in_flight` again. The increment had been
dropped while removing a `label_frame_id` write, so the guard never
blocked, every frame re-planned the same tracks, and the executor queue
grew without bound until the server stopped answering.
- Label memory boxes carry the shelf origin they were measured against and
are shifted before the IoU test, so a panning camera no longer slides a
stale box onto its neighbour.
- The text lane is resolved before the session lock is taken, removing a
lock-ordering hazard against the lane's own build lock.
- A native crop always reads; the frame crop skips OCR only when it already
has a reading.
Stream (20 s pan, 4 runs): 71-79% coverage at 76-84% precision, against
62-71% at 73-75% before.
`classify_boxes` returned only the identity, so every accepted still-image
answer reached the client as a bare SKU id with no name or price. Measured
through the real server on add/image6.jpg: 41 of 41 accepted detections had no
`product`. The live path attached the row; the still path did not, which is
the F-02 finding the decision document recorded as still live in production.
The row is attached where the decision is made, so both paths carry it, and
`recognize` passes it through to the wire. Verified through the running server:
41 of 41 accepted answers now carry product_name, price, and stock counts.
Also corrects the decision document's full-resolution claim, which was
measured before the reference-upscaling and text-signature fixes: it said 77%
precision with 11 false accepts and a hub alarm, where the current lane reads
88% with 5 and no alarm. The accept line does not need to differ by input path
- correct facings sit at p25 2.07 (full) and 2.87 (live), wrong ones at p75
1.00 and 1.66, so 2.0 is the boundary in both - and the results table and the
stream G1 numbers are refreshed to the current measurements.
`catalog_hash` hashed the CSV and the view *names*, so replacing
`sku_l09.jpg` with a different product's packshot left the hash unchanged and
a report could claim the same catalog while every l09 decision in it had
moved. It hashes each view's bytes now.
`identity_lanes` carried its own `normalize_text` and `bigrams`, near-copies
of the ones in `rebuild_catalog` that had already drifted (the lane's table
folded ユ to itself, the catalog's did not). The lane imports the catalog's
now, so one read scores the same either way.
`evidence/image6_v2.json` was byte-identical to `eval/labels/image6.json`; the
evidence scripts and the decision document point at the canonical file and the
copy is gone. `score_crop`'s docstring now states the contract its callers
already depend on: the dict's insertion order is the lane's ranking, and
re-sorting by value names a different SKU than the lane chose.
Tests: the lane is shared by the classify pool, so a concurrent-ranking test
pins that one BFMatcher does not cross crops under eight threads.
The per-track OCR cache kept any non-empty read, so a first crop that read
only a size ("400g") was replayed on every later attempt. A size matches every
400g product's size line, so the track could never be named even after a clean
crop arrived: the cached read replaced it. Only a read carrying identity text
is cached now, so the next attempt gets a fresh OCR.
`carries_identity` moves to module scope in live.py; a function-local import
shadowed the name at the cache site and raised UnboundLocalError.
The text lane built each row's signature from its printed name plus the OCR of
its enrolled packshot, and scored a crop against that whole string. l09's
packshot prints "マヨネーズ", which is l10's actual name, so l09 could explain
an l10 crop's text better than l10's own name could. Every l10 facing reads
"ピュア セレクト マヨネーズ" with no "コクうま" and no "65", and l09 still won
or tied them.
The signature is now the row's name alone, weighted by inverse document
frequency over the catalog so copy every sibling prints counts for little and
the words that name one product dominate. The packshot OCR is gone: every row
has a name, so it added nothing but the confusion.
Measured on image6, local lane at live resolution:
l10 correct 2/11 -> 5/11
wrong 3 -> 1
precision 95% -> 97%
l09 hub share 34% (alarm) -> 29% (clear)
Full resolution end-to-end 59% at 88% precision, live 56% at 97%.
Three defects, all found by scoring the live path against ground truth.
Inherited labels carried frame id 0. `_inherit_labels` set `label_box` but
never `label_frame_id`, so every inherited label was mapped back to the source
image with the first frame's offset. During a pan that puts the box a whole
pan-distance from the product it names, which is why most streamed tracks
matched no labelled facing. It now stamps the current frame and anchors to the
raw detection box rather than the smoothed track box, which lags a pan.
The reference lane upscaled views smaller than a pyramid level. `sku_l09.jpg`
is 407x305, so its 400 px level was a 305->400 blow-up: a blur with no
high-frequency detail to disagree with, which won 31 of 54 crops on the shelf
it was cut from. A view is now used only at the levels it actually has.
`score_stream.py` measured the wrong thing. It counted any facing touching the
window by one pixel (23 of 47 "in-catalog" facings were less than half
visible), scored a correct text-lane answer as a false accept because it
checked enrollment before correctness, and counted tracks rather than facings,
which reported 138% coverage of a 24-facing window. It now takes the union of
the window rects the client showed, keeps facings at least half inside, checks
correctness first, and reports facings named separately from labels correct.
Measured on add/image6.jpg, stock-lopia-clean, 20 s per run:
pan backend facings named precision
0 van 11/24 (46%) 52%
0 local 11/24 (46%) 100%
2 van 12/24 (50%) 45%
2 local 18/24 (75%) 100%
12 van 11/24 (46%) 28%
12 local 16/24 (67%) 83%
eval_identity on the same catalog is unchanged by the upscale fix (local 61%
end-to-end at 88% precision full, 59% at 95% live), and 1 of 602 distractor
crops from the 11 out-of-catalog shelf photos reaches 10 inliers, none reach 20.
Review 2, item 2. The first version of this gate counted bigrams with a
non-digit character, which let the unit's own letters through: "400g 50%"
left "0G" and "0%", "450g カロリー50%" left "0%", and "500ml" left
"0M". All three still named products.
The quantity token is now cut whole - digits plus unit, in either script -
before counting, and the floor is three bigrams rather than two, because
"カロリ" is two and it is a descriptor every low-calorie package prints.
DecisionPolicy.decide gated the fill-in only when a read was passed, so the
unsafe behaviour was the default. Omitting the read now means no fill-in: a
caller with a ranking and no read has not shown the text can name anything.
TextLane.carries_identity was a dead duplicate of the module function; deleted.
Tests: the five cases from the review plus the default-gate case. Suite 284 ->
286. Evaluator unchanged: local 61%/77% full, 59%/95% live.
F-01's target was 'every scored label carries provenance', and the fields
existed but nothing enforced them: a v1-style file with no 'source' loaded
exactly like the audited one, which is how 23% wrong labels went unnoticed.
load_labels now refuses such a file by name and index. The v2 labels load
unchanged; a stripped copy is refused. 3 tests. Suite 281 -> 284.
The pacing paragraph described a defect that no longer exists. Replaced with
the measured before/after, and added the two review findings (the missing
always-argmax baseline lane, the size-token fill-in) with their test counts.
Vision took a crop from 5 ms to 25 ms, so re-reading the same package on every
attempt was the measured cause of the live coverage collapse: 789 -> 60 crops/s
at 8 workers, and 12/8/2/6 labels on the same clip at four pacings.
The reading is now cached on the track and later attempts re-run only the local
lane. Measured through the real server on add/image6.jpg:
saturated pool 15.9 -> 49.4 crops/s (8 workers, 64 crops)
per crop 142 -> 92 ms on a 300x400 crop
3 s clip, send 19.7 mean labels (15-24, unstable) -> 24.0 (23-25, stable)
12 s clip, send 48/131 -> 53/131; result pacing 53/134 -> 50/135
The 12 s numbers are within run-to-run noise; the short clip is where the
pacing sensitivity lived and it is now flat across pacings.
An empty read is not cached - a crop too small or too blurry to read says
nothing about the next one - and the cache is cleared with the rest of the
evidence on a method change or a drift relabel, so a reading never outlives the
product it described. The native crop lane skips OCR on the same rule.
Test: OCR runs once per track while the local lane keeps running. Disabling the
skip makes it fail 2 != 1. Suite 280 -> 281.
Two review findings, both confirmed by measurement.
1. The evaluator had no always-argmax lane, so every "vs Van" claim compared
against the *gated* matcher, which is not Van's contract - the original demo
answered every crop with its best-scoring SKU and had no accept line. The new
'van' lane is that contract: same scores, no gate. Against it the local lane
wins both axes at both resolutions (full 61%/77% vs 52%/29%; live 59%/95% vs
57%/32%), but the live coverage margin is one facing, not seven, and the 95%
figure is live-resolution only - at full resolution the same lane reads 77%
with 11 false accepts and a hub alarm.
2. A crop reading only "400g" scored 0.667 on three SKUs, above TEXT_ACCEPT, so
the text lane named a product from a size line. The fill-in job now requires
two non-digit bigrams; the veto is unchanged, so "65%" still contradicts a
local answer. The raw OCR read travels with the ranking through
score_crop_lanes for that check.
Tests: 3 for the size gate (policy + live) and the van lane's own row. Removing
the gate fails all three. Suite 276 -> 280.
load_shelf_skus ran after write_catalog, so a missing path or a malformed list
escaped as a raw traceback over a catalog that had already been replaced. It is
now read inside the try, before anything is written, so both cases fail with the
tool's own message and the destination is untouched.
Adds BadShelfListFailsBeforeTheSwapTests, which asserts the clean message and
that the output path does not exist. Re-injecting the ordering fails both tests
with the raw FileNotFoundError. Suite 274 -> 276.
The protocol asks for a source at least 800px tall so a view can be stored as a
200/400/800px pyramid. Nothing enforced it, and the clean catalog's l09 view is
305px - the documented defect where its inlier scores dwarf every other
reference (85-129 against 10-40) and it wins facings it should not.
Reported, not refused: the view is usable and dropping it would remove the SKU
from the catalog entirely, which is worse than an incomparable score. The fix is
re-sourcing the packshot, which is a data task. The check names the view and its
height so the re-sourcing has a target. Suite 272 -> 274.
Three items from DECISION §8's enrollment protocol were unimplemented and none
of them needed Tier-2 data:
- Eval reports now record provenance: catalog hash, label-file hash, image hash
and the code revision, so a number can be traced to the inputs that produced
it. The catalog hash covers the CSV bytes and the view names, not image bytes:
a re-encoded view of the same product is the same catalog.
- rebuild_catalog flags cross-SKU near-duplicates as text-discriminated
siblings. These are the near-unknown case no threshold rejects - the geometry
is real because the sibling shares the brand's artwork - so they are reported
rather than refused.
- --shelf-tags turns the completeness rule into a live-ready verdict. Without
the shelf's SKU list the verdict is 'unknown', not a guess.
Adds test_rebuild_catalog.py (11 tests; the tool had no test file). Suite 261
-> 272. Also corrects score_crop_lanes' docstring, which still described the
live path as averaging local scores when the local scale decides on the newest
crop.
The live label count is a function of frame pacing, not a fixed property: a
track needs two attempts to settle, so pushing frames faster than the classifier
drains them reports fewer labels (12/8/2/6 at full speed on the same clip, 14
paced, 38 of 30 frames). Precision stays 100% and 0 unknowns are accepted at
every pacing. Also records the local-scale and DecisionPolicy tests.
The shipped default scale had no test: make_session only builds matcher-backend
engines, so scale == "local" in _on_classified never ran in the suite - and
that is the branch that was wrong twice (the rank re-sort, and the early return
that made the text lane's fill-in job unreachable).
Adds LocalScaleEngine, which injects the lanes' evidence into the live path, and
two live tests: a text-only read still labels the track, and a text veto blocks a
local accept. Adds DecisionPolicyTests for the veto/fill-in boundary, including
the lane-order-vs-value case that the rank re-sort broke.
Both regressions were re-injected to confirm the tests fail for the intended
reason: restoring the early return fails the fill-in test, and re-sorting the
lane output by value fails the order test. Suite 251 -> 261.
The record said '2 false accepts of 48 unknowns', which conflated two fields:
the 2 are 0 unknown accepts plus 1 unenrolled in-catalog facing and 1 enrolled
misidentification. Also restores the hybrid-verify line the lane vocabulary
edit dropped, and reframes the l09 note: its hubness ratio is 1.76 full /
1.36 live against G7's limit of 2.0, so the gate passes and the 30% internal
share heuristic is what raises the alarm.