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.
This commit is contained in:
kiyo9w 2026-09-29 03:35:41 +09:00
parent 20fb459ee7
commit 8db280d220
2 changed files with 34 additions and 0 deletions

View file

@ -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"]:

View file

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