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