Skip to content

Commit 50937b3

Browse files
committed
feat(verify): add laptop and monitor domain scoring
Refs #98 Refs GetTechAPI/TechAPI#297
1 parent ccb668d commit 50937b3

6 files changed

Lines changed: 131 additions & 49 deletions

File tree

‎app/verify/cli.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ def _print_markdown(hist: dict[str, Counter[str]], scored: int, hard_flags: Coun
198198
)
199199
gtot = sum(totals.values()) or 1
200200
print(f"**{scored} record(s) assessed.**\n")
201-
print("Laptop, monitor, software and website assess required fields and sources only; "
201+
print("Software and website assess required fields and sources only; "
202202
"domain consistency rules are unavailable and these categories cannot earn green.\n")
203203

204204
# Overall distribution as a Mermaid pie (rendered by GitHub). Mermaid colors

‎app/verify/offline.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@
5050
"cameras", "os_version"),
5151
"watch": ("display.size_inch", "display.resolution", "os_version"),
5252
"pda": ("display.size_inch", "display.resolution", "os_version"),
53+
"laptop": ("cpu_name", "ram_gb", "storage_gb", "display.size_inch",
54+
"display.resolution", "weight_g", "gpu_name", "os", "release_date"),
55+
"monitor": ("size_inch", "resolution", "refresh_hz", "panel_type", "ppi",
56+
"aspect_ratio", "features.ports", "features.response_time_ms"),
5357
"brand": ("founded_year", "description_en"),
5458
}
5559

‎app/verify/signals.py‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,69 @@ def brand_signals(rec: dict[str, Any], now_year: int) -> list[Signal]:
286286
return [founded]
287287

288288

289+
def _positive_range(name: str, value: Any, lo: float, hi: float) -> Signal:
290+
"""Missing measurements are NA; impossible ones are hard, outliers soft."""
291+
if value is None:
292+
return Signal(name, "na")
293+
number = _num(value)
294+
if number is None:
295+
return Signal(name, "fail")
296+
if not math.isfinite(number) or number <= 0:
297+
return Signal(name, "fail", hard=True)
298+
return Signal(name, "pass" if lo <= number <= hi else "fail")
299+
300+
301+
def _resolution_signal(value: Any) -> Signal:
302+
if value in (None, ""):
303+
return Signal("resolution_parses", "na")
304+
parsed = parse_resolution(value)
305+
if parsed is None:
306+
return Signal("resolution_parses", "fail")
307+
if min(parsed) <= 0:
308+
return Signal("resolution_parses", "fail", hard=True)
309+
return Signal("resolution_parses", "pass")
310+
311+
312+
def _device_release_signal(rec: dict[str, Any], now_year: int) -> Signal:
313+
# Future dates may describe announced products, so flag rather than force red.
314+
year = _year_of(rec.get("release_date"))
315+
if year is None:
316+
return Signal("release_not_future", "na")
317+
return Signal("release_not_future", "pass" if year <= now_year else "fail")
318+
319+
320+
def laptop_signals(rec: dict[str, Any], now_year: int) -> list[Signal]:
321+
raw_display = rec.get("display")
322+
display = raw_display if isinstance(raw_display, dict) else {}
323+
return [
324+
_positive_range("ram_plausible", rec.get("ram_gb"), 1, 256),
325+
_positive_range("storage_plausible", rec.get("storage_gb"), 1, 16384),
326+
_positive_range("display_size_plausible", display.get("size_inch"), 7, 21),
327+
_positive_range("weight_plausible", rec.get("weight_g"), 400, 6000),
328+
_resolution_signal(display.get("resolution")),
329+
_ppi_signal(display),
330+
_device_release_signal(rec, now_year),
331+
]
332+
333+
334+
def monitor_signals(rec: dict[str, Any], now_year: int) -> list[Signal]:
335+
# Includes 7-inch touch monitors and 86-inch signage in the seed dataset.
336+
return [
337+
_positive_range("display_size_plausible", rec.get("size_inch"), 7, 86),
338+
_positive_range("refresh_plausible", rec.get("refresh_hz"), 24, 600),
339+
_resolution_signal(rec.get("resolution")),
340+
_ppi_signal(rec),
341+
_device_release_signal(rec, now_year),
342+
]
343+
344+
289345
def signals_for(
290346
category: str, rec: dict[str, Any], now_year: int, soc_release: dict[str, str]
291347
) -> list[Signal]:
348+
if category == "laptop":
349+
return laptop_signals(rec, now_year)
350+
if category == "monitor":
351+
return monitor_signals(rec, now_year)
292352
if category == "cpu":
293353
return cpu_signals(rec, now_year)
294354
if category == "gpu":

‎tests/unit/test_bot_coverage.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def test_all_categories_share_registry():
2121
}
2222

2323

