mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
Ports the motif scoring surface from pist_trace_classify_mcp.py (lines 136–149)
into a provable Lean module:
score = frequency / max(library_size, 1) + (0.3 if tactic_family matches)
New module: Semantics.PIST.Motif (201 lines)
§1 familyMatchBonus constant (ofRatio 3 10 = 19660 raw)
§2 MotifInputs, baseScore, motifScore
§3 MotifCandidate record, mkCandidate constructor
§4 rankMotifs / topKMotifs (mergeSort desc, motifId tie-break)
§5 8 executable #eval witnesses with -- expect: annotations
§6 6 proved invariants:
motifScore_bonus_pos (decide)
motifScore_match_ge_base_witness (decide, concrete)
motifScore_zero_freq_base (simp)
motifScore_zero_freq_no_match (simp)
motifScore_zero_freq_match_witness (decide, concrete)
rankMotifs_match_beats_no_match (native_decide — mergeSort sort witness)
Full workspace build: 3570 jobs, 0 errors.
pist_trace_classify_mcp.py PARTIAL BOUNDARY updated: motif score + rank order
now explicitly point to Semantics.PIST.Motif as authoritative.
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
268 lines
12 KiB
Python
268 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
"""pist-trace-classify MCP server — classify proof traces against the flexure joint library.
|
||
|
||
Calling convention (MCP JSON-RPC on stdio):
|
||
|
||
Send:
|
||
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"pist_trace_classify","arguments":{"trace_path":"..."},"id":1}}
|
||
|
||
With options:
|
||
trace_path: path to a ProofTraceReceipt v2 JSON file
|
||
trace_json: inline JSON object (alternative to trace_path)
|
||
insert: bool (default false) — store result in ene.artifacts
|
||
top_k: int (default 5) — number of nearest motifs to return
|
||
"""
|
||
# PARTIAL BOUNDARY: decision-critical logic ported to Lean; Python retains I/O only.
|
||
# classify_tactic_from_name → Semantics.PIST.Spectral.classifyTacticFromName
|
||
# compute_spectral → Semantics.PIST.Spectral.computeSpectral
|
||
# motif score + rank order → Semantics.PIST.Motif (motifScore, rankMotifs)
|
||
# RDS queries, MCP protocol, artifact insertion, classify_trace orchestration
|
||
# remain in Python.
|
||
|
||
import json
|
||
import math
|
||
import os
|
||
from rds_connect import connect_rds
|
||
import sys
|
||
import uuid
|
||
from collections import Counter, defaultdict
|
||
from pathlib import Path
|
||
|
||
SERVER_NAME = "pist-trace-classify"
|
||
SERVER_VERSION = "0.1.0"
|
||
|
||
FLEXURE_SESSION = "a4a0eb20-93fe-413e-8e0b-50334bb778d8"
|
||
|
||
FEATURE_KEYS = ["matrix_size", "rank", "spectral_gap", "laplacian_zero_count", "density", "eigenvalue_max"]
|
||
|
||
|
||
def connect():
|
||
return connect_rds()
|
||
|
||
|
||
def power_iteration(matrix, max_iter=100):
|
||
n = len(matrix)
|
||
if n == 0: return 0.0
|
||
v = [1.0 / math.sqrt(n)] * n
|
||
for _ in range(max_iter):
|
||
vn = [sum(matrix[i][j] * v[j] for j in range(n)) for i in range(n)]
|
||
nm = math.sqrt(sum(x * x for x in vn))
|
||
if nm < 1e-12: return 0.0
|
||
v = [x / nm for x in vn]
|
||
num = sum(v[i] * sum(matrix[i][j] * v[j] for j in range(n)) for i in range(n))
|
||
den = sum(v[i] * v[i] for i in range(n))
|
||
return num / den if den > 0 else 0.0
|
||
|
||
|
||
def compute_spectral(matrix):
|
||
"""Full v2 spectral profile from a transition matrix."""
|
||
n = len(matrix)
|
||
if n == 0: return {}
|
||
sym = [[(matrix[i][j] + matrix[j][i]) / 2.0 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)]
|
||
ev_max = power_iteration(sym)
|
||
shifted = [[sym[i][j] - 0.9 * ev_max * (1 if i == j else 0) for j in range(n)] for i in range(n)]
|
||
ev_shift = power_iteration(shifted)
|
||
ev_second = max(0, ev_max - ev_shift) if ev_shift < ev_max else ev_max
|
||
gap = ev_max - ev_second
|
||
lap_max = power_iteration(lap)
|
||
neg_lap = [[-lap[i][j] for j in range(n)] for i in range(n)]
|
||
lap_min = -power_iteration(neg_lap)
|
||
ata = [[sum(matrix[k][i] * matrix[k][j] for k in range(n)) for j in range(n)] for i in range(n)]
|
||
sv_max = math.sqrt(max(0, power_iteration(ata)))
|
||
rank = sum(1 for row in matrix if sum(row) > 0)
|
||
total = sum(sum(row) for row in matrix)
|
||
frob = math.sqrt(sum(cell * cell for row in matrix for cell in row))
|
||
lap_zero = 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": lap_zero,
|
||
"adjacency_eigenvalue_max": round(ev_max, 6),
|
||
"laplacian_eigenvalue_max": round(lap_max, 6),
|
||
"singular_value_max": round(sv_max, 6),
|
||
}
|
||
|
||
|
||
def classify_trace(trace: dict, top_k: int = 5, insert: bool = False) -> dict:
|
||
"""Full classification pipeline for a proof trace."""
|
||
import hashlib
|
||
name = trace.get("theorem_name", trace.get("name", "unnamed"))
|
||
status = trace.get("status", "?")
|
||
matrix = trace.get("transition_matrix", [])
|
||
tags = trace.get("trace_tags", [])
|
||
|
||
if not matrix or len(matrix) == 0:
|
||
return {"error": "empty_transition_matrix", "theorem_name": name}
|
||
|
||
spectral = compute_spectral(matrix)
|
||
n = spectral.get("matrix_size", 0)
|
||
vec = [spectral.get(k, 0) for k in FEATURE_KEYS]
|
||
|
||
# Connect to RDS and query nearest motifs
|
||
result = {"theorem_name": name, "trace_hash": hashlib.sha256(json.dumps(trace).encode()).hexdigest()[:16],
|
||
"status": status, "spectral": spectral, "feature_version": "flexure-spectrum-v2",
|
||
"predictions": {"proof_status": {"label": status, "confidence": 0.0}},
|
||
"calibration": {"dataset": "tier2b_57_theorem_batch", "proof_status_loocv": 0.895,
|
||
"sample_count": 57, "feature_version": "flexure-spectrum-v2", "status": "experimental"},
|
||
"nearest_motifs": [], "flexures": []}
|
||
|
||
try:
|
||
conn = connect()
|
||
cur = conn.cursor()
|
||
|
||
# Load all flexure features from the trained session
|
||
cur.execute(
|
||
"SELECT decision_signals, converged FROM ene.flexures WHERE session_id = %s",
|
||
(FLEXURE_SESSION,),
|
||
)
|
||
library = []
|
||
for row in cur.fetchall():
|
||
sig = json.loads(row[0]) if isinstance(row[0], str) else row[0]
|
||
sp = sig.get("spectral", {})
|
||
lvec = [sp.get(k, 0) for k in FEATURE_KEYS]
|
||
library.append({
|
||
"features": lvec, "tactic_family": sig.get("tactic_family", "?"),
|
||
"joint_label": sig.get("joint_label", "?"), "converged": row[1],
|
||
"rrc_shape": sig.get("rrc_shape", "?"),
|
||
})
|
||
|
||
# Load motifs
|
||
cur.execute("SELECT id, pattern_signature, frequency, pre_conditions, decision_rules FROM ene.flexure_patterns ORDER BY frequency DESC")
|
||
motifs = [{"id": str(r[0]), "signature": r[1], "frequency": r[2],
|
||
"pre": json.loads(r[3]) if isinstance(r[3], str) else r[3],
|
||
"rules": json.loads(r[4]) if isinstance(r[4], str) else r[4]}
|
||
for r in cur.fetchall()]
|
||
|
||
# Nearest motifs by frequency × label match
|
||
for m in motifs:
|
||
score = m["frequency"] / max(len(library), 1)
|
||
if m["pre"].get("tactic_family", "") == classify_tactic_from_name(name):
|
||
score += 0.3
|
||
result["nearest_motifs"].append({
|
||
"motif_id": m["id"], "score": round(score, 3),
|
||
"support": m["frequency"],
|
||
"tactic_family": m["pre"].get("tactic_family", "?"),
|
||
"joint_label": m["rules"].get("joint_type", "?"),
|
||
})
|
||
|
||
# Sort by score
|
||
result["nearest_motifs"].sort(key=lambda x: -x["score"])
|
||
result["nearest_motifs"] = result["nearest_motifs"][:top_k]
|
||
|
||
# Build predictions from nearest motifs
|
||
if result["nearest_motifs"]:
|
||
result["predictions"] = {
|
||
"proof_status": {"label": status, "confidence": round(0.895, 2)},
|
||
"tactic_family": {"label": result["nearest_motifs"][0]["tactic_family"],
|
||
"confidence": round(result["nearest_motifs"][0]["score"], 2)},
|
||
"joint_label": {"label": result["nearest_motifs"][0]["joint_label"],
|
||
"confidence": round(result["nearest_motifs"][0]["score"], 2)},
|
||
}
|
||
|
||
if insert:
|
||
artifact_id = str(uuid.uuid4())
|
||
content = json.dumps({"trace": name, "spectral": spectral, "classification": result["predictions"]})
|
||
cur.execute(
|
||
"INSERT INTO ene.artifacts (id, path, kind, language, title, content, content_hash, metadata) "
|
||
"VALUES (%s, %s, 'pist_trace', 'json', %s, %s, %s, %s::jsonb)",
|
||
(artifact_id, f"pist_traces/{name}_{uuid.uuid4().hex[:8]}.json",
|
||
f"PIST Classified: {name}",
|
||
content, hashlib.sha256(content.encode()).hexdigest(),
|
||
json.dumps({"pist_classified": True, "proof_status": status, "session": FLEXURE_SESSION})),
|
||
)
|
||
conn.commit()
|
||
result["artifact_id"] = artifact_id
|
||
|
||
cur.close()
|
||
conn.close()
|
||
except Exception as e:
|
||
result["error"] = str(e)[:200]
|
||
result["database_available"] = False
|
||
|
||
return result
|
||
|
||
|
||
def classify_tactic_from_name(name: str) -> str:
|
||
name_lower = name.lower()
|
||
if "rw" in name_lower: return "rewrite"
|
||
if "simp" in name_lower: return "normalization"
|
||
if "omega" in name_lower: return "arithmetic"
|
||
if "induct" in name_lower: return "induction"
|
||
if "ring" in name_lower or "calc" in name_lower: return "algebraic"
|
||
if "cases" in name_lower or "constructor" in name_lower: return "case_analysis"
|
||
if any(k in name_lower for k in ["apply", "intro", "have", "logic"]): return "discharge"
|
||
if "rfl" in name_lower: return "reflexivity"
|
||
return "unknown"
|
||
|
||
|
||
TOOL_SCHEMA = {
|
||
"name": "pist_trace_classify",
|
||
"description": "Classify a proof trace against the flexure joint library. Returns transition matrix spectra, nearest motifs, and predictions for proof status, tactic family, and joint label.",
|
||
"inputSchema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"trace_path": {"type": "string", "description": "Path to a ProofTraceReceipt v2 JSON file on disk"},
|
||
"trace_json": {"type": "object", "description": "Inline ProofTraceReceipt v2 JSON (alternative to trace_path)"},
|
||
"insert": {"type": "boolean", "default": False, "description": "Store result in ene.artifacts"},
|
||
"top_k": {"type": "number", "default": 5, "description": "Number of nearest motifs to return"},
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
def handle_request(request: dict) -> dict:
|
||
method = request.get("method", "")
|
||
req_id = request.get("id")
|
||
|
||
if method == "initialize":
|
||
return {"jsonrpc": "2.0", "id": req_id, "result": {
|
||
"protocolVersion": "2025-03-26", "capabilities": {"tools": {}},
|
||
"serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
|
||
}}
|
||
elif method == "notifications/initialized":
|
||
return {"jsonrpc": "2.0", "id": req_id, "result": None}
|
||
elif method == "tools/list":
|
||
return {"jsonrpc": "2.0", "id": req_id, "result": {"tools": [TOOL_SCHEMA]}}
|
||
elif method == "tools/call":
|
||
params = request.get("params", {})
|
||
args = params.get("arguments", {})
|
||
trace_path = args.get("trace_path")
|
||
trace_json = args.get("trace_json")
|
||
insert = args.get("insert", False)
|
||
top_k = args.get("top_k", 5)
|
||
|
||
if trace_path:
|
||
try:
|
||
with open(trace_path) as f:
|
||
trace = json.load(f)
|
||
except Exception as e:
|
||
return {"jsonrpc": "2.0", "id": req_id, "result": {"content": [{"type": "text", "text": json.dumps({"error": str(e)})}]}}
|
||
elif trace_json:
|
||
trace = trace_json
|
||
else:
|
||
return {"jsonrpc": "2.0", "id": req_id, "result": {"content": [{"type": "text", "text": json.dumps({"error": "provide trace_path or trace_json"})}]}}
|
||
|
||
result = classify_trace(trace, top_k, insert)
|
||
return {"jsonrpc": "2.0", "id": req_id,
|
||
"result": {"content": [{"type": "text", "text": json.dumps(result, indent=2)}]}}
|
||
else:
|
||
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": f"Unknown method: {method}"}}
|
||
|
||
|
||
def main():
|
||
for line in sys.stdin:
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
try:
|
||
request = json.loads(line)
|
||
response = handle_request(request)
|
||
print(json.dumps(response))
|
||
sys.stdout.flush()
|
||
except json.JSONDecodeError as e:
|
||
continue
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|