diff --git a/app/verify/cli.py b/app/verify/cli.py index 310f92f..2235da7 100644 --- a/app/verify/cli.py +++ b/app/verify/cli.py @@ -254,14 +254,24 @@ def cmd_status(args: argparse.Namespace) -> int: by_category: dict[str, dict[str, Any]] = {} tot = ver = g = y = r = 0 + all_models: set[tuple[str, ...]] = set() + all_verified_models: set[tuple[str, ...]] = set() for cat in CATEGORIES: ct = cv = cg = cy = cr = 0 + # A model counts as verified once any of its SKUs is: the variants of one + # phone share a provenance, so the source that verifies one describes the + # product the others are configurations of. + models: set[tuple[str, ...]] = set() + verified_models: set[tuple[str, ...]] = set() for rec in records[cat]: if not rec.slug: continue ct += 1 + key = rec.model_key + models.add(key) if rec.verified: cv += 1 + verified_models.add(key) band = offline.score_record(rec, now_year, soc_release).band cg += band == "green" cy += band == "yellow" @@ -270,6 +280,11 @@ def cmd_status(args: argparse.Namespace) -> int: "total": ct, "verified": cv, "verified_pct": round(100 * cv / ct, 2) if ct else 0.0, + "models": len(models), + "verified_models": len(verified_models), + "verified_models_pct": ( + round(100 * len(verified_models) / len(models), 2) if models else 0.0 + ), "green": cg, "yellow": cy, "red": cr, @@ -281,6 +296,8 @@ def cmd_status(args: argparse.Namespace) -> int: g += cg y += cy r += cr + all_models |= models + all_verified_models |= verified_models status = { "generated_at": _now_iso(), @@ -289,6 +306,13 @@ def cmd_status(args: argparse.Namespace) -> int: "records": tot, "verified": ver, "verified_pct": round(100 * ver / tot, 2) if tot else 0.0, + # Same verifications counted per product rather than per SKU. + "models": len(all_models), + "verified_models": len(all_verified_models), + "verified_models_pct": ( + round(100 * len(all_verified_models) / len(all_models), 2) + if all_models else 0.0 + ), "green": g, "yellow": y, "red": r, diff --git a/app/verify/common.py b/app/verify/common.py index 01136a4..82f3c14 100644 --- a/app/verify/common.py +++ b/app/verify/common.py @@ -54,6 +54,22 @@ def slug(self) -> str | None: def verified(self) -> bool: return self.data.get("verified") is True + @property + def model_key(self) -> tuple[str, ...]: + """Identifies the product, not the SKU. + + 78% of the smartphone set is regional/RAM variants of the same phone — + one model can carry dozens of records ("…k61…latam-q630ha…costa-rica- + 4gb-128gb"). Counting per record makes the dataset look far less + verified than the products in it are, because no source documents an + individual SKU. Variants collapse onto their base model; a standalone + record is its own model. + """ + base = self.data.get("base_model_slug") + if isinstance(base, str) and base: + return (self.category, str(self.data.get("brand") or ""), base) + return (self.category, self.slug or self.path) + def content_hash(self) -> str: """Stable hash of the record body — invalidates stale ledger decisions on edit.""" blob = json.dumps(self.data, sort_keys=True, ensure_ascii=False) diff --git a/tests/verify/test_offline.py b/tests/verify/test_offline.py index f985e29..48199ec 100644 --- a/tests/verify/test_offline.py +++ b/tests/verify/test_offline.py @@ -63,3 +63,26 @@ def test_future_release_red(): "source_urls": ["https://en.wikipedia.org/wiki/x"], } assert _score("cpu", rec).band == "red" + + +def test_model_key_collapses_variants_of_one_phone(): + """Regional/RAM SKUs of one phone are one product, not many.""" + from app.verify.common import Record + + def variant(slug, base): + return Record("smartphone", f"smartphone/lg/2020/{base}/{slug}.json", + {"slug": slug, "brand": "lg", "base_model_slug": base}) + + a = variant("lg-k61-costa-rica-4gb-128gb", "k61-2020") + b = variant("lg-k61-colombia-3gb-64gb", "k61-2020") + other = variant("lg-k51-usa-3gb-32gb", "k51-2020") + assert a.model_key == b.model_key + assert a.model_key != other.model_key + + +def test_model_key_of_a_standalone_record_is_itself(): + from app.verify.common import Record + + rec = Record("cpu", "cpu/intel/2023/desktop/core-i9-14900k.json", + {"slug": "core-i9-14900k"}) + assert rec.model_key == ("cpu", "core-i9-14900k")