Skip to content

Commit ccb668d

Browse files
committed
fix(verify): normalize scores without domain rules
Normalize the available structural, host and provenance weights while preserving structural hard failures and the no-green policy. Closes #98 Refs GetTechAPI/TechAPI#297
1 parent 78d2080 commit ccb668d

2 files changed

Lines changed: 61 additions & 2 deletions

File tree

‎app/verify/offline.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
* host trust 0..30 — authority of the cited ``source_urls`` (:mod:`hosts`)
88
* provenance 0..10 — clean normalized data vs raw-blob-only imports
99
10+
Categories without domain rules omit consistency and normalize the remaining
11+
65 points to 0..100. Their subscores retain the original weights, and their band
12+
cannot be green until domain rules exist.
13+
1014
Hard predicate violations (threads<cores, boost<base, chip postdates device,
1115
future release) force the band to red regardless of the numeric score.
1216
"""
@@ -111,7 +115,8 @@ def score_record(
111115
completeness = _completeness(rec.category, data)
112116
sigs = signals.signals_for(rec.category, data, now_year, soc_release)
113117
consistency, flags, hard_failed = _consistency(sigs)
114-
if rec.category not in RICH_FIELDS:
118+
has_domain_rules = rec.category in RICH_FIELDS
119+
if not has_domain_rules:
115120
# Only assess defined structural fields; absent domain rules earn no credit.
116121
required = getattr(validate, f"{rec.category.upper()}_REQUIRED", {"slug", "name"})
117122
completeness = W_COMPLETENESS * sum(k in data for k in required) / len(required)
@@ -126,6 +131,11 @@ def score_record(
126131
provenance = _provenance(data, best_tier)
127132

128133
total = completeness + consistency + host + provenance
134+
if not has_domain_rules:
135+
# Missing domain rules remove an assessment dimension, not evidence of
136+
# validity. Normalize the remaining weights onto the same 0..100 scale.
137+
available = W_COMPLETENESS + W_HOST + W_PROVENANCE
138+
total = total * 100.0 / available
129139
subscores = {
130140
"completeness": round(completeness, 1),
131141
"consistency": round(consistency, 1),
@@ -135,7 +145,7 @@ def score_record(
135145

136146
if hard_failed:
137147
band = "red"
138-
elif total >= GREEN_MIN and best_tier in (1, 2):
148+
elif has_domain_rules and total >= GREEN_MIN and best_tier in (1, 2):
139149
band = "green"
140150
elif total < RED_MAX:
141151
band = "red"

‎tests/verify/test_offline.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,61 @@
11
"""Tier 0 scorer + host classification tests."""
22

3+
import pytest
4+
5+
from app import validate
36
from app.verify import hosts, offline
47
from app.verify.common import Record
58

69
NOW = 2026
710
NO_SOC: dict[str, str] = {}
811

912

13+
@pytest.mark.parametrize("category,slug,fields,url,expected", [
14+
("laptop", "acer-chromebook-14-ammok-691",
15+
{"ram_gb": 4, "os": "Chrome OS"},
16+
"https://huggingface.co/datasets/Ammok/laptop_price_prediction", 53.8),
17+
("monitor", "acer-nitro-monitor-amazonmon-636",
18+
{"size_inch": 23.8, "resolution": "1920x1080"},
19+
"https://www.kaggle.com/datasets/durjoychandrapaul/amazon-products-sales-monitor-dataset",
20+
58.5),
21+
])
22+
def test_structurally_valid_variants_are_yellow(category, slug, fields, url, expected):
23+
base = "acer-chromebook-14" if category == "laptop" else "acer-nitro-monitor"
24+
data = {
25+
"slug": slug, "base_model_slug": base, "name": "Example", "brand": "acer",
26+
"release_date": "2023-01-01", "verified": False, "source_urls": [url], **fields,
27+
}
28+
path = f"{category}/acer/2023/{base}/{slug}.json"
29+
required = getattr(validate, f"{category.upper()}_REQUIRED")
30+
errors = []
31+
validate._check_required(path, data, required, errors)
32+
validate._check_slug(path, slug, errors)
33+
validate._check_source_urls(path, data, errors)
34+
validate._check_variant_path(path, data, category, errors, allow_flat=True)
35+
assert errors == []
36+
37+
def score():
38+
return offline.score_record(Record(category, path, data), NOW, NO_SOC)
39+
40+
result = score()
41+
assert result.band == "yellow"
42+
assert result.score == expected
43+
assert result.subscores["consistency"] == 0
44+
assert result.flags == ["domain_rules_unavailable"]
45+
46+
# Strong sources can increase the normalized score, but cannot earn green
47+
# without domain consistency rules.
48+
data["source_urls"] = ["https://intel.com/example", "https://en.wikipedia.org/wiki/x"]
49+
assert score().score >= offline.GREEN_MIN
50+
assert score().band == "yellow"
51+
52+
del data[next(iter(fields))]
53+
broken = score()
54+
assert broken.score >= offline.RED_MAX # hard fail, despite a good numeric score
55+
assert broken.band == "red"
56+
assert "!structural_integrity" in broken.flags
57+
58+
1059
def _score(category, data):
1160
return offline.score_record(Record(category, f"{category}/x.json", data), NOW, NO_SOC)
1261

0 commit comments

Comments
 (0)