Files
felhom.eu/documentation/audits/forum-research-2026-10-12/spotcheck.py
T
admin 52b6599d97
gates / gates (push) Successful in 6m36s
Audit: three years of the PROHARDVER! home-server forum read against Felhom; R-990, R-991 filed
Read-only. fetch.py (polite, 5 s apart, 34 pages, all 200), parse.py (own text only; four parser
defects found by the controls and fixed), build.py + spotcheck.py (stdlib). tags.csv holds post
numbers and tags only - no username, author id, salt or post text in git. FINDINGS.md (2,908 posts,
195 people, 82 beginner threads in 7 clusters), FELHOM.md (catalog gap, hardware fit, 15 website
questions, objections, ten ranked recommendations), METHOD.md (controls, spot-check: role and
hardware below 85 % after two rounds, the table-driving splits 92.5 % and 97.3 %), REPORT.md.
R-990: operator rules on the recommendations. R-991: no buyer-readable hardware floor.

Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
2026-10-12 22:20:52 +02:00

95 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Compare the session's own blind re-tags of a random 5% sample with the helper tags (spec 9.6).
Mine: ~/research-cache/.../spot/mine_*.txt, one line per post:
num|role|beginner(y/n)|question_type|hardware|hardware_status|apps|apps_status|platform|platform_status|pain|budget_huf|resolved
Theirs: ~/research-cache/.../tags/tags_*.csv
Agreement per column = exact match of the normalised value (sets for ;-lists; hardware compared by
class, the bracketed model dropped). Prints overall agreement and agreement where either side is
non-empty (the strict view: empty-vs-empty agreement inflates the overall rate).
Usage: python3 spotcheck.py (v1: every column, helper tags/ vs spot/mine_*.txt)
python3 spotcheck.py --v2 (v2 re-tag: role, hardware, hardware_status;
tags_v2/v2_*.csv vs spot/mine_v2.txt num|role|hardware|hardware_status)
"""
import csv
import glob
import os
import re
import sys
CACHE = os.path.expanduser("~/research-cache/prohardver-hazi-szerver")
COLS = ["role", "beginner", "question_type", "hardware", "hardware_status", "apps", "apps_status",
"platform", "platform_status", "pain", "budget_huf", "resolved"]
SETS = {"question_type", "hardware", "apps", "platform", "pain"}
def norm(col, v):
v = (v or "").strip()
if col in SETS:
parts = [re.sub(r"\s*\(.*?\)", "", x).strip().lower() for x in v.split(";")]
return tuple(sorted(x for x in parts if x))
if col == "budget_huf":
return v.split(".")[0]
return v.lower()
def main_v2():
cols = ["role", "hardware", "hardware_status"]
theirs = {}
for f in glob.glob(os.path.join(CACHE, "tags_v2", "v2_*.csv")):
for r in csv.DictReader(open(f, encoding="utf-8")):
theirs[int(r["num"])] = r
mine = {}
for line in open(os.path.join(CACHE, "spot", "mine_v2.txt"), encoding="utf-8"):
v = line.rstrip("\n").split("|")
mine[int(v[0])] = dict(zip(cols, v[1:]))
nums = sorted(n for n in mine if n in theirs)
print("v2 sample=%d compared=%d" % (len(mine), len(nums)))
for c in cols:
agree = sum(norm(c, mine[n][c]) == norm(c, theirs[n].get(c)) for n in nums)
ne = [n for n in nums if norm(c, mine[n][c]) or norm(c, theirs[n].get(c))]
agree_ne = sum(norm(c, mine[n][c]) == norm(c, theirs[n].get(c)) for n in ne)
print("%-16s %7.1f%% %10.1f%% (n=%d)" % (c, 100.0 * agree / len(nums),
100.0 * agree_ne / len(ne) if ne else 100.0, len(ne)))
if os.environ.get("SHOW") == c:
for n in nums:
if norm(c, mine[n][c]) != norm(c, theirs[n].get(c)):
print(" #%d mine=%s theirs=%s" % (n, norm(c, mine[n][c]), norm(c, theirs[n].get(c))))
def main():
if "--v2" in sys.argv:
return main_v2()
theirs = {}
for f in glob.glob(os.path.join(CACHE, "tags", "tags_*.csv")):
for r in csv.DictReader(open(f, encoding="utf-8")):
r["beginner"] = "y" if (r.get("beginner_evidence") or "").strip() else "n"
theirs[int(r["num"])] = r
mine = {}
for f in glob.glob(os.path.join(CACHE, "spot", "mine_[0-9].txt")):
for line in open(f, encoding="utf-8"):
if not line.strip():
continue
v = line.rstrip("\n").split("|")
mine[int(v[0])] = dict(zip(COLS, v[1:]))
nums = sorted(n for n in mine if n in theirs)
print("sample=%d compared=%d (missing in helper tags: %s)" %
(len(mine), len(nums), sorted(set(mine) - set(theirs))))
print("%-16s %8s %18s" % ("column", "overall", "non-empty either"))
for c in COLS:
agree = sum(norm(c, mine[n][c]) == norm(c, theirs[n].get(c)) for n in nums)
ne = [n for n in nums if norm(c, mine[n][c]) or norm(c, theirs[n].get(c))]
agree_ne = sum(norm(c, mine[n][c]) == norm(c, theirs[n].get(c)) for n in ne)
print("%-16s %7.1f%% %10.1f%% (n=%d)" % (c, 100.0 * agree / len(nums),
100.0 * agree_ne / len(ne) if ne else 100.0, len(ne)))
if os.environ.get("SHOW"):
c = os.environ["SHOW"]
for n in nums:
a, b = norm(c, mine[n][c]), norm(c, theirs[n].get(c))
if a != b:
print(" #%d mine=%s theirs=%s" % (n, a, b))
if __name__ == "__main__":
main()