Skip to content

Commit aa7d64a

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

7 files changed

Lines changed: 194 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":
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Laptop and monitor scoring verification
2+
3+
Dataset: external TechAPI data, 2026-09-27; year 2026.
4+
5+
Completeness counts nonempty normalized fields; raw imports and optional vendor FK links are excluded.
6+
7+
## laptop (1995 records)
8+
9+
| Rich field | Filled | Coverage |
10+
|---|---:|---:|
11+
| cpu_name | 1990 | 99.7% |
12+
| ram_gb | 1995 | 100.0% |
13+
| storage_gb | 1982 | 99.3% |
14+
| display.size_inch | 1993 | 99.9% |
15+
| display.resolution | 1990 | 99.7% |
16+
| weight_g | 1001 | 50.2% |
17+
| gpu_name | 1964 | 98.4% |
18+
| os | 1995 | 100.0% |
19+
| release_date | 1995 | 100.0% |
20+
21+
| Signal | Evaluated | Flags | Hard flags |
22+
|---|---:|---:|---:|
23+
| ram_plausible | 1995 | 0 | 0 |
24+
| storage_plausible | 1982 | 0 | 0 |
25+
| display_size_plausible | 1993 | 0 | 0 |
26+
| weight_plausible | 1001 | 0 | 0 |
27+
| resolution_parses | 1990 | 0 | 0 |
28+
| ppi_consistent | 1984 | 0 | 0 |
29+
| release_not_future | 1995 | 0 | 0 |
30+
31+
Bands: green 107, yellow 1888, red 0.
32+
33+
## monitor (882 records)
34+
35+
| Rich field | Filled | Coverage |
36+
|---|---:|---:|
37+
| size_inch | 882 | 100.0% |
38+
| resolution | 882 | 100.0% |
39+
| refresh_hz | 677 | 76.8% |
40+
| panel_type | 468 | 53.1% |
41+
| ppi | 882 | 100.0% |
42+
| aspect_ratio | 880 | 99.8% |
43+
| features.ports | 760 | 86.2% |
44+
| features.response_time_ms | 567 | 64.3% |
45+
46+
| Signal | Evaluated | Flags | Hard flags |
47+
|---|---:|---:|---:|
48+
| display_size_plausible | 882 | 1 | 0 |
49+
| refresh_plausible | 677 | 0 | 0 |
50+
| resolution_parses | 882 | 0 | 0 |
51+
| ppi_consistent | 882 | 0 | 0 |
52+
| release_not_future | 882 | 0 | 0 |
53+
54+
Bands: green 0, yellow 882, red 0.
55+
56+
Monitor size uses 7..86 inches to include the observed touch-display and signage records; the one 6-inch entry is a monitor arm accessory. Laptop ranges are RAM 1..256 GB, storage 1..16384 GB, size 7..21 inches, weight 400..6000 g; monitor refresh is 24..600 Hz. Nonpositive/nonfinite numeric measurements and zero resolution dimensions are hard failures; other outliers and future dates are soft. PPI uses the existing 15% tolerance.
57+
58+
107 laptops now reach green; bulk-only records remain yellow because green still requires a T1/T2 source. All monitors remain yellow.
59+
Observed numeric ranges: laptop RAM 2..64 GB, storage 1..6144 GB, display 10.1..18.4 inches, weight 690..4700 g, PPI 91..352; monitor size 6..86 inches, refresh 60..380 Hz, PPI 40..734. The broad upper limits allow legitimate configurations beyond this sample without making outliers hard failures.
60+
61+
Local status output: `C:/Users/29/Desktop/TechEngine-e2-status.json` (recomputed from external data, without changing the data repository).
62+
63+
Validation: full pytest suite 445 passed with `TECHAPI_DATA_DIR=C:/Users/29/Desktop/TechAPI/data` and `--basetemp C:/Users/29/Desktop/TechEngine-test-temp-e2`; targeted scoring/boundary suite 30 passed (including the subsequently added boundary cases). `mypy app`, `ruff check app tests`, and `git diff --check` pass.

‎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)