Skip to content

Commit 4cfccf1

Browse files
committed
feat(h25): honest categorisation audit + closure report
Closes V6 H25 β€” the meta-honesty audit that labels every test in tests/v4 + tests/v5 + vbgui/e2e/scenarios into: 🟒 math-effect β€” asserts on a real numerical quantity (loss, weight delta, entropy, mask effect, dtype cast, bit-equality bounds, …). 🟑 propagation β€” asserts that a value made it through the pipeline (status=="ok", path equality, "true" string echoes, structural extras presence). πŸ”΄ decorative β€” testid exists / no crash / length > 0 only. Tooling: - scripts/audit_v3_v4_v5.py walks the test trees with an AST walker for Python and a regex-per-test slicer for *.spec.ts. Each function body is run through MATH_PATTERNS / PROPAGATION_PATTERNS heuristics and labelled. - Writes tests/fixtures/honesty_audit_v6.md with the count table, per-file breakdown, and a top-50 list of decorative tests as V7 follow-up candidates. Baseline counts (this commit): 🟒 math-effect: 294 🟑 propagation: 35 πŸ”΄ decorative: 1119 total: 1448 The decorative bucket is large because many tests are pure existence assertions (testid present, dropdown renders, palette has chips, …) that don't have a math-claim attached β€” those are honest UX gates, not failures. The closure report explicitly invites V7 tightening of this bucket where a math-claim is implicit. Tests: - pytest tests/test_audit_v3_v4_v5.py: 3/3 β€” script runs, produces a report with the expected sections, classifier labels math-effect and propagation snippets correctly.
1 parent b01f5a3 commit 4cfccf1

3 files changed

Lines changed: 774 additions & 0 deletions

File tree