24-
@pytest.mark.parametrize("category", ["laptop", "monitor", "software", "website", "future"])
24+
@pytest.mark.parametrize("category", ["software", "website", "future"])
2525
def test_missing_domain_rules_never_earn_green(category):
2626
score = offline.score_record(Record(category, "example.json", {
2727
"slug": "example", "name": "Example", "source_urls": ["https://intel.com/example"],
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Plausibility boundaries distinguish outliers, missing data and impossibilities."""
2+
3+
import pytest
4+
5+
from app.verify.signals import signals_for
6+
7+
8+
@pytest.mark.parametrize("category,record,name,hard", [
9+
("laptop", {"ram_gb": 512}, "ram_plausible", False),
10+
("laptop", {"weight_g": 300}, "weight_plausible", False),
11+
("laptop", {"storage_gb": -1}, "storage_plausible", True),
12+
("laptop", {"display": {"size_inch": float("inf")}}, "display_size_plausible", True),
13+
("monitor", {"refresh_hz": 700}, "refresh_plausible", False),
14+
("monitor", {"size_inch": -1}, "display_size_plausible", True),
15+
("monitor", {"resolution": "unknown"}, "resolution_parses", False),
16+
("monitor", {"resolution": "00x1080"}, "resolution_parses", True),
17+
("monitor", {"refresh_hz": True}, "refresh_plausible", False),
18+
("laptop", {"release_date": "2027-01-01"}, "release_not_future", False),
19+
("monitor", {"release_date": "2027-01-01"}, "release_not_future", False),
20+
("monitor", {"size_inch": 24, "resolution": "1920x1080", "ppi": 300},
21+
"ppi_consistent", False),
22+
])
23+
def test_outliers_are_soft_and_impossible_measurements_are_hard(category, record, name, hard):
24+
signal = next(s for s in signals_for(category, record, 2026, {}) if s.name == name)
25+
assert signal.failed
26+
assert signal.hard is hard
27+
28+
29+
@pytest.mark.parametrize("size", [7, 86])
30+
def test_specialty_monitor_sizes_are_plausible(size):
31+
assert not any(s.failed for s in signals_for("monitor", {"size_inch": size}, 2026, {}))
32+
33+
34+
@pytest.mark.parametrize("category", ["laptop", "monitor"])
35+
def test_absent_measurements_are_not_flags(category):
36+
assert all(s.result == "na" for s in signals_for(category, {}, 2026, {}))

‎tests/verify/test_offline.py‎

Lines changed: 29 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,60 +2,13 @@
22

33
import pytest
44

5-
from app import validate
65
from app.verify import hosts, offline
76
from app.verify.common import Record
87

98
NOW = 2026
109
NO_SOC: dict[str, str] = {}
1110

1211

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-
5912
def _score(category, data):
6013
return offline.score_record(Record(category, f"{category}/x.json", data), NOW, NO_SOC)
6114

@@ -148,3 +101,32 @@ def test_model_key_of_a_standalone_record_is_itself():
148101
rec = Record("cpu", "cpu/intel/2023/desktop/core-i9-14900k.json",
149102
{"slug": "core-i9-14900k"})
150103
assert rec.model_key == ("cpu", "core-i9-14900k")
104+
105+
106+
@pytest.mark.parametrize("category,fields", [
107+
("laptop", {"cpu_name": "Example CPU", "ram_gb": 16, "storage_gb": 512,
108+
"display": {"size_inch": 14, "resolution": "1920x1080", "ppi": 157},
109+
"weight_g": 1400, "gpu_name": "Integrated", "os": "Linux"}),
110+
("monitor", {"size_inch": 24, "resolution": "1920x1080", "refresh_hz": 144,
111+
"panel_type": "IPS", "ppi": 92, "aspect_ratio": "16:9",
112+
"features": {"ports": ["HDMI"], "response_time_ms": 1}}),
113+
])
114+
@pytest.mark.parametrize("url", ["https://intel.com/example", "https://wikidata.org/wiki/Q1"])
115+
def test_complete_laptop_monitor_can_be_green(category, fields, url):
116+
data = {**fields, "release_date": "2023-01-01", "source_urls": [url]}
117+
score = _score(category, data)
118+
assert score.band == "green"
119+
assert score.subscores["consistency"] == 35
120+
assert score.flags == []
121+
# A physical impossibility overrides an otherwise rich, well-sourced record.
122+
data["ram_gb" if category == "laptop" else "size_inch"] = 0
123+
assert _score(category, data).band == "red"
124+
125+
126+
@pytest.mark.parametrize("category", ["laptop", "monitor"])
127+
def test_sparse_bulk_laptop_monitor_stays_yellow(category):
128+
data = {"release_date": "2023-01-01", "source_urls": ["https://kaggle.com/example"]}
129+
score = _score(category, data)
130+
assert score.band == "yellow"
131+
assert "domain_rules_unavailable" not in score.flags
132+
assert score.subscores["consistency"] == 35

0 commit comments

Comments
 (0)