Files
rune/scripts/dichotomy_audit.py
T
Nick 29e7947282
CI Pipeline / documentation (push) Successful in 8s
CI Pipeline / best-practices (push) Successful in 8s
CI Pipeline / rust-surface (push) Failing after 13s
CI Pipeline / go-surface (push) Failing after 16s
CI Pipeline / gitea-offline (push) Successful in 17s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / lint (push) Canceled after 35s
CI Pipeline / unit-tests (push) Canceled after 35s
CI Pipeline / canary (push) Canceled after 36s
CI Pipeline / type-check (push) Canceled after 30s
Auto-version and Release / release (push) Canceled after 39s
deploy / deploy (push) Canceled after 26s
feat(audit): FULL-based headline per Nick directive — progress measured in finished modules
Headline is now module-based: FULL/total (22% — 43/192), function-level
demoted to secondary detail. Mixing complete and in-flight work in one
number flatters progress; FULL is the only honest denominator.
PARTIAL rising is the funnel filling (NOT TOUCHED -> PARTIAL on landing),
not completion stalling — FULL went 42->44 in the same window.
2026-10-13 05:33:46 +01:00

318 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""dichotomy_audit.py — the honest completion instrument for the dichotomy rebase.
Function-level, not directory-level: a module counts as covered only when its
PYTHON FUNCTIONS have identifiable counterparts in the Go/Rust implementation.
A Go directory with 3 structs and none of the module's 70 functions is a
facade, and this audit names it as one.
Matching rule (name-preserving ports): a Python function name matches when
its snake_case form, or its Go/Rust camelCase/PascalCase form, appears in the
candidate implementation sources. Deliberately strict — novel Go names that
don't echo the Python names do NOT count, because unverifiable "trust me,
it's in there somewhere" is exactly the failure mode this instrument exists
to kill.
Usage: python3 scripts/dichotomy_audit.py [--write]
--write regenerates docs/dichotomy-rebase-honest-audit.md
"""
from __future__ import annotations
import ast
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SRC = ROOT / "src" / "rune"
GO = ROOT / "src" / "go"
RUST = ROOT / "src" / "rust"
REPORT = ROOT / "docs" / "dichotomy-rebase-honest-audit.md"
CLASSIFICATION = ROOT / "store" / "rebase" / "classification.json"
TEST_PREFIXES = ("test_",)
# Modules that stay Python by decision (test infra stays pytest)
TEST_INFRA = {
"conftest", "test_truth", "test_adaptive_router", "test_canaries",
}
RETIRES = {"memory_env", "__main__"}
# The *_loader family is the programme's OWN ctypes FFI machinery — the
# delegation glue created by the ports themselves (broker_loader.py et al).
# Per the strangler-fig deletion doctrine they burn with the Python facades
# at stage 3; porting a loader to Rust is circular. Machinery, not port
# surface — same category as dunders.
FFI_MACHINERY = ("_loader",)
def snake_to_camel(name: str) -> list[str]:
"""Candidate identifier forms for a Python name in Go/Rust sources."""
parts = name.split("_")
camel = parts[0] + "".join(p.capitalize() for p in parts[1:])
pascal = "".join(p.capitalize() for p in parts)
return [name, camel, pascal]
def module_functions(path: Path) -> list[str]:
"""Every function and method name defined in the module."""
try:
tree = ast.parse(path.read_text(encoding="utf-8", errors="replace"))
except SyntaxError:
return []
names = []
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if node.name.startswith("__") and node.name.endswith("__"):
continue # dunders are runtime machinery, not port surface
names.append(node.name)
return names
def load_classification() -> dict:
if CLASSIFICATION.exists():
return json.loads(CLASSIFICATION.read_text())
return {}
def implementation_corpus() -> dict[str, str]:
"""module-kebab -> concatenated Go/Rust source text for that module."""
corpus: dict[str, str] = {}
for d in sorted(GO.iterdir()):
if not d.is_dir():
continue
chunks = []
for f in d.rglob("*.go"):
if f.name.endswith("_test.go"):
continue
chunks.append(f.read_text(encoding="utf-8", errors="replace"))
if chunks:
corpus[d.name] = "\n".join(chunks)
# Survivor packages (per-crate Go membranes under cognition/)
cog = GO / "cognition"
if cog.exists():
for d in sorted(cog.iterdir()):
if d.is_dir():
chunks = [f.read_text(encoding="utf-8", errors="replace")
for f in d.rglob("*.go") if not f.name.endswith("_test.go")]
if chunks:
corpus[f"cognition-{d.name}"] = "\n".join(chunks)
for d in sorted(RUST.iterdir()):
src = d / "src"
if src.exists():
# A module may have BOTH a Go and a Rust implementation
# (e.g. verification_layer: full Go port + partial Rust
# crate). Concatenate — never let one side shadow the other.
chunks = [f.read_text(encoding="utf-8", errors="replace")
for f in src.rglob("*.rs")]
if chunks:
corpus[d.name] = corpus.get(d.name, "") + "\n" + "\n".join(chunks)
return corpus
def module_to_keys(module_stem: str) -> list[str]:
"""Candidate implementation keys for a Python module stem."""
kebab = module_stem.replace("_", "-")
keys = [kebab, module_stem]
# Hyphenated legacy twins (drift-ledger.py vs drift_ledger.py) share ONE
# implementation; the underscore form is the crate/dir name.
if "-" in module_stem:
keys.append(module_stem.replace("-", "_"))
# Deep ports and multi-module binaries register aliases:
aliases = {
"rune_operator": ["operator", "operator-deep", "operator-support", "core-server"],
"rune_tui": ["tui"],
"rune_operator_cli": ["operator"],
# curator was ported to FULL Go (src/go/curator-core, a95c6ce1);
# without this alias the audit mislabels it NOT TOUCHED.
"curator": ["curator-core"],
"memory_store": ["memory-ops", "memory-store-deep"],
"warm_router": ["warm-router", "operator-support"],
"tool_executor": ["tool-executor", "local-tool-runtime", "tool-protocol", "tool-protocol-rw"],
"local_tool_runtime": ["local-tool-runtime", "operator"],
"provider_registry": ["provider-registry", "model-connect"],
"adaptive_router": ["adaptive-router", "operator-support"],
"hook_engine": ["hook-engine"],
"event_bus": ["event-bus"],
"event_envelope": ["event-bus"],
"message_queue": ["message-queue"],
"subsystem_health": ["subsystem-health"],
"parallel_processor": ["parallel-processor"],
"connection_pool": ["connection-pool"],
"recall_router": ["recall-router"],
"cron_engine": ["cron-engine"],
"cache": ["cache"],
"night_mode": ["night-mode"],
"work_manager": ["work-manager"],
"failure_auto_filer": ["failure-filer"],
"microkernel": ["microkernel"],
"construction_interface": ["cognition-cognition", "cognition"],
"construction_decision": ["cognition-cognition", "cognition"],
"rune_spine": ["cognition-spine", "cognition"],
"spine": ["cognition-spine", "cognition", "rune_spine"],
"memory_storage": ["cognition-storage", "cognition"],
"memory_retrieval": ["cognition-retrieval", "cognition"],
"scaffold_core": ["cognition-scaffold", "cognition"],
"evidence_provenance": ["cognition-provenance", "cognition"],
"memory_scoring": ["cognition-scoring", "cognition"],
"capability_scoring": ["cognition-capability", "cognition"],
"curator": ["curator-core"],
"token_counting": ["cognition-token", "cognition"],
"query_memory": ["cognition-query", "cognition"],
}
if module_stem in aliases:
keys = aliases[module_stem] + keys
# cognition_core broker.rs hosts many kernels
if module_stem in {
"tier_manager", "safety_override", "pins", "identity_anchors",
"abstraction", "growth_delta", "witness", "persona_gate",
"memory_consolidation", "drift_compression", "context_broker",
"persona_definition", "goal",
}:
keys.append("cognition_core")
keys.append("cognition-cognition")
return keys
def audit() -> dict:
classification = load_classification()
corpus = implementation_corpus()
rows = []
for path in sorted(SRC.glob("*.py")):
stem = path.stem
if stem in RETIRES:
rows.append({"module": stem, "lines": len(path.read_text(errors='replace').splitlines()),
"fns": 0, "matched": 0, "side": "RETIRES", "status": "RETIRES", "missing": []})
continue
if stem.endswith(FFI_MACHINERY) or stem == "ffi_query_memory_smoke":
rows.append({"module": stem, "lines": len(path.read_text(errors='replace').splitlines()),
"fns": 0, "matched": 0, "side": "FFI-MACHINERY", "status": "RETIRES", "missing": []})
continue
if stem.startswith(TEST_PREFIXES) or stem in TEST_INFRA:
rows.append({"module": stem, "lines": len(path.read_text(errors='replace').splitlines()),
"fns": 0, "matched": 0, "side": "TEST", "status": "TEST", "missing": []})
continue
fns = module_functions(path)
lines = len(path.read_text(errors="replace").splitlines())
entry = classification.get(stem) or classification.get(path.name) or {}
side = entry.get("side", entry.get("language", "unclassified"))
sources = []
for key in module_to_keys(stem):
if key in corpus:
sources.append(corpus[key])
blob = "\n".join(sources)
matched, missing = 0, []
for fn in fns:
hits = any(cand in blob for cand in snake_to_camel(fn))
if hits:
matched += 1
else:
missing.append(fn)
ratio = matched / len(fns) if fns else (1.0 if lines < 30 else 0.0)
if stem in RETIRES:
status = "RETIRES"
elif not blob:
status = "NOT TOUCHED"
elif ratio >= 0.8:
status = "FULL"
elif ratio >= 0.2:
status = "PARTIAL"
else:
status = "NOT TOUCHED"
rows.append({"module": stem, "lines": lines, "fns": len(fns),
"matched": matched, "side": side, "status": status, "missing": missing[:12]})
return {"rows": rows}
def summarize(result: dict) -> str:
rows = result["rows"]
runtime = [r for r in rows if r["status"] not in ("TEST", "RETIRES")]
full = [r for r in runtime if r["status"] == "FULL"]
partial = [r for r in runtime if r["status"] == "PARTIAL"]
untouched = [r for r in runtime if r["status"] == "NOT TOUCHED"]
total_fns = sum(r["fns"] for r in runtime)
total_matched = sum(r["matched"] for r in runtime)
total_lines = sum(r["lines"] for r in runtime)
fn_pct = 100 * total_matched / total_fns if total_fns else 0
out = []
out.append("# DICHOTOMY REBASE — HONEST AUDIT (function-level)")
out.append("")
out.append("_Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL only when 80%+ of its Python functions have name-preserving counterparts in the Go/Rust implementation — directories without the functions are facades and count as NOT TOUCHED._")
out.append("")
# HEADLINE (Nick, 2026-10-13): progress is measured in FINISHED modules
# (FULL), not half-done function matches — a percentage that mixes
# complete and in-flight work flatters progress.
total_mods = len(full) + len(partial) + len(untouched)
mod_pct = 100 * len(full) / total_mods if total_mods else 0
out.append(f"**FULL completion: {mod_pct:.0f}%** ({len(full)}/{total_mods} runtime modules at FULL)")
out.append("")
out.append(f"Function-level detail: {fn_pct:.0f}% ({total_matched}/{total_fns} runtime functions matched) — secondary; includes in-flight work")
out.append("")
out.append("| Status | Modules | Lines | Meaning |")
out.append("|---|---|---|---|")
out.append(f"| FULL | {len(full)} | {sum(r['lines'] for r in full)} | 80%+ of functions ported |")
out.append(f"| PARTIAL | {len(partial)} | {sum(r['lines'] for r in partial)} | 20–80% — real logic exists, gaps named below |")
out.append(f"| NOT TOUCHED | {len(untouched)} | {sum(r['lines'] for r in untouched)} | no name-preserving implementation |")
out.append(f"| Total runtime | {len(runtime)} | {total_lines} | |")
out.append("")
if untouched:
out.append("## NOT TOUCHED — the port queue, in priority order (largest first)")
out.append("")
out.append("| Module | Lines | Fns | Side |")
out.append("|---|---|---|---|")
for r in sorted(untouched, key=lambda r: -r["lines"]):
out.append(f"| {r['module']} | {r['lines']} | {r['fns']} | {r['side']} |")
out.append("")
if partial:
out.append("## PARTIAL — gaps to close (matched/total fns; first missing named)")
out.append("")
out.append("| Module | Lines | Coverage | Missing (first named) |")
out.append("|---|---|---|---|")
for r in sorted(partial, key=lambda r: -(r["fns"] - r["matched"])):
miss = ", ".join(r["missing"][:6]) if r["missing"] else "(structural)"
out.append(f"| {r['module']} | {r['lines']} | {r['matched']}/{r['fns']} | {miss} |")
out.append("")
out.append("## FULL — ported, parity-gated")
out.append("")
out.append("| Module | Lines | Fns | Side |")
out.append("|---|---|---|---|")
for r in sorted(full, key=lambda r: -r["lines"]):
out.append(f"| {r['module']} | {r['lines']} | {r['matched']}/{r['fns']} | {r['side']} |")
out.append("")
return "\n".join(out)
def main() -> int:
write = "--write" in sys.argv
result = audit()
report = summarize(result)
if write:
REPORT.write_text(report)
print(f"written: {REPORT}")
# stderr summary for the session log
rows = result["rows"]
runtime = [r for r in rows if r["status"] not in ("TEST", "RETIRES")]
tf = sum(r["fns"] for r in runtime)
tm = sum(r["matched"] for r in runtime)
n_full = sum(1 for r in runtime if r['status'] == 'FULL')
n_total = len(runtime)
print(f"FULL: {100*n_full/n_total if n_total else 0:.0f}% ({n_full}/{n_total} modules) | "
f"fn-level: {tm}/{tf} ({100*tm/tf if tf else 0:.0f}%) | "
f"PARTIAL {sum(1 for r in runtime if r['status']=='PARTIAL')} | "
f"NOT TOUCHED {sum(1 for r in runtime if r['status']=='NOT TOUCHED')}")
return 0
if __name__ == "__main__":
raise SystemExit(main())