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." } }