Skip to content

Commit 92a657b

Browse files
committed
ci: summarize TechAPI PR data validation
1 parent 097ed1c commit 92a657b

1 file changed

Lines changed: 202 additions & 0 deletions

File tree

.github/workflows/techapi-pr-validation-comment.yml

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ jobs:
5151
ref: ${{ env.TECHAPI_HEAD_SHA }}
5252
path: TechAPI
5353

54+
- name: Checkout TechAPI main
55+
uses: actions/checkout@v4
56+
with:
57+
repository: GetTechAPI/TechAPI
58+
ref: main
59+
path: TechAPI-main
60+
5461
- uses: actions/setup-python@v5
5562
with:
5663
python-version: "3.12"
@@ -90,6 +97,199 @@ jobs:
9097
echo "app_status=${app_status:-1}" >> "$GITHUB_OUTPUT"
9198
echo "integrity_status=${integrity_status:-1}" >> "$GITHUB_OUTPUT"
9299
100+
- name: Build data quality summary
101+
shell: bash
102+
run: |
103+
python - <<'PY'
104+
from __future__ import annotations
105+
106+
import hashlib
107+
import json
108+
import re
109+
from collections import Counter, defaultdict
110+
from pathlib import Path
111+
from typing import Any
112+
113+
HEAD = Path("TechAPI/data")
114+
BASE = Path("TechAPI-main/data")
115+
CATEGORIES = ("brand", "soc", "smartphone", "gpu", "cpu")
116+
MAX_WARNINGS = 20
117+
118+
def load_json(path: Path) -> dict[str, Any]:
119+
return json.loads(path.read_text(encoding="utf-8-sig"))
120+
121+
def rel_jsons(root: Path, category: str) -> dict[str, Path]:
122+
base = root / category
123+
if not base.exists():
124+
return {}
125+
return {
126+
str(path.relative_to(root)).replace("\\", "/"): path
127+
for path in sorted(base.rglob("*.json"))
128+
}
129+
130+
def digest(path: Path) -> str:
131+
return hashlib.sha256(path.read_bytes()).hexdigest()
132+
133+
def verified_value(record: dict[str, Any]) -> bool | None:
134+
value = record.get("verified")
135+
return value if isinstance(value, bool) else None
136+
137+
def has_kaggle_source(record: dict[str, Any]) -> bool:
138+
return any(
139+
isinstance(url, str) and "kaggle.com" in url.lower()
140+
for url in record.get("source_urls", [])
141+
)
142+
143+
def name_warnings(category: str, rel: str, record: dict[str, Any]) -> list[str]:
144+
warnings: list[str] = []
145+
name = record.get("name")
146+
if not isinstance(name, str):
147+
return warnings
148+
if name != name.strip():
149+
warnings.append("leading/trailing whitespace in name")
150+
if " " in name:
151+
warnings.append("double spaces in name")
152+
if "\ufffd" in name:
153+
warnings.append("replacement character in name")
154+
if name.count("(") != name.count(")"):
155+
warnings.append("unbalanced parentheses in name")
156+
words = re.findall(r"[A-Za-z0-9]+", name.lower())
157+
if any(a == b and len(a) > 1 for a, b in zip(words, words[1:])):
158+
warnings.append("repeated adjacent word in name")
159+
if re.search(r"\b(unknown|unk|n/a|tbd|null)\b", name, re.I):
160+
warnings.append("placeholder-like token in name")
161+
return [f"{category}: {rel}: {warning}" for warning in warnings]
162+
163+
def value_warnings(category: str, rel: str, record: dict[str, Any]) -> list[str]:
164+
warnings: list[str] = []
165+
if category == "cpu":
166+
cores = record.get("cores")
167+
threads = record.get("threads")
168+
base = record.get("base_clock_ghz")
169+
boost = record.get("boost_clock_ghz")
170+
if isinstance(cores, int) and isinstance(threads, int) and threads < cores:
171+
warnings.append("threads < cores")
172+
if (
173+
isinstance(base, (int, float))
174+
and isinstance(boost, (int, float))
175+
and boost > 0
176+
and base > 0
177+
and boost < base
178+
):
179+
warnings.append("boost clock below base clock")
180+
arch = record.get("architecture")
181+
if isinstance(arch, str) and re.search(r"\b(unknown|n/a|tbd|null)\b", arch, re.I):
182+
warnings.append("placeholder-like architecture")
183+
if category == "gpu":
184+
base = record.get("base_clock_mhz")
185+
boost = record.get("boost_clock_mhz")
186+
if (
187+
isinstance(base, (int, float))
188+
and isinstance(boost, (int, float))
189+
and boost > 0
190+
and base > 0
191+
and boost < base
192+
):
193+
warnings.append("boost clock below base clock")
194+
return [f"{category}: {rel}: {warning}" for warning in warnings]
195+
196+
lines: list[str] = []
197+
lines.append("## Data summary")
198+
lines.append("")
199+
lines.append("| Category | Total | Verified | Unverified | Verified % |")
200+
lines.append("| --- | ---: | ---: | ---: | ---: |")
201+
202+
total_all = verified_all = unverified_all = 0
203+
by_category: dict[str, dict[str, int]] = {}
204+
for category in CATEGORIES:
205+
paths = rel_jsons(HEAD, category)
206+
verified = unverified = 0
207+
for path in paths.values():
208+
value = verified_value(load_json(path))
209+
if value is True:
210+
verified += 1
211+
elif value is False:
212+
unverified += 1
213+
total = len(paths)
214+
pct = f"{(verified / total * 100):.1f}%" if total else "0.0%"
215+
by_category[category] = {
216+
"total": total,
217+
"verified": verified,
218+
"unverified": unverified,
219+
}
220+
total_all += total
221+
verified_all += verified
222+
unverified_all += unverified
223+
lines.append(f"| {category} | {total} | {verified} | {unverified} | {pct} |")
224+
pct_all = f"{(verified_all / total_all * 100):.1f}%" if total_all else "0.0%"
225+
lines.append(f"| **all** | **{total_all}** | **{verified_all}** | **{unverified_all}** | **{pct_all}** |")
226+
227+
lines.append("")
228+
lines.append("## PR data delta")
229+
lines.append("")
230+
lines.append("| Category | Added | Modified | Deleted | Added verified | Added unverified | Added Kaggle-sourced |")
231+
lines.append("| --- | ---: | ---: | ---: | ---: | ---: | ---: |")
232+
233+
all_added: list[tuple[str, str, Path]] = []
234+
for category in CATEGORIES:
235+
head = rel_jsons(HEAD, category)
236+
base = rel_jsons(BASE, category)
237+
added_keys = sorted(set(head) - set(base))
238+
deleted_keys = sorted(set(base) - set(head))
239+
modified_keys = sorted(
240+
key for key in set(head) & set(base) if digest(head[key]) != digest(base[key])
241+
)
242+
added_verified = added_unverified = added_kaggle = 0
243+
for key in added_keys:
244+
record = load_json(head[key])
245+
all_added.append((category, key, head[key]))
246+
if verified_value(record) is True:
247+
added_verified += 1
248+
elif verified_value(record) is False:
249+
added_unverified += 1
250+
if has_kaggle_source(record):
251+
added_kaggle += 1
252+
lines.append(
253+
f"| {category} | {len(added_keys)} | {len(modified_keys)} | {len(deleted_keys)} | "
254+
f"{added_verified} | {added_unverified} | {added_kaggle} |"
255+
)
256+
257+
lines.append("")
258+
lines.append("## Heuristic review")
259+
lines.append("")
260+
warnings: list[str] = []
261+
manufacturer_counter: Counter[str] = Counter()
262+
source_counter: Counter[str] = Counter()
263+
for category, rel, path in all_added:
264+
record = load_json(path)
265+
manufacturer = record.get("manufacturer") or record.get("brand")
266+
if isinstance(manufacturer, str):
267+
manufacturer_counter[manufacturer] += 1
268+
if has_kaggle_source(record):
269+
source_counter["kaggle"] += 1
270+
else:
271+
source_counter["other"] += 1
272+
warnings.extend(name_warnings(category, rel, record))
273+
warnings.extend(value_warnings(category, rel, record))
274+
275+
if manufacturer_counter:
276+
top = ", ".join(f"{name}: {count}" for name, count in manufacturer_counter.most_common(8))
277+
lines.append(f"- Added records by manufacturer/brand: {top}")
278+
if source_counter:
279+
top = ", ".join(f"{name}: {count}" for name, count in source_counter.most_common())
280+
lines.append(f"- Added records by source class: {top}")
281+
282+
if warnings:
283+
lines.append(f"- Heuristic warnings: {len(warnings)} total; showing first {min(MAX_WARNINGS, len(warnings))}.")
284+
lines.append("")
285+
for warning in warnings[:MAX_WARNINGS]:
286+
lines.append(f" - {warning}")
287+
else:
288+
lines.append("- Heuristic warnings: none found.")
289+
290+
Path("quality-summary.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
291+
PY
292+
93293
- name: Build PR comment
94294
shell: bash
95295
run: |
@@ -114,6 +314,8 @@ jobs:
114314
echo "| \`python -m app.validate\` | $([ "${{ steps.validate.outputs.app_status }}" = "0" ] && echo PASS || echo FAIL) |"
115315
echo "| \`python integrity_check.py TechAPI/data --strict\` | $([ "${{ steps.validate.outputs.integrity_status }}" = "0" ] && echo PASS || echo FAIL) |"
116316
echo
317+
cat quality-summary.md
318+
echo
117319
echo "<details><summary>Validation log</summary>"
118320
echo
119321
echo '```text'

0 commit comments

Comments
 (0)