|
| 1 | +"""One-off data-integrity scan for TechAPI CPU+GPU (structural + benchmark anomaly). |
| 2 | +
|
| 3 | +Complements app/validate.py (schema) with: duplicate detection, slug/file match, |
| 4 | +verified-without-source, name/tier vs core-count consistency, single>multi sanity, |
| 5 | +era-vs-score outliers, and CROSS-SOURCE correlation outliers (the key wrong-variant |
| 6 | +contamination detector). Read-only; prints flagged items for human review. |
| 7 | +
|
| 8 | +Usage:: |
| 9 | +
|
| 10 | + python integrity_check.py [DATA_ROOT] [--strict] |
| 11 | +
|
| 12 | +By default it prints every flagged item and exits 0 (human-review mode). With |
| 13 | +``--strict`` it additionally exits non-zero when any *hard* anomaly is found — |
| 14 | +unambiguous corruption that must block the weekly refresh PR: duplicate slugs, |
| 15 | +slug/filename mismatches, and physically-impossible single>multi benchmarks. |
| 16 | +The statistical cross-source/era outliers stay advisory (a heterogeneous catalog |
| 17 | +of server + desktop + mobile parts legitimately produces many ratio outliers), so |
| 18 | +they are printed for review but never fail the gate. |
| 19 | +""" |
| 20 | +from __future__ import annotations |
| 21 | +import os, json, math, re, statistics, sys |
| 22 | + |
| 23 | +# Em-dash etc. in section headers must not crash on legacy consoles (e.g. cp949). |
| 24 | +try: |
| 25 | + sys.stdout.reconfigure(encoding="utf-8") # type: ignore[union-attr] |
| 26 | +except Exception: |
| 27 | + pass |
| 28 | + |
| 29 | +_argv = sys.argv[1:] |
| 30 | +STRICT = "--strict" in _argv |
| 31 | +_positional = [a for a in _argv if not a.startswith("-")] |
| 32 | +ROOT = _positional[0] if _positional else r"C:\Users\29\Desktop\TechAPI\data" |
| 33 | + |
| 34 | +# Hard anomalies block the weekly gate under --strict; soft ones are review-only. |
| 35 | +HARD: list[str] = [] |
| 36 | +def hard(msg: str) -> None: |
| 37 | + HARD.append(msg) |
| 38 | + print(msg) |
| 39 | + |
| 40 | +def load(comp): |
| 41 | + recs = [] |
| 42 | + for dp, _, fs in os.walk(os.path.join(ROOT, comp)): |
| 43 | + for fn in fs: |
| 44 | + if fn.endswith(".json") and not fn.startswith("_"): |
| 45 | + p = os.path.join(dp, fn) |
| 46 | + recs.append((p, fn[:-5], json.load(open(p, encoding="utf-8")))) |
| 47 | + return recs |
| 48 | + |
| 49 | +def mad_outliers(pairs, lo=0.34, hi=3.0): |
| 50 | + """pairs: list of (label, a, b); flag log(a/b) outliers via median±3*MAD.""" |
| 51 | + rs = [(l, math.log(a / b)) for l, a, b in pairs if a and b] |
| 52 | + if len(rs) < 8: |
| 53 | + return [] |
| 54 | + med = statistics.median(r for _, r in rs) |
| 55 | + mad = statistics.median(abs(r - med) for _, r in rs) or 1e-9 |
| 56 | + return [(l, round(math.exp(r), 2)) for l, r in rs if abs(r - med) > 4 * mad] |
| 57 | + |
| 58 | +def section(t): print(f"\n### {t}") |
| 59 | + |
| 60 | +cpus = load("cpu"); gpus = load("gpu") |
| 61 | +print(f"loaded CPU={len(cpus)} GPU={len(gpus)}") |
| 62 | + |
| 63 | +# --- 1. duplicates + slug/file + verified-no-source --- |
| 64 | +section("structural") |
| 65 | +for comp, recs in (("cpu", cpus), ("gpu", gpus)): |
| 66 | + slugs, names = {}, {} |
| 67 | + for p, fn, d in recs: |
| 68 | + slugs.setdefault(d.get("slug"), []).append(fn) |
| 69 | + names.setdefault(d.get("name"), []).append(fn) |
| 70 | + if d.get("slug") != fn: |
| 71 | + hard(f" [{comp}] slug!=file: {fn} slug={d.get('slug')}") |
| 72 | + for s, fl in slugs.items(): |
| 73 | + if len(fl) > 1: hard(f" [{comp}] DUP slug {s}: {fl}") |
| 74 | + for n, fl in names.items(): |
| 75 | + if len(fl) > 1: hard(f" [{comp}] DUP name {n!r}: {fl}") |
| 76 | + |
| 77 | +# --- 2. AMD Ryzen line vs DESKTOP model tier-digit (2nd digit); APU/mobile excepted --- |
| 78 | +section("CPU name/tier consistency (desktop mainstream only)") |
| 79 | +TIERMAP = {"6": "5", "7": "7", "8": "7", "9": "9"} # 2nd model digit -> expected line |
| 80 | +for p, fn, d in cpus: |
| 81 | + n = d.get("name", "") |
| 82 | + # mainstream desktop: 4-digit model, no G/U/H/HS/HX (APU/mobile) suffix |
| 83 | + m = re.match(r"AMD Ryzen (\d) (\d)(\d)\d\d(X3D|X|XT)?$", n) |
| 84 | + if m: |
| 85 | + line, _gen, tier = m.group(1), m.group(2), m.group(3) |
| 86 | + exp = TIERMAP.get(tier) |
| 87 | + if exp and exp != line: |
| 88 | + print(f" [tier] {n!r}: line Ryzen {line} but tier-digit {tier} → expect Ryzen {exp}") |
| 89 | + |
| 90 | +# --- 3. benchmark sanity: single>multi (consistent-scale benches) --- |
| 91 | +section("CPU single>multi (cinebench/geekbench — should be multi>=single)") |
| 92 | +for p, fn, d in cpus: |
| 93 | + for s, mu in [("cinebench_r23_single","cinebench_r23_multi"), |
| 94 | + ("geekbench_single","geekbench_multi"), |
| 95 | + ("cinebench_2024_single","cinebench_2024_multi")]: |
| 96 | + a, b = d.get(s), d.get(mu) |
| 97 | + if a and b and a > b and (d.get("threads") or 1) > 1: |
| 98 | + hard(f" {d['name']!r}: {s}={a} > {mu}={b}") |
| 99 | + |
| 100 | +# --- 4. era vs score (catch wrong-variant: old chip w/ modern score) --- |
| 101 | +section("CPU era-vs-score outliers") |
| 102 | +for p, fn, d in cpus: |
| 103 | + y = (d.get("release_date") or "0")[:4] |
| 104 | + pm = d.get("passmark_cpu_mark"); r23 = d.get("cinebench_r23_multi") |
| 105 | + if y < "2006" and pm and pm > 1500: |
| 106 | + print(f" {d['name']!r} ({y}): passmark {pm} too high for era") |
| 107 | + if y < "2011" and r23 and r23 > 3000: |
| 108 | + print(f" {d['name']!r} ({y}): r23 {r23} too high for era") |
| 109 | + |
| 110 | +# --- 5. cross-source correlation outliers (KEY contamination detector) --- |
| 111 | +section("CPU cross-source ratio outliers (possible wrong-variant)") |
| 112 | +def collect(recs, fa, fb): |
| 113 | + return [(d["name"], d[fa], d[fb]) for p, fn, d in recs if d.get(fa) and d.get(fb)] |
| 114 | +for fa, fb in [("passmark_cpu_mark","cinebench_r23_multi"), |
| 115 | + ("passmark_cpu_mark","geekbench_multi"), |
| 116 | + ("cinebench_r23_multi","geekbench_multi"), |
| 117 | + ("cinebench_2024_multi","cinebench_r23_multi")]: |
| 118 | + out = mad_outliers(collect(cpus, fa, fb)) |
| 119 | + for label, ratio in out: |
| 120 | + print(f" [{fa}/{fb}] {label!r}: ratio={ratio}") |
| 121 | + |
| 122 | +# --- 6. GPU cross-source + sanity --- |
| 123 | +section("GPU cross-source ratio outliers + sanity") |
| 124 | +for fa, fb in [("passmark_g3d_mark","timespy_score"), |
| 125 | + ("timespy_score","blender_score"), |
| 126 | + ("fp32_tflops","timespy_score"), |
| 127 | + ("passmark_g3d_mark","fp32_tflops")]: |
| 128 | + for label, ratio in mad_outliers(collect(gpus, fa, fb)): |
| 129 | + print(f" [{fa}/{fb}] {label!r}: ratio={ratio}") |
| 130 | + |
| 131 | +print("\n(no lines under a section = clean)") |
| 132 | + |
| 133 | +if STRICT and HARD: |
| 134 | + print(f"\n❌ integrity gate: {len(HARD)} hard anomaly(ies) — blocking refresh.") |
| 135 | + sys.exit(1) |
| 136 | +if STRICT: |
| 137 | + print("\n✅ integrity gate: no hard anomalies.") |
0 commit comments