β€Žscripts/audit_v3_v4_v5.pyβ€Ž

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"""H25: honest categorisation audit across V3/V4/V5/V6 tests.
2+
3+
Walks the relevant test directories and labels each `def test_*`
4+
function by the kind of assertion it makes:
5+
6+
🟒 math-effect β€” asserts on a numerical quantity that requires
7+
real math to land (loss, weight delta, norm,
8+
routing entropy, mask effect, dtype cast,
9+
bit-equality, etc.).
10+
🟑 propagation β€” asserts that a config value made it through a
11+
pipeline (string echo, status=="ok", path
12+
matching, structural extras presence).
13+
πŸ”΄ decorative β€” asserts on cosmetics only (testid exists, no
14+
crash, length>0, opt object is non-null).
15+
16+
Heuristic-based classifier; intended for quick triage and as a
17+
regression baseline. Writes `tests/fixtures/honesty_audit_v6.md`.
18+
19+
Run: python -m scripts.audit_v3_v4_v5
20+
"""
21+
22+
from __future__ import annotations
23+
24+
import argparse
25+
import ast
26+
import collections
27+
import pathlib
28+
import re
29+
import sys
30+
31+
REPO = pathlib.Path(__file__).resolve().parent.parent
32+
33+
MATH_PATTERNS = re.compile(
34+
r"\b("
35+
r"loss|losses|weight_delta|grad_norm|entropy|mask|cast|"
36+
r"dtype_actual|per_rank|routing|load_balance|l2_diff|"
37+
r"cos_sim|bit_identical|within|approx|isclose|"
38+
r"finite|inf\b|isnan"
39+
r")",
40+
re.IGNORECASE,
41+
)
42+
PROPAGATION_PATTERNS = re.compile(
43+
r"\b("
44+
r"status\s*==\s*['\"]ok|saved_path|loaded_path|"
45+
r"toBe\(['\"](true|false|null)|toContain|toBe\(['\"][a-z_]+['\"]"
46+
r"|equals|==.*['\"][a-z_]+['\"]"
47+
r")",
48+
re.IGNORECASE,
49+
)
50+
51+
52+
def classify_function_body(body: str) -> str:
53+
if MATH_PATTERNS.search(body):
54+
return "math-effect"
55+
if PROPAGATION_PATTERNS.search(body):
56+
return "propagation"
57+
return "decorative"
58+
59+
60+
def walk_python_tests(root: pathlib.Path):
61+
for path in root.rglob("test_*.py"):
62+
if "__pycache__" in path.parts:
63+
continue
64+
try:
65+
tree = ast.parse(path.read_text(encoding="utf-8"))
66+
except SyntaxError:
67+
continue
68+
for node in ast.walk(tree):
69+
if (isinstance(node, ast.FunctionDef)
70+
and node.name.startswith("test_")):
71+
body = ast.unparse(node)
72+
yield (path.relative_to(REPO), node.name,
73+
classify_function_body(body))
74+
75+
76+
def walk_e2e_specs(root: pathlib.Path):
77+
for path in root.rglob("*.spec.ts"):
78+
text = path.read_text(encoding="utf-8")
79+
# Naive regex per test(...) block; good enough for triage.
80+
for m in re.finditer(
81+
r"test\(\s*['\"`]([^'\"`]+)['\"`]\s*,",
82+
text):
83+
name = m.group(1)
84+
start = m.end()
85+
# Slice the rest of the file for body matches; close
86+
# enough since most spec.ts files have one test per
87+
# `test(...)` declaration with short bodies.
88+
tail = text[start:start + 4000]
89+
yield (path.relative_to(REPO), name,
90+
classify_function_body(tail))
91+
92+
93+
def main() -> int:
94+
parser = argparse.ArgumentParser()
95+
parser.add_argument(
96+
"--out",
97+
default=str(REPO / "tests" / "fixtures" / "honesty_audit_v6.md"))
98+
args = parser.parse_args()
99+
100+
py_roots = [REPO / "tests" / "v4", REPO / "tests" / "v5"]
101+
e2e_root = REPO / "vbgui" / "e2e" / "scenarios"
102+
103+
rows: list[tuple[str, str, str]] = []
104+
for r in py_roots:
105+
if r.exists():
106+
for relpath, name, label in walk_python_tests(r):
107+
rows.append((str(relpath), name, label))
108+
if e2e_root.exists():
109+
for relpath, name, label in walk_e2e_specs(e2e_root):
110+
rows.append((str(relpath), name, label))
111+
112+
counts = collections.Counter(r[2] for r in rows)
113+
glyph = {"math-effect": "🟒", "propagation": "🟑",
114+
"decorative": "πŸ”΄"}
115+
116+
out_path = pathlib.Path(args.out)
117+
out_path.parent.mkdir(parents=True, exist_ok=True)
118+
with out_path.open("w", encoding="utf-8") as f:
119+
f.write("# V6 honesty audit\n\n")
120+
f.write("Classifier from `scripts/audit_v3_v4_v5.py` "
121+
"(heuristic).\n\n")
122+
f.write("| Category | Count |\n|---|---|\n")
123+
for k in ("math-effect", "propagation", "decorative"):
124+
f.write(f"| {glyph[k]} {k} | {counts.get(k, 0)} |\n")
125+
f.write(f"\nTotal: {sum(counts.values())}\n\n")
126+
f.write("## By file\n\n")
127+
by_file = collections.defaultdict(list)
128+
for path, name, label in rows:
129+
by_file[path].append((name, label))
130+
for path in sorted(by_file):
131+
tests = by_file[path]
132+
by_label = collections.Counter(t[1] for t in tests)
133+
f.write(f"### {path}\n")
134+
f.write(f"{glyph['math-effect']}={by_label.get('math-effect',0)} "
135+
f"{glyph['propagation']}={by_label.get('propagation',0)} "
136+
f"{glyph['decorative']}={by_label.get('decorative',0)}"
137+
f" / total {len(tests)}\n\n")
138+
f.write("\n## V7 follow-ups\n\n")
139+
decoratives = [r for r in rows if r[2] == "decorative"]
140+
if decoratives:
141+
f.write(f"Decorative-only tests ({len(decoratives)}) β€” "
142+
"candidates for tightening:\n\n")
143+
for path, name, _ in decoratives[:50]:
144+
f.write(f"- `{path}::{name}`\n")
145+
if len(decoratives) > 50:
146+
f.write(f"- … {len(decoratives) - 50} more\n")
147+
else:
148+
f.write("None.\n")
149+
150+
print(f"wrote {out_path}")
151+
print("counts:", dict(counts))
152+
return 0
153+
154+
155+
if __name__ == "__main__":
156+
raise SystemExit(main())

0 commit comments

Comments
Β (0)