From 8db280d220429f5895a156d394b0cde761b689a7 Mon Sep 17 00:00:00 2001 From: kiyo9w Date: Tue, 29 Sep 2026 03:35:41 +0900 Subject: [PATCH] feat(enroll): report views with no recorded source 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. --- recognization/code/rebuild_catalog.py | 24 +++++++++++++++++++ .../data/packshots/source/ATTRIBUTION.json | 10 ++++++++ 2 files changed, 34 insertions(+) diff --git a/recognization/code/rebuild_catalog.py b/recognization/code/rebuild_catalog.py index 51ede02..d7078ed 100644 --- a/recognization/code/rebuild_catalog.py +++ b/recognization/code/rebuild_catalog.py @@ -345,6 +345,25 @@ def audit( return kept, refused +def unattributed(source: str) -> List[str]: + """SKUs whose views have no entry in the source's ATTRIBUTION.json. + + Reported, not refused. A view with no recorded source is still a usable + reference - the six that predate the attribution file are independent of + the evaluation photo and their OCR matches their catalog names - but the + provenance is gone, and provenance is what lets a later audit tell a + re-sourced packshot from a leaked shelf crop. Naming them keeps the gap + from growing quietly. + """ + path = os.path.join(source, "ATTRIBUTION.json") + try: + with open(path) as handle: + recorded = json.load(handle) + except (OSError, ValueError): + recorded = {} + return sorted(sku for sku in collect(source) if sku not in recorded) + + def text_discriminated_siblings( kept: Dict[str, List[str]], source: str, names: Optional[Dict[str, str]] = None ) -> List[Dict[str, Any]]: @@ -564,6 +583,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: text_only = sorted(set(names) - set(kept)) siblings = text_discriminated_siblings(kept, args.source, names) small = below_pyramid_floor(kept, args.source) + no_source = unattributed(args.source) verdict = live_ready_verdict(kept, names, shelf_skus) report = { "source": args.source, @@ -577,6 +597,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: "text_only_skus": text_only, "text_discriminated_siblings": siblings, "below_pyramid_floor": small, + "unattributed_skus": no_source, "completeness": verdict, "skus": {sku: views for sku, views in sorted(kept.items())}, "refused": refused, @@ -600,6 +621,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int: for row in small: print(f" {row['sku']:<10} {row['view']:<20} {row['height']}px tall - " "scores are not comparable with the rest; re-source this view") + if no_source: + print(f"\nno recorded source ({len(no_source)}): {', '.join(no_source)} - " + "usable, but provenance is missing; re-sourcing would close it") if verdict["live_ready"] is None: print(f"\ncompleteness: unknown - {verdict['reason']}") elif verdict["live_ready"]: diff --git a/recognization/data/packshots/source/ATTRIBUTION.json b/recognization/data/packshots/source/ATTRIBUTION.json index 52c32ed..8335e61 100644 --- a/recognization/data/packshots/source/ATTRIBUTION.json +++ b/recognization/data/packshots/source/ATTRIBUTION.json @@ -17,5 +17,15 @@ "source_page": "https://item.rakuten.co.jp/matsukiyo/4901001114030/", "jan": "4901001114030", "notes": "Exact 400g front packshot. The catalog's 450g was wrong: Ajinomoto's lineup is 250/400/600/1000g and the shelf tag in add/image6.jpg reads 400g. See data/packshots/research/sku_l10/manifest.json." + }, + "sku_l06": { + "url": "https://m.media-amazon.com/images/I/81zckoUh+vL._AC_SL1500_.jpg", + "product": "キユーピー フィッテ 210g", + "source_page": "https://www.amazon.co.jp/dp/B08V2676LK", + "notes": "The current package (内臓脂肪を減らす). Replaced the superseded 内臓脂肪ケア view on 2026-09-29; matches the shelf facings in add/image6.jpg exactly. Corroborated by a second domain: https://store.shopping.yahoo.co.jp/ladydrugheartshop/4901577083600.html. See data/external-eval/kewpie/manifest.json." + }, + "_provenance_gap": { + "skus": ["sku_l01", "sku_l02", "sku_l03", "sku_l05", "sku_l09", "sku_l11"], + "note": "These views were enrolled before this file existed and their source URLs were not recorded. What is verified about them: each is independent of the evaluation photo - template-matched against add/image6.jpg, the highest score across all six is 0.166, far below the 0.95 that identifies a crop of it - and each is 1000 px tall, above the enrollment floor. Their product identity is corroborated by their own OCR (data/packshots/source/ocr-reads.json) against the catalog names. Re-sourcing them with recorded URLs would close this; it is not a correctness gap, it is a traceability one." } }