Files
tai/taicpp/tools/parity_ledger.py
T

324 lines
14 KiB
Python
Executable File

#!/usr/bin/env python3
"""Parity ledger: every tai.py symbol against the C++ tree and the differential results.
Usage: tools/parity_ledger.py [--check] [--publish]
Writes build/parity/ledger.json, ledger.md and PARITY.md (all under build/).
--publish also copies the generated PARITY.md over taicpp/PARITY.md (the committed copy).
--check exits 1 while ANY symbol is missing or unverified, a case names a symbol the
ledger does not know, or the differential results are absent or stale.
Status per symbol (strongest first):
verified a passing differential case (tools/parity_diff.py, unfiltered run, tai.py and
detz hashes equal to the current files) names the symbol. Facets list WHAT the
case verified (`parse`, `output`, ...); verified never means "all behavior".
name-only an identifier in src/ or main.cpp matches the normalized name. Behavior unproven.
missing no identifier matches.
Only a differential case sets `verified`; nothing here infers it.
Symbol ids are `kind:name`, optionally `#facet` in case files:
class:Store method:Store.load function:main tool:shell flag:--version
setting:model command:/help
"""
import ast
import hashlib
import json
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
REPO = os.path.dirname(ROOT)
PY = os.path.join(REPO, "tai.py")
OUT = os.path.join(ROOT, "build", "parity")
DETZ = os.environ.get("DETZ_BIN", os.path.join(ROOT, "build", "detz"))
CASES_DIR = os.path.join(ROOT, "tests", "diff", "cases")
PARITY_MD = os.path.join(ROOT, "PARITY.md")
KINDS = ("class", "method", "function", "tool", "flag", "setting", "command")
def norm(name):
return re.sub(r"[^a-z0-9]", "", name.lower())
def sha256_file(path):
if not os.path.isfile(path):
return "missing"
digest = hashlib.sha256()
with open(path, "rb") as fh:
for block in iter(lambda: fh.read(1 << 20), b""):
digest.update(block)
return digest.hexdigest()
class PySymbols:
"""Symbols extracted from tai.py with ast."""
def __init__(self, path):
with open(path, encoding="utf-8") as fh:
self.tree = ast.parse(fh.read())
def code(self):
out = []
for node in self.tree.body:
if isinstance(node, ast.ClassDef):
out.append(("class", node.name, node.lineno))
for sub in node.body:
if isinstance(sub, (ast.FunctionDef, ast.AsyncFunctionDef)):
out.append(("method", "%s.%s" % (node.name, sub.name), sub.lineno))
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
out.append(("function", node.name, node.lineno))
return out
def tools(self):
out = []
for node in ast.walk(self.tree):
if isinstance(node, ast.Call) and getattr(node.func, "id", "") == "tool_schema" and node.args:
arg = node.args[0]
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
out.append(("tool", arg.value, node.lineno))
return out
def flags(self):
out = []
for node in ast.walk(self.tree):
if isinstance(node, ast.Call) and getattr(node.func, "attr", "") == "add_argument":
for arg in node.args:
if isinstance(arg, ast.Constant) and isinstance(arg.value, str) and arg.value.startswith("--"):
out.append(("flag", arg.value, node.lineno))
return out
def settings(self):
out = []
for node in self.tree.body:
if isinstance(node, ast.Assign) and any(getattr(t, "id", "") == "SETTINGS_CATALOG" for t in node.targets):
for sub in ast.walk(node):
if isinstance(sub, ast.Call) and getattr(sub.func, "id", "") == "setting_spec" and sub.args:
arg = sub.args[0]
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
out.append(("setting", arg.value, sub.lineno))
return out
def commands(self):
out = []
for node in self.tree.body:
if isinstance(node, ast.Assign) and any(getattr(t, "id", "") == "HELP_TEXT" for t in node.targets):
text = "".join(c.value for c in ast.walk(node.value) if isinstance(c, ast.Constant) and isinstance(c.value, str))
seen = set()
for line in text.splitlines():
m = re.match(r"\s+(/[a-z]+)", line)
if m and m.group(1) not in seen:
seen.add(m.group(1))
out.append(("command", m.group(1), node.lineno))
return out
def all(self):
return self.code() + self.tools() + self.flags() + self.settings() + self.commands()
class CppIndex:
"""Normalized identifiers found in the C++ tree."""
def __init__(self):
self.idents = set()
files = [os.path.join(ROOT, "main.cpp")]
for base, _dirs, names in os.walk(os.path.join(ROOT, "src")):
files += [os.path.join(base, n) for n in names if n.endswith((".cpp", ".h", ".hpp"))]
for path in files:
if os.path.isfile(path):
with open(path, encoding="utf-8", errors="replace") as fh:
self.idents.update(norm(t) for t in re.findall(r"[A-Za-z_][A-Za-z0-9_]*", fh.read()))
def has(self, kind, name):
if kind in ("tool", "setting"):
return norm(name) in self.idents
if kind == "flag":
return norm(name) in self.idents
if kind == "command":
return norm(name.lstrip("/")) in self.idents and len(norm(name)) > 3
leaf = name.split(".")[-1]
if norm(name.replace(".", "")) in self.idents:
return True
return kind != "class" and len(norm(leaf)) > 5 and norm(leaf) in self.idents
class Evidence:
"""Passing differential cases, and whether they may be trusted for the current files."""
def __init__(self):
self.path = os.path.join(OUT, "diff-results.json")
self.problems = []
self.by_symbol = {} # "kind:name" -> {"cases": [ids], "facets": set}
self.cases = {} # id -> result dict
self.meta = None
if not os.path.isfile(self.path):
self.problems.append("no differential results (run tools/parity_diff.py)")
return
with open(self.path) as fh:
doc = json.load(fh)
self.meta = doc["meta"]
now_py, now_detz = sha256_file(PY), sha256_file(self.meta.get("detz_path", DETZ))
if self.meta["tai_sha"] != now_py:
self.problems.append("differential results are stale: tai.py changed since the run")
if self.meta["detz_sha"] != now_detz:
self.problems.append("differential results are stale: detz binary changed or missing since the run")
if self.meta.get("no_llm"):
self.problems.append("last differential run skipped LLM cases (--no-llm)")
trusted = not any("stale" in p for p in self.problems)
for res in doc["results"]:
self.cases[res["id"]] = res
if res["status"] != "pass" or not trusted:
continue
for sym in res["symbols"]:
base, _, facet = sym.partition("#")
slot = self.by_symbol.setdefault(base, {"cases": [], "facets": set()})
slot["cases"].append(res["id"])
slot["facets"].add(facet or "all")
class Ledger:
def __init__(self):
self.py = PySymbols(PY)
self.cpp = CppIndex()
self.evidence = Evidence()
self.rows = []
self.case_symbol_errors = []
self._build()
def _build(self):
known = set()
for kind, name, line in self.py.all():
sid = "%s:%s" % (kind, name)
if sid in known:
continue
known.add(sid)
ev = self.evidence.by_symbol.get(sid)
if ev:
status, facets, cases = "verified", sorted(ev["facets"]), ev["cases"]
else:
status, facets, cases = ("name-only" if self.cpp.has(kind, name) else "missing"), [], []
self.rows.append({"id": sid, "kind": kind, "name": name, "line": line, "status": status, "facets": facets, "cases": cases})
for cid, res in self.evidence.cases.items():
for sym in res["symbols"]:
if sym.partition("#")[0] not in known:
self.case_symbol_errors.append("case %s names unknown symbol %s" % (cid, sym))
def count(self, kind=None):
rows = [r for r in self.rows if kind is None or r["kind"] == kind]
tally = {"total": len(rows), "verified": 0, "name-only": 0, "missing": 0}
for r in rows:
tally[r["status"]] += 1
return tally
def problems(self):
out = list(self.evidence.problems) + self.case_symbol_errors
t = self.count()
if t["missing"]:
out.append("%d symbols missing" % t["missing"])
if t["name-only"]:
out.append("%d symbols name-only (unverified)" % t["name-only"])
return out
class Writer:
def __init__(self, ledger):
self.ledger = ledger
def tally_table(self):
lines = ["| kind | total | verified | name-only | missing |", "|---|---|---|---|---|"]
for kind in KINDS + (None,):
t = self.ledger.count(kind)
lines.append("| %s | %d | %d | %d | %d |" % (kind or "**all**", t["total"], t["verified"], t["name-only"], t["missing"]))
return lines
def parity_md(self):
led, ev = self.ledger, self.ledger.evidence
meta = ev.meta or {}
lines = ["# Parity detz vs tai.py (generated, do not edit)", "",
"Generated by `tools/parity_ledger.py` from the symbol ledger and the last differential run.",
"Only a passing differential case (`tools/parity_diff.py`) makes a symbol `verified`; a facet",
"after the symbol says what that case proved (for example `parse` means argument parsing only).",
"`name-only` means an identifier matches in the C++ tree and NOTHING about behavior is known.", "",
"- tai.py sha256: `%s`" % sha256_file(PY)[:16],
"- detz sha256: `%s`" % sha256_file(meta.get("detz_path", DETZ))[:16],
"- differential run: %s (tai.py `%s`, detz `%s`)" % (meta.get("ts", "none"), meta.get("tai_sha", "-")[:16], meta.get("detz_sha", "-")[:16]), ""]
if ev.problems:
lines += ["## Evidence problems", ""] + ["- " + p for p in ev.problems] + [""]
lines += ["## Summary", ""] + self.tally_table() + [""]
results = list(ev.cases.values())
counts = {}
for r in results:
counts[r["status"]] = counts.get(r["status"], 0) + 1
lines += ["## Differential cases", "", "Counts: " + (", ".join("%s=%d" % kv for kv in sorted(counts.items())) or "none"), ""]
failing = [r for r in results if r["status"] in ("fail", "xfail", "xpass")]
if failing:
lines += ["| case | status | reason |", "|---|---|---|"]
for r in sorted(failing, key=lambda x: x["id"]):
reason = r["expected_fail"] or (r["problems"][0] if r["problems"] else "")
lines.append("| %s | %s | %s |" % (r["id"], r["status"], reason.replace("|", "/")[:120]))
lines.append("")
tm = os.path.join(OUT, "tests-map.json")
if os.path.isfile(tm):
with open(tm) as fh:
cov = json.load(fh)["summary"]
lines += ["## Python test-suite coverage", "",
"%d test classes, %d tests; tests reproduced by a passing diff case: %d (in %d classes); classes with no covered test: %d."
% (cov["classes"], cov["tests"], cov["tests_covered"], cov["classes_covered"], cov["classes_uncovered"]),
"Full table: `build/parity/tests-map.md`.", ""]
for kind in ("flag", "tool", "setting", "command"):
rows = [r for r in led.rows if r["kind"] == kind]
lines += ["## %ss (%d)" % (kind, len(rows)), "", "| name | status | facets | cases |", "|---|---|---|---|"]
for r in sorted(rows, key=lambda x: x["name"]):
lines.append("| `%s` | %s | %s | %s |" % (r["name"], r["status"], ",".join(r["facets"]), ",".join(r["cases"][:3])))
lines.append("")
miss = {}
for r in led.rows:
if r["kind"] == "method" and r["status"] == "missing":
miss.setdefault(r["name"].split(".")[0], 0)
miss[r["name"].split(".")[0]] += 1
top = sorted(miss.items(), key=lambda kv: -kv[1])[:25]
lines += ["## Classes with the most missing methods (top 25 of %d)" % len(miss), "", "| class | missing methods |", "|---|---|"]
lines += ["| %s | %d |" % kv for kv in top]
lines.append("")
return "\n".join(lines)
def ledger_md(self):
lines = ["# Parity ledger (generated, do not edit)", ""] + self.tally_table() + [""]
lines += ["| id | status | facets | cases |", "|---|---|---|---|"]
for r in self.ledger.rows:
lines.append("| `%s` | %s | %s | %s |" % (r["id"], r["status"], ",".join(r["facets"]), ",".join(r["cases"][:3])))
return "\n".join(lines) + "\n"
def write(self, publish):
os.makedirs(OUT, exist_ok=True)
with open(os.path.join(OUT, "ledger.json"), "w") as fh:
json.dump(self.ledger.rows, fh, indent=1)
with open(os.path.join(OUT, "ledger.md"), "w") as fh:
fh.write(self.ledger_md())
text = self.parity_md()
with open(os.path.join(OUT, "PARITY.md"), "w") as fh:
fh.write(text)
if publish:
with open(PARITY_MD, "w") as fh:
fh.write(text)
def main():
ledger = Ledger()
Writer(ledger).write("--publish" in sys.argv)
print("\n".join(Writer(ledger).tally_table()))
problems = ledger.problems()
for p in problems:
print("PROBLEM: %s" % p)
if "--check" in sys.argv:
if problems:
print("parity-check: FAIL (%d problems)" % len(problems))
return 1
print("parity-check: OK")
return 0
if __name__ == "__main__":
sys.exit(main())