Files
tai/taicpp/tools/parity_ledger.py
T

111 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Parity ledger: every tai.py symbol against the C++ tree.
Usage: tools/parity_ledger.py [--check] (writes build/parity/ledger.json and ledger.md)
Status per symbol:
missing no identifier in src/ or main.cpp matches the normalized name
name-only an identifier matches; behavior is NOT verified by this tool
Verification (diff of real outputs against tai.py) is a separate, later status
and is never inferred here. --check exits 1 while any symbol is missing.
"""
import ast
import json
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
PY = os.path.join(os.path.dirname(ROOT), "tai.py")
OUT = os.path.join(ROOT, "build", "parity")
def norm(name):
return re.sub(r"[^a-z0-9]", "", name.lower())
def cpp_idents():
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:
with open(path, encoding="utf-8", errors="replace") as fh:
idents.update(norm(t) for t in re.findall(r"[A-Za-z_][A-Za-z0-9_]*", fh.read()))
return idents
def python_symbols(tree):
out = []
for node in 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 tool_names(tree):
names = []
for node in ast.walk(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):
names.append((arg.value, node.lineno))
return names
def flag_names(tree):
flags = []
for node in ast.walk(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("--"):
flags.append((arg.value, node.lineno))
return flags
def build():
with open(PY, encoding="utf-8") as fh:
tree = ast.parse(fh.read())
idents = cpp_idents()
rows = []
for kind, name, line in python_symbols(tree):
leaf = name.split(".")[-1]
hit = norm(name.replace(".", "")) in idents or (kind != "class" and len(norm(leaf)) > 5 and norm(leaf) in idents)
rows.append({"kind": kind, "name": name, "line": line, "status": "name-only" if hit else "missing"})
for name, line in tool_names(tree):
rows.append({"kind": "tool", "name": name, "line": line, "status": "name-only" if norm(name) in idents else "missing"})
for name, line in flag_names(tree):
rows.append({"kind": "flag", "name": name, "line": line, "status": "name-only" if norm(name) in idents else "missing"})
return rows
def main():
rows = build()
os.makedirs(OUT, exist_ok=True)
with open(os.path.join(OUT, "ledger.json"), "w") as fh:
json.dump(rows, fh, indent=1)
kinds = sorted({r["kind"] for r in rows})
lines = ["# Parity ledger (generated, do not edit)", "",
"Source: tai.py. `name-only` means an identifier matches, NOT that behavior is verified.", "",
"| kind | total | name-only | missing |", "|---|---|---|---|"]
for kind in kinds:
sub = [r for r in rows if r["kind"] == kind]
have = sum(1 for r in sub if r["status"] == "name-only")
lines.append("| %s | %d | %d | %d |" % (kind, len(sub), have, len(sub) - have))
total_missing = sum(1 for r in rows if r["status"] == "missing")
lines += ["", "Total symbols: %d, missing: %d, verified: 0" % (len(rows), total_missing)]
with open(os.path.join(OUT, "ledger.md"), "w") as fh:
fh.write("\n".join(lines) + "\n")
print("\n".join(lines))
return 1 if "--check" in sys.argv and total_missing else 0
if __name__ == "__main__":
sys.exit(main())