Research-Stack/4-Infrastructure/shim/pist_trace_classify_offline.py
allaun 67e77d8f24 feat(lean,infra): SilverSight-first offline PIST trace classifier
Moves all classification authority from Python into Lean:

- Adds `0-Core-Formalism/lean/Semantics/PistClassifyTrace.lean` executable.
  Reads a raw trace JSON and emits spectral radius, RRC shape, and tactic
  family using `Semantics.PIST.Spectral` and `Semantics.PIST.Classify`.
- Registers `pist-classify-trace` in `lakefile.toml`.
- Fixes `Semantics.PIST.Spectral` power iteration to handle directed
  transition matrices:
  - `symmetrize` now preserves half-integer weights as Q16_16 raw values.
  - `matVecMul` uses saturated Q16_16 arithmetic to prevent overflow.
- Rewrites `4-Infrastructure/shim/pist_trace_classify_offline.py` as a pure
  I/O wrapper: reads JSON, calls the Lean classifier, optionally calls
  `rrc-watchdog`, and emits the combined JSON. Removes Python-side spectral
  computation, shape thresholds, tactic heuristic, and KNN.
- Updates `AGENTS.md` and `4-Infrastructure/AGENTS.md` with the new build
  baseline and shim contract.

Verification:
- `lake build` → 8604 jobs, 0 errors
- Canary trace outputs match previous Python outputs to within Q16_16 rounding
  (e.g., apply_chain λ_q16 = 59044 vs 59045).
- `python3 -m py_compile` on the rewritten shim passes.
2026-06-22 01:11:17 -05:00

153 lines
5 KiB
Python
Executable file

#!/usr/bin/env python3
"""
pist_trace_classify_offline.py
==============================
SilverSight-ruleset-first offline proof-trace classifier.
This shim owns **only** I/O and subprocess orchestration. All classification
authority lives in Lean:
- Spectral feature extraction → `pist-classify-trace` (Semantics.PIST.Spectral)
- Shape threshold / color gate → `pist-classify-trace` (Semantics.PIST.Classify)
- Tactic-family inference → `pist-classify-trace` (Semantics.PIST.Spectral)
- RRC alignment verification → `rrc-watchdog`
The Python script never computes a spectral radius, never chooses a shape, and
never applies a heuristic. It reads the trace JSON, forwards it to the Lean
classifier, optionally invokes the alignment watchdog, and emits the combined
result JSON.
Usage:
python3 pist_trace_classify_offline.py trace.json --rrc-shape signalShapedRouteCompiler
"""
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
PIST_CLASSIFY_HOST = (
REPO_ROOT / "0-Core-Formalism" / "lean" / "Semantics" /
".lake" / "build" / "bin" / "pist-classify-trace"
)
PIST_CLASSIFY_CONTAINER = (
"/home/researcher/stack/0-Core-Formalism/lean/Semantics/"
".lake/build/bin/pist-classify-trace"
)
WATCHDOG_CONTAINER = (
"/home/researcher/stack/0-Core-Formalism/lean/Semantics/"
".lake/build/bin/rrc-watchdog"
)
def run_pist_classify(trace: dict) -> dict:
"""Forward the raw trace JSON to the Lean classifier via stdin.
Uses the host binary when available; otherwise falls back to the
research-stack container (with stdin attached).
"""
payload = json.dumps(trace)
if PIST_CLASSIFY_HOST.exists():
cmd = [str(PIST_CLASSIFY_HOST)]
res = subprocess.run(
cmd, input=payload, capture_output=True, text=True, timeout=30
)
else:
cmd = [
"podman", "exec", "-i", "research-stack",
PIST_CLASSIFY_CONTAINER,
]
res = subprocess.run(
cmd, input=payload, capture_output=True, text=True, timeout=30
)
if res.returncode != 0:
return {
"error": f"pist-classify-trace exited {res.returncode}",
"stderr": res.stderr,
}
try:
return json.loads(res.stdout)
except json.JSONDecodeError as e:
return {
"error": f"invalid JSON from pist-classify-trace: {e}",
"stdout": res.stdout,
"stderr": res.stderr,
}
def run_lean_watchdog(predicted_shape: str, expected_shape: str) -> dict:
"""Invoke the Lean RRC alignment watchdog."""
cmd = [
"podman", "exec", "research-stack",
WATCHDOG_CONTAINER,
"--pist-label", predicted_shape,
"--exact-label", predicted_shape,
"--rrc-shape", expected_shape,
]
try:
res = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
if res.returncode not in (0, 1):
return {"error": f"watchdog returned exit code {res.returncode}", "stderr": res.stderr}
return json.loads(res.stdout)
except Exception as e:
return {"error": f"failed to run rrc-watchdog: {e}"}
def main():
parser = argparse.ArgumentParser(
description="SilverSight-first offline RRC trace classifier"
)
parser.add_argument("trace_path", help="Path to ProofTraceReceipt v2 JSON file")
parser.add_argument(
"--rrc-shape",
default="signalShapedRouteCompiler",
help="Expected shape for Lean alignment gate check",
)
args = parser.parse_args()
with open(args.trace_path) as f:
trace = json.load(f)
if not trace.get("transition_matrix"):
print(json.dumps({"error": "Empty transition matrix"}))
sys.exit(1)
# 1. Lean classification (spectral + shape + tactic)
classify_res = run_pist_classify(trace)
if "error" in classify_res:
print(json.dumps(classify_res))
sys.exit(1)
predicted_shape = classify_res.get("predicted_rrc_shape", "HoldForUnlawfulOrUnderspecifiedShape")
# 2. Lean alignment verification
watchdog_res = run_lean_watchdog(predicted_shape, args.rrc_shape)
# 3. Combine into the legacy output shape for downstream consumers.
# knn_predictions is retired: KNN is decision logic and has no Lean
# authority yet. It is kept as a placeholder to avoid breaking parsers.
output = {
"theorem_name": classify_res.get("theorem_name", trace.get("name", "unnamed")),
"spectral_radius": classify_res.get("spectral_radius"),
"spectral_radius_q16": classify_res.get("spectral_radius_q16"),
"predicted_rrc_shape": predicted_shape,
"tactic_family_heuristic": classify_res.get("tactic_family", "unknown"),
"knn_predictions": {
"tactic_family": "unknown",
"status": "unknown",
"support": 0,
},
"lean_alignment": watchdog_res,
}
print(json.dumps(output, indent=2))
if __name__ == "__main__":
main()