mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-08-10 17:50:36 +00:00
348 lines
15 KiB
Python
348 lines
15 KiB
Python
#!/usr/bin/env python3
|
||
"""Route-Repair v1.2: hybrid spectral + text/goal-state obstruction classification.
|
||
|
||
Gate: if matrix_size >= 3 and rank > 0 → spectral flexure retrieval
|
||
else → text/goal-state obstruction classifier
|
||
"""
|
||
|
||
import hashlib, json, math, os, re, subprocess, sys, time, uuid
|
||
from collections import Counter, defaultdict
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "."))
|
||
from lean_trace_bridge_v2 import instrument_theorem, prove
|
||
from failure_flexure_bank import FAILURE_THEOREMS
|
||
|
||
WORKER_URL = os.environ.get("CANARY_WORKER_URL", "http://100.110.163.82:8787")
|
||
PROOF_SERVER_TOKEN = os.environ.get("PROOF_SERVER_TOKEN", "")
|
||
if not PROOF_SERVER_TOKEN:
|
||
tf = os.environ.get("PROOF_SERVER_TOKEN_FILE", os.path.expanduser("~/.config/ene/language-proof-server.token"))
|
||
try: PROOF_SERVER_TOKEN = Path(tf).read_text().strip()
|
||
except: pass
|
||
|
||
LIBRARY_SESSION = "a4a0eb20-93fe-413e-8e0b-50334bb778d8"
|
||
|
||
V2_FEATURES = ["matrix_size","rank","spectral_gap","laplacian_zero_count","density",
|
||
"adjacency_eigenvalue_max","adjacency_eigenvalue_second",
|
||
"laplacian_eigenvalue_max","laplacian_eigenvalue_min",
|
||
"singular_value_max","trace","frobenius_norm"]
|
||
|
||
REPAIR_POLICY = {
|
||
"missing_rewrite_direction": ["rw [← %s]", "rw [%s]"],
|
||
"missing_assumption_bridge": ["assumption", "exact %s", "apply %s"],
|
||
"arithmetic_gap": ["omega"],
|
||
"case_split_missing": ["cases %s", "constructor"],
|
||
"induction_incomplete": ["induction %s"],
|
||
"simplifier_gap": ["simp", "simp [%s]"],
|
||
"coercion_mismatch": ["norm_cast", "exact %s"],
|
||
"order_inequality_gap": ["omega"],
|
||
}
|
||
|
||
|
||
def compute_spectral(matrix):
|
||
n=len(matrix)
|
||
if n==0: return {}
|
||
sym=[[(matrix[i][j]+matrix[j][i])/2 for j in range(n)] for i in range(n)]
|
||
lap=[[sum(sym[i]) if i==j else -sym[i][j] for j in range(n)] for i in range(n)]
|
||
def pe(m):
|
||
v=[1.0/math.sqrt(n)]*n
|
||
for _ in range(100):
|
||
vn=[sum(m[i][j]*v[j] for j in range(n)) for i in range(n)]
|
||
nm=math.sqrt(sum(x*x for x in vn))
|
||
v=[x/nm for x in vn] if nm>0 else v
|
||
num=sum(v[i]*sum(m[i][j]*v[j] for j in range(n)) for i in range(n))
|
||
return num/max(sum(v[i]*v[i] for i in range(n)),1e-12)
|
||
sm=pe(sym); lm=pe(lap)
|
||
sh=[[sym[i][j]-0.9*sm*(i==j) for j in range(n)] for i in range(n)]
|
||
sm2=pe(sh); gap=sm-max(0,sm-sm2)
|
||
ev_second=sm-max(0,sm-sm2)
|
||
neg=[[-lap[i][j] for j in range(n)] for i in range(n)]
|
||
nm=pe(neg)
|
||
ata=[[sum(matrix[k][i]*matrix[k][j] for k in range(n)) for j in range(n)] for i in range(n)]
|
||
sva=math.sqrt(max(0,pe(ata)))
|
||
rank=sum(1 for row in matrix if sum(row)>0)
|
||
total=sum(sum(r) for r in matrix)
|
||
frob=math.sqrt(sum(cell*cell for row in matrix for cell in row))
|
||
lap0=sum(1 for i in range(n) if abs(sum(matrix[i])-matrix[i][i])<1e-9)
|
||
return {"matrix_size":n,"rank":rank,"spectral_gap":round(gap,6),"density":round(total/max(n*n,1),6),
|
||
"trace":sum(matrix[i][i] for i in range(n)),"frobenius_norm":round(frob,6),
|
||
"laplacian_zero_count":lap0,"adjacency_eigenvalue_max":round(sm,6),
|
||
"adjacency_eigenvalue_second":round(ev_second,6),
|
||
"laplacian_eigenvalue_max":round(lm,6),"laplacian_eigenvalue_min":round(-nm,6),
|
||
"singular_value_max":round(sva,6)}
|
||
|
||
|
||
def is_degenerate(features: dict) -> bool:
|
||
return features.get("matrix_size", 0) <= 2 or features.get("rank", 0) == 0 or features.get("spectral_gap", 0) == 0
|
||
|
||
|
||
def extract_text_features(code: str) -> dict:
|
||
"""Extract text/goal-state features from a Lean theorem for obstruction classification."""
|
||
code_lower = code.lower()
|
||
tactic = "unknown"
|
||
if "by " in code or "by\n" in code:
|
||
m = re.search(r'by\s+(\S+)', code)
|
||
if m: tactic = m.group(1)
|
||
|
||
# Extract hypotheses
|
||
hyps = []
|
||
for m in re.finditer(r'\(([^)]+:\s*[^)]+)\)', code):
|
||
parts = m.group(1).split(":")
|
||
if len(parts) >= 2:
|
||
names = parts[0].strip().split()
|
||
typ = ":".join(parts[1:]).strip()
|
||
for n in names:
|
||
hyps.append((n.strip(), typ))
|
||
|
||
hyp_types = [t for _, t in hyps]
|
||
hyp_names = [n for n, _ in hyps]
|
||
|
||
# Goal analysis
|
||
goal = code # use full code for pattern detection since goal extraction is unreliable
|
||
|
||
has_equality = any("=" in t for t in hyp_types) or "=" in goal
|
||
has_order = any(t in ["Nat","ℕ","Int","ℤ"] for t in hyp_types) and any(c in goal for c in "≤≥<>")
|
||
has_arithmetic = any(c in code for c in "+-*/") or "omega" in tactic
|
||
has_constructor = "∧" in goal or "∨" in goal or "→" in goal or "↔" in goal
|
||
has_inductive = any(n in code_lower for n in ["nat","list","option"])
|
||
|
||
op_counts = {}
|
||
for c in "+-*/^∧∨→¬∀∃≤≥=":
|
||
op_counts[c] = code.count(c)
|
||
|
||
return {
|
||
"tactic": tactic, "goal": goal[:100],
|
||
"hypothesis_count": len(hyps), "hypothesis_names": hyp_names[:5],
|
||
"hypothesis_types": hyp_types[:5],
|
||
"has_equality": has_equality, "has_order": has_order,
|
||
"has_arithmetic": has_arithmetic, "has_constructor": has_constructor,
|
||
"has_inductive": has_inductive,
|
||
"operator_counts": {k: v for k, v in op_counts.items() if v > 0},
|
||
}
|
||
|
||
|
||
def classify_obstruction(tf: dict) -> str:
|
||
"""Classify obstruction type from text features — ordered by specificity."""
|
||
tactic = tf.get("tactic", "")
|
||
has_eq = tf.get("has_equality", False)
|
||
has_order = tf.get("has_order", False)
|
||
has_arith = tf.get("has_arithmetic", False)
|
||
has_constructor = tf.get("has_constructor", False)
|
||
has_inductive = tf.get("has_inductive", False)
|
||
n_hyps = tf.get("hypothesis_count", 0)
|
||
goal = tf.get("goal", "")
|
||
|
||
# RW with equality → rewrite direction
|
||
if tactic in ("rw", "rw_simp") and has_eq:
|
||
return "missing_rewrite_direction"
|
||
# Constructor goal with rfl/simp → case split
|
||
if has_constructor or ("∧" in goal or "∨" in goal or "→" in goal):
|
||
return "case_split_missing"
|
||
# Arithmetic symbols with simp/rfl/omega that are _not_ pure equality — arithmetic gap
|
||
if has_arith and tactic in ("simp", "rfl", "omega"):
|
||
return "arithmetic_gap"
|
||
# RFL with hypotheses and no constructor target → assumption bridge failure
|
||
if tactic == "rfl" and n_hyps > 0:
|
||
return "missing_assumption_bridge"
|
||
# Inductive type → induction incomplete
|
||
if has_inductive and tactic in ("simp", "rfl"):
|
||
return "induction_incomplete"
|
||
# Simp with no clear pattern → simplifier gap
|
||
if tactic == "simp":
|
||
return "simplifier_gap"
|
||
# Order/inequality symbols
|
||
if has_order:
|
||
return "order_inequality_gap"
|
||
# Coercion-like patterns
|
||
if tactic == "rfl" and any(c in str(tf) for c in ["nat", "int"]):
|
||
return "coercion_mismatch"
|
||
return "other"
|
||
|
||
|
||
def build_trace(name: str, code: str) -> dict | None:
|
||
"""Run a theorem through the trace bridge and return spectral + text features."""
|
||
try:
|
||
instr, tags = instrument_theorem(code)
|
||
if not tags: return None
|
||
resp = prove(instr, name + "_flex")
|
||
stdout = resp.get("stdout", "") or ""
|
||
found = [l.split("@@PIST_TRACE_JSON@@")[1].strip() for l in stdout.split("\n") if "@@PIST_TRACE_JSON@@" in l]
|
||
if not found: return None
|
||
hs = [hashlib.sha256(t.encode()).hexdigest()[:16] for t in found]
|
||
uniq = list(dict.fromkeys(hs)); n = len(uniq)
|
||
mat = [[0] * n for _ in range(n)]
|
||
hi = {h: i for i, h in enumerate(uniq)}
|
||
for i in range(len(hs) - 1):
|
||
if hs[i] in hi and hs[i + 1] in hi: mat[hi[hs[i]]][hi[hs[i + 1]]] += 1
|
||
sp = compute_spectral(mat)
|
||
tf = extract_text_features(code)
|
||
return {"name": name, "spectral": sp, "text_features": tf, "code": code}
|
||
except:
|
||
return None
|
||
|
||
|
||
def connect():
|
||
host = os.environ.get("RDS_HOST", "database-1-instance-1.cghu8yqogqwo.us-east-1.rds.amazonaws.com")
|
||
port = os.environ.get("RDS_PORT", "5432"); user = os.environ.get("RDS_USER", "postgres")
|
||
db = os.environ.get("RDS_DB", "postgres")
|
||
token = os.environ.get("RDS_IAM_TOKEN", "")
|
||
if not token:
|
||
token = subprocess.check_output(["aws", "rds", "generate-db-auth-token",
|
||
"--region", os.environ.get("AWS_REGION", "us-east-1"),
|
||
"--hostname", host, "--port", port, "--username", user], text=True).strip()
|
||
import psycopg2
|
||
return psycopg2.connect(host=host, port=port, user=user, password=token, dbname=db, sslmode="require")
|
||
|
||
|
||
def spectral_route(features, library_session, failure_session):
|
||
"""kNN against flexure library for multi-step traces."""
|
||
conn = connect(); cur = conn.cursor()
|
||
cur.execute("SELECT decision_signals FROM ene.flexures WHERE session_id=%s OR session_id=%s",
|
||
(library_session, failure_session))
|
||
library = []
|
||
for row in cur.fetchall():
|
||
sig = json.loads(row[0]) if isinstance(row[0], str) else row[0]
|
||
sl = sig.get("spectral", {}); lv = [sl.get(k, 0) for k in V2_FEATURES]
|
||
library.append({"features": lv, "obs": sig.get("obstruction_type", "?"), "tf": sig.get("tactic_family", "?")})
|
||
cur.close(); conn.close()
|
||
|
||
if not library: return "other"
|
||
vec = [features.get(k, 0) for k in V2_FEATURES]
|
||
scored = [(math.sqrt(sum((vec[i] - l["features"][i])**2 for i in range(len(vec)))), l) for l in library if len(l["features"]) == len(vec)]
|
||
scored.sort(key=lambda x: x[0])
|
||
top3 = scored[:3]
|
||
obs_votes = Counter(lib["obs"] for _, lib in top3)
|
||
return obs_votes.most_common(1)[0][0] if obs_votes else "other"
|
||
|
||
|
||
def text_route(tf: dict) -> str:
|
||
"""Classify obstruction from text features (fallback for degenerate traces)."""
|
||
return classify_obstruction(tf)
|
||
|
||
|
||
def generate_patches(obstruction_type: str, code: str, max_patches: int = 3) -> list[str]:
|
||
"""Generate repair patches for an obstruction type."""
|
||
templates = REPAIR_POLICY.get(obstruction_type, [])
|
||
# Extract hypothesis names — filter to single-named hypotheses (not type decls)
|
||
hyps = re.findall(r'\(([^)]+:\s*[^)]+)\)', code)
|
||
hyp_names = []
|
||
for h in hyps:
|
||
parts = h.split(":")
|
||
names_part = parts[0].strip()
|
||
type_part = ":".join(parts[1:]).strip()
|
||
# A hypothesis has exactly one name (like "h : A" not "A B : Prop")
|
||
if len(names_part.split()) == 1 and type_part not in ("Prop", "Nat", "Int", "ℕ", "ℤ", "Type"):
|
||
hyp_names.append(names_part.strip())
|
||
first_hyp = hyp_names[0] if hyp_names else "h"
|
||
|
||
patches = []
|
||
for tmpl in templates:
|
||
if "%s" in tmpl:
|
||
patches.append(tmpl % first_hyp)
|
||
else:
|
||
patches.append(tmpl)
|
||
return patches[:max_patches]
|
||
|
||
|
||
def route_repair(name: str, code: str, library_session: str, failure_session: str, max_attempts: int = 5) -> dict:
|
||
"""Hybrid route-repair: spectral for multi-step, text for degenerate."""
|
||
# Build trace
|
||
instr, tags = instrument_theorem(code)
|
||
if not tags:
|
||
sp = {"matrix_size": 0, "rank": 0, "spectral_gap": 0}
|
||
else:
|
||
hs = [hashlib.sha256(t.encode()).hexdigest()[:16] for t in tags]
|
||
uniq = list(dict.fromkeys(hs)); n = len(uniq)
|
||
mat = [[0] * n for _ in range(n)]
|
||
hi = {h: i for i, h in enumerate(uniq)}
|
||
for i in range(len(hs) - 1):
|
||
if hs[i] in hi and hs[i+1] in hi:
|
||
mat[hi[hs[i]]][hi[hs[i+1]]] += 1
|
||
sp = compute_spectral(mat)
|
||
|
||
tf = extract_text_features(code)
|
||
|
||
# Degeneracy gate
|
||
if is_degenerate(sp):
|
||
obstruction = text_route(tf)
|
||
routing_method = "text_fallback"
|
||
else:
|
||
obstruction = spectral_route(sp, library_session, failure_session)
|
||
routing_method = "spectral_flexure"
|
||
|
||
# Verify initial failure
|
||
resp = prove(code, name + "_init")
|
||
if resp.get("ok", False):
|
||
return {"name": name, "initial_status": "verified", "recovered": False, "notes": "already verified"}
|
||
|
||
# Generate patches
|
||
patches = generate_patches(obstruction, code, max_patches=max_attempts)
|
||
init_vars = code.count("("); init_ops = sum(1 for c in code if c in "+-*/^∧∨→¬∀∃≤≥")
|
||
attempts = []; best_delta = -999; best_attempt = None; recovered = False
|
||
|
||
for i, patch in enumerate(patches):
|
||
patched = code.split(":=")[0] + ":= by\n " + patch
|
||
r = prove(patched, f"{name}_repair_{i}")
|
||
ok = r.get("ok", False)
|
||
av = patched.count("("); ao = sum(1 for c in patched if c in "+-*/^∧∨→¬∀∃≤≥")
|
||
delta = (init_vars + init_ops) - (av + ao)
|
||
if delta > best_delta:
|
||
best_delta = delta; best_attempt = {"patch": patch, "delta": delta, "ok": ok}
|
||
if ok:
|
||
recovered = True; best_attempt = {"patch": patch, "delta": delta, "ok": ok}; break
|
||
attempts.append({"attempt": i + 1, "patch": patch, "obstruction": obstruction, "ok": ok, "delta": delta})
|
||
|
||
return {
|
||
"name": name, "obstruction": obstruction,
|
||
"routing_method": routing_method,
|
||
"initial_status": "failed", "recovered": recovered,
|
||
"best_delta": best_delta, "partial_improvement": best_delta > 0,
|
||
"attempts": attempts, "best_attempt": best_attempt,
|
||
"matrix_size": sp.get("matrix_size", 0),
|
||
"rank": sp.get("rank", 0),
|
||
"tactic": tf.get("tactic", "?"),
|
||
"has_equality": tf.get("has_equality", False),
|
||
"has_arithmetic": tf.get("has_arithmetic", False),
|
||
"has_constructor": tf.get("has_constructor", False),
|
||
}
|
||
|
||
|
||
def main():
|
||
print("Route-Repair v1.2: hybrid spectral + text obstruction classifier\n")
|
||
failure_session = "9b1f9591-1c34-4c21-aeb8-594448b82003"
|
||
test_set = FAILURE_THEOREMS[:30]
|
||
|
||
results = []
|
||
for i, (n, c) in enumerate(test_set):
|
||
print(f" [{i+1}/{len(test_set)}] {n:35s} ... ", end="", flush=True)
|
||
r = route_repair(n, c, LIBRARY_SESSION, failure_session)
|
||
if r["initial_status"] == "verified":
|
||
print("already verified"); continue
|
||
s = "RECOVERED" if r["recovered"] else "improved" if r.get("partial_improvement") else "no change"
|
||
print(f"{s:15s} obs={r['obstruction']:30s} method={r['routing_method']:18s} "
|
||
f"tactic={r['tactic']:8s} eq={r['has_equality']} arith={r['has_arithmetic']}", flush=True)
|
||
results.append(r)
|
||
|
||
n = len(results); rec = sum(1 for r in results if r["recovered"]); part = sum(1 for r in results if r.get("partial_improvement"))
|
||
|
||
print(f"\n{'='*60}\nV1.2 HYBRID REPORT\n{'='*60}")
|
||
print(f"Test: {n} failed | Recovered: {rec} ({rec/max(n,1):.0%}) | Partial: {part} ({part/max(n,1):.0%})")
|
||
|
||
# Obstruction distribution
|
||
obs_dist = Counter(r["obstruction"] for r in results)
|
||
print(f"\nObstruction distribution:", flush=True)
|
||
for obs, cnt in sorted(obs_dist.items(), key=lambda x: -x[1]):
|
||
rec_obs = sum(1 for r in results if r["obstruction"] == obs and r["recovered"])
|
||
print(f" {obs:30s}: n={cnt:2d} rec={rec_obs}")
|
||
|
||
routing = Counter(r["routing_method"] for r in results)
|
||
print(f"\nRouting: text_fallback={routing.get('text_fallback', 0)}, spectral={routing.get('spectral_flexure', 0)}")
|
||
|
||
rp_path = os.path.join(os.path.dirname(__file__), "..", "..", "shared-data", "pist_route_repair_v12_benchmark.json")
|
||
with open(rp_path, "w") as f:
|
||
json.dump({"n": n, "recovered": rec, "partial": part, "results": results, "routing": dict(routing)}, f, indent=2)
|
||
print(f"\nReport: {rp_path}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|