Research-Stack/4-Infrastructure/shim/chentsov_fusion.py
allaun c44a01df3b feat(lean): InformationManifold + SLUQ; chentsov_fusion, tdoku_16d; docs reconciliation suite
- New: InformationManifold.lean — tensor integration module
- Update: SLUQ.lean — proof refinements
- New: chentsov_fusion.py — Chentsov fusion bridge
- New: tdoku_16d.py — 16-dimensional TDoku solver
- New: validate_docs.py — documentation validation script
- New: negative_tests.json + test_negative_suite.py — negative test fixtures
- Update: flac_dsp_node.py — DSP node refinements
- Update: CITATION.cff — citation metadata
- Docs: GEOMETRIC_SUBSTANCE_CANONICAL_RECONCILIATION, LITERATURE_MAPPING,
  GROTHENDIECKIAN_ORGANIZATIONAL_ROTATION_PROPOSAL, formula extraction suite
- New: package/ — public-apis npm metadata
2026-06-28 10:38:13 -05:00

662 lines
26 KiB
Python
Raw Permalink 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
"""
chentsov_fusion.py — Parallel proof attack on ChentsovFinite.lean sorries.
DML-style planning + backtracking (DeepClause pattern):
1. PLAN: For each sorry, generate ranked tactic strategies
2. EXECUTE: Try first strategy, backtrack on failure
3. FUSE: Run across qfox (Gemma4, GPU) and neon (Qwen3.6, CPU) in parallel
Usage:
python3 chentsov_fusion.py # all sorries, both endpoints
python3 chentsov_fusion.py --sorries 1,7,9 # specific sorries
python3 chentsov_fusion.py --endpoints qfox # qfox only
python3 chentsov_fusion.py --max-retries 3 # backtracking depth
"""
import argparse
import json
import os
import re
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
# Force line-buffered output even when stdout is redirected to a file
sys.stdout.reconfigure(line_buffering=True)
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
import urllib.request
import urllib.error
# ============================================================
# Configuration
# ============================================================
ENDPOINTS = {
"qfox": {
"url": "http://100.88.57.96:8081/v1/chat/completions",
"model": "gemma4-12b",
"name": "Gemma4-12B (qfox, GPU)",
"timeout": 600,
"max_tokens": 4096,
"temperature": 0.1,
},
"neon-qwen": {
"url": "http://100.92.88.64:11434/v1/chat/completions",
"model": "hf.co/llmfan46/Qwen3.6-35B-A3B-uncensored-heretic-GGUF:Q4_K_M",
"name": "Qwen3.6-35B-A3B (neon, CPU)",
"timeout": 3600,
"max_tokens": 4096,
"temperature": 0.1,
},
"neon-step": {
"url": "http://100.92.88.64:11434/v1/chat/completions",
"model": "step3.7-flash",
"name": "Step-3.7-Flash (neon, CPU)",
"timeout": 3600,
"max_tokens": 4096,
"temperature": 0.1,
},
# DeepSeek-Prover-V2-7B: purpose-built Lean 4 / math proof model (~4-5 GB Q4_K_M).
# Best for hard construction sorries; trained on Lean proof search data.
"neon-deepseek": {
"url": "http://100.92.88.64:11434/v1/chat/completions",
"model": "hf.co/irmma/DeepSeek-Prover-V2-7B-Q4_K_M-GGUF:latest",
"name": "DeepSeek-Prover-V2-7B (neon, CPU)",
"timeout": 3600,
"max_tokens": 4096,
"temperature": 0.05, # lower temp: prover models are more deterministic
},
# FreeLLMAPI: runs on cupfox (100.72.130.76), zero neon RAM impact.
# Offloads generation to external models; good overflow for hard sorries.
"freellmapi": {
"url": "http://46.232.249.226:3001/v1/chat/completions",
"model": "auto",
"name": "FreeLLMAPI (cupfox, zero-RAM)",
"timeout": 120,
"max_tokens": 4096,
"temperature": 0.1,
},
}
# ============================================================
# Proof Strategies (DML-style plan)
# ============================================================
# Each strategy is a tactic family with a focused prompt.
# The planner ranks strategies by difficulty match.
# The executor tries them in order, backtracking on failure.
PROOF_STRATEGIES = [
{
"id": "simp_comm",
"tactic": "simp",
"prompt": "Use simp with mul_comm to swap the multiplication order. The goal is symmetric in two variables.",
"template": "simp [fisherMetric, mul_comm]",
"applicable_to": ["fisherMetric_sym"],
},
{
"id": "simp_ite",
"tactic": "simp",
"prompt": "Use simp with Finset.sum_ite to evaluate the sum of if-then-else expressions. The tangentBasis has two if-then-else layers.",
"template": "simp [tangentBasis, Finset.sum_ite]",
"applicable_to": ["tangentBasis_sum"],
},
{
"id": "simp_membership",
"tactic": "simp",
"prompt": "Use simp to unfold the set membership and apply the sum lemma.",
"template": "simp [tangentSpace, tangentBasis_sum]",
"applicable_to": ["tangentBasis_in_tangentSpace"],
},
{
"id": "constructor_ring",
"tactic": "constructor; ring",
"prompt": "For IsLinearMap, use constructor to split into add and smul, then use simp with add_mul/mul_add and Finset.sum_add_distrib, followed by ring.",
"template": "constructor\n· intro x y; simp [fisherMetric, add_mul, Finset.sum_add_distrib]; ring\n· intro c x; simp [fisherMetric, mul_assoc, Finset.sum_add_distrib]; ring",
"applicable_to": ["fisherMetric_linear_left", "fisherMetric_linear_right"],
},
{
"id": "div_pos",
"tactic": "positivity",
"prompt": "For positive definiteness, each term X i * X i / p.1 i is non-negative (p is in the open simplex so p.1 i > 0). Use div_pos and mul_nonneg, then show at least one term is strictly positive.",
"template": "simp [fisherMetric]\napply Finset.sum_pos'\n· intro i _; exact div_nonneg (sq_nonneg _) (le_of_lt (p.2.1 i))\n· obtain ⟨i, hi⟩ := not_forall.mp hX\n exact ⟨i, Finset.mem_unicast.mpr (by simp), div_pos (sq_pos_of_ne_zero hi) (p.2.1 i)⟩",
"applicable_to": ["fisherMetric_pos_def"],
},
{
"id": "equiv_reindex",
"tactic": "Finset.sum_equiv",
"prompt": "For permutation invariance of the sum, use Finset.sum_equiv with the permutation to reindex. The sum ∑ i, p.1 (σ.symm i) = 1 follows from p.2.2 by reindexing.",
"template": "have := p.2.2\nconv_lhs at this => rw [← Finset.sum_equiv σ.symm (Finset.univ) (by simp)]\nexact this",
"applicable_to": ["IsPermutationInvariant_sigma_sum"],
},
{
"id": "construction",
"tactic": "exact/mk",
"prompt": "This is a definition, not a lemma. Construct the term directly using structure syntax ⟨...⟩.",
"template": "exact ⟨fun i => if h : i = f.splitIdx then f.hq_pos * p.1 i else if h2 : i = ⟨f.splitIdx, by omega⟩ then (1 - f.hq_pos) * p.1 (f.splitIdx) else p.1 i, ...⟩",
"applicable_to": ["SplitEmbedding.apply", "SplitEmbedding.pushforward", "uniformDist"],
},
{
"id": "omega_arith",
"tactic": "omega",
"prompt": "This is a pure arithmetic goal. Use omega to close it.",
"template": "omega",
"applicable_to": [],
},
{
"id": "native_decide",
"tactic": "native_decide",
"prompt": "This might be decidable by computation. Try native_decide.",
"template": "native_decide",
"applicable_to": [],
},
{
"id": "sorry_fallback",
"tactic": "sorry",
"prompt": "Cannot prove this yet. Leave as sorry with a TODO comment.",
"template": "sorry -- TODO: needs further work",
"applicable_to": [],
},
]
# ============================================================
# ChentsovFinite sorries
# ============================================================
SORRIES = [
{
"id": 1, "line": 40, "name": "tangentBasis_sum", "difficulty": "easy",
"strategies": ["simp_ite", "omega_arith"],
"context": """def tangentBasis {n : } (i j : Fin n) : Fin n → :=
fun k => if k = i then 1 else if k = j then -1 else 0
lemma tangentBasis_sum {n : } (p : openSimplex n) (i j : Fin n) :
∑ k, tangentBasis i j k = 0 := by""",
},
{
"id": 2, "line": 44, "name": "tangentBasis_in_tangentSpace", "difficulty": "easy",
"strategies": ["simp_membership", "simp_ite"],
"context": """lemma tangentBasis_in_tangentSpace {n : } (p : openSimplex n) (i j : Fin n) :
tangentBasis i j ∈ tangentSpace p := by""",
},
{
"id": 3, "line": 64, "name": "SplitEmbedding.apply", "difficulty": "medium",
"strategies": ["construction"],
"context": """structure SplitEmbedding (n : ) where
splitIdx : Fin n
q :
hq_pos : q > 0
hq_lt_one : q < 1
def SplitEmbedding.refinedSize {n : } (_ : SplitEmbedding n) : := n + 1
def SplitEmbedding.apply {n : } (f : SplitEmbedding n) (p : openSimplex n) :
openSimplex (refinedSize f) :=""",
},
{
"id": 4, "line": 68, "name": "SplitEmbedding.pushforward", "difficulty": "medium",
"strategies": ["construction"],
"context": """def SplitEmbedding.pushforward {n : } (f : SplitEmbedding n) (p : openSimplex n)
(X : Fin n → ) : Fin (refinedSize f) → :=""",
},
{
"id": 5, "line": 73, "name": "SplitEmbedding.pushforward_sum", "difficulty": "hard",
"strategies": ["simp_ite", "omega_arith", "sorry_fallback"],
"context": """lemma SplitEmbedding.pushforward_sum {n : } (f : SplitEmbedding n) (p : openSimplex n)
(X : Fin n → ) (hX : ∑ i, X i = 0) :
∑ j, f.pushforward p X j = 0 := by""",
},
{
"id": 6, "line": 78, "name": "SplitEmbedding.pushforward_tangent", "difficulty": "hard",
"strategies": ["equiv_reindex", "sorry_fallback"],
"context": """lemma SplitEmbedding.pushforward_tangent {n : } (f : SplitEmbedding n) (p : openSimplex n)
(X : Fin n → ) (hX : X ∈ tangentSpace p) :
f.pushforward p X ∈ tangentSpace (f.apply p) := by""",
},
{
"id": 7, "line": 93, "name": "fisherMetric_sym", "difficulty": "easy",
"strategies": ["simp_comm", "omega_arith"],
"context": """noncomputable def fisherMetric {n : } (p : openSimplex n) (X Y : Fin n → ) : :=
∑ i, X i * Y i / p.1 i
lemma fisherMetric_sym {n : } (p : openSimplex n) (X Y : Fin n → ) :
fisherMetric p X Y = fisherMetric p Y X := by""",
},
{
"id": 8, "line": 98, "name": "fisherMetric_pos_def", "difficulty": "medium",
"strategies": ["div_pos", "sorry_fallback"],
"context": """lemma fisherMetric_pos_def {n : } (p : openSimplex n) (X : Fin n → )
(hX : X ≠ 0) (hXsum : ∑ i, X i = 0) :
fisherMetric p X X > 0 := by""",
},
{
"id": 9, "line": 102, "name": "fisherMetric_linear_left", "difficulty": "easy",
"strategies": ["constructor_ring", "omega_arith"],
"context": """lemma fisherMetric_linear_left {n : } (p : openSimplex n) (Y : Fin n → ) :
IsLinearMap (fun X => fisherMetric p X Y) := by""",
},
{
"id": 10, "line": 106, "name": "fisherMetric_linear_right", "difficulty": "easy",
"strategies": ["constructor_ring", "omega_arith"],
"context": """lemma fisherMetric_linear_right {n : } (p : openSimplex n) (X : Fin n → ) :
IsLinearMap (fun Y => fisherMetric p X Y) := by""",
},
{
"id": 11, "line": 136, "name": "IsPermutationInvariant_sigma_sum", "difficulty": "medium",
"strategies": ["equiv_reindex", "sorry_fallback"],
"context": """In IsPermutationInvariant definition:
let σp : openSimplex n :=
⟨fun i => p.1 (σ.symm i),
⟨fun i => p.2.1 (σ.symm i), by
have := p.2.2
sorry⟩⟩
-- Need: ∑ i, p.1 (σ.symm i) = 1""",
},
{
"id": 12, "line": 156, "name": "fisherMetric_chentsov_invariant", "difficulty": "hard",
"strategies": ["simp_comm", "sorry_fallback"],
"context": """lemma fisherMetric_chentsov_invariant {n : } :
IsChentsovInvariant (...) (...) := by
intro f p X Y hXsum hYsum
sorry""",
},
{
"id": 13, "line": 169, "name": "uniformDist", "difficulty": "medium",
"strategies": ["construction", "sorry_fallback"],
"context": """noncomputable def uniformDist (N : ) (hN : N > 0) : openSimplex N :=""",
},
{
"id": 14, "line": 181, "name": "metric_at_uniform", "difficulty": "hard",
"strategies": ["sorry_fallback"],
"context": """lemma metric_at_uniform {N : } (hN : N ≥ 2) (g : RiemannianMetric N)
(h_perm : IsPermutationInvariant g) :
∃ (lambda_N : ), lambda_N > 0 ∧
∀ u v : Fin N → , ∑ i, u i = 0 → ∑ i, v i = 0 →
g.toFun (uniformDist N (by linarith)) u v = lambda_N * ∑ i, u i * v i := by""",
},
{
"id": 15, "line": 204, "name": "equal_refinement_const", "difficulty": "hard",
"strategies": ["sorry_fallback"],
"context": """lemma equal_refinement_const ... :
∃ C : , C > 0 ∧ ... := by""",
},
{
"id": 16, "line": 228, "name": "fisher_on_rational", "difficulty": "hard",
"strategies": ["sorry_fallback"],
"context": """lemma fisher_on_rational ... :
∃ C : , C > 0 ∧ ... := by""",
},
{
"id": 17, "line": 255, "name": "chentsov_theorem", "difficulty": "hard",
"strategies": ["sorry_fallback"],
"context": """theorem chentsov_theorem ... :
∃ (c : ), c > 0 ∧ ... := by""",
},
]
# ============================================================
# System prompt
# ============================================================
SYSTEM_PROMPT = """You are a Lean 4 formal verification expert. You will be given:
1. A theorem statement with `sorry` to prove
2. A specific tactic strategy to try
Output ONLY the Lean tactic proof that replaces `sorry`. No explanation, no markdown fences.
Key definitions:
- openSimplex n = { p | (∀ i, p i > 0) ∧ (∑ i, p i = 1) }
- tangentSpace p = { X | ∑ i, X i = 0 }
- fisherMetric p X Y = ∑ i, X i * Y i / p.1 i
- tangentBasis i j = fun k => if k = i then 1 else if k = j then -1 else 0
Available tactics: simp, ring, omega, exact, apply, constructor, intro, ext,
funext, rcases, calc, nlinarith, norm_num, decide, native_decide,
Finset.sum_ite, Finset.sum_add_distrib, add_mul, mul_add, mul_comm,
Finset.sum_equiv, div_pos, mul_pos, sq_nonneg, sq_pos_of_ne_zero."""
# ============================================================
# API calls
# ============================================================
def call_model(endpoint: dict, prompt: str, sorry_id: int) -> dict:
"""Call a model endpoint and return the result."""
start = time.time()
try:
payload = {
"model": endpoint["model"],
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": prompt},
],
"max_tokens": endpoint["max_tokens"],
"temperature": endpoint["temperature"],
"stream": False,
}
if HAS_REQUESTS:
resp = requests.post(endpoint["url"], json=payload, timeout=endpoint["timeout"])
resp.raise_for_status()
data = resp.json()
else:
req = urllib.request.Request(
endpoint["url"],
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=endpoint["timeout"]) as resp:
data = json.loads(resp.read().decode("utf-8"))
elapsed = time.time() - start
content = data["choices"][0]["message"]["content"]
# Some models (Gemma4) put reasoning in reasoning_content
reasoning = data["choices"][0]["message"].get("reasoning_content", "")
# Strip markdown code fences
if content:
content = re.sub(r"^```(?:lean)?\s*\n?", "", content.strip())
content = re.sub(r"\n?```\s*$", "", content.strip())
return {
"sorry_id": sorry_id,
"endpoint": endpoint["name"],
"response": content,
"reasoning": reasoning[:500] if reasoning else None,
"elapsed_s": round(elapsed, 1),
"success": True,
"error": None,
}
except Exception as e:
elapsed = time.time() - start
return {
"sorry_id": sorry_id,
"endpoint": endpoint["name"],
"response": None,
"elapsed_s": round(elapsed, 1),
"success": False,
"error": str(e),
}
# ============================================================
# DML-style planning + backtracking
# ============================================================
def plan_proof(sorry: dict) -> list[dict]:
"""Generate ranked proof strategies for a sorry (DML planner phase)."""
strategy_ids = sorry.get("strategies", [])
ranked = []
for sid in strategy_ids:
strategy = next((s for s in PROOF_STRATEGIES if s["id"] == sid), None)
if strategy:
ranked.append(strategy)
# Add fallback if not already present
if not any(s["id"] == "sorry_fallback" for s in ranked):
ranked.append(next(s for s in PROOF_STRATEGIES if s["id"] == "sorry_fallback"))
return ranked
def build_strategy_prompt(sorry: dict, strategy: dict) -> str:
"""Build a focused prompt for one sorry + one strategy."""
return f"""Prove this Lean 4 theorem using the specified tactic strategy.
Theorem:
{sorry['context']}
Strategy: {strategy['prompt']}
Suggested tactic: {strategy['template']}
Provide ONLY the tactic proof (the `by` block). No markdown fences."""
def execute_with_backtracking(
sorry: dict,
endpoint: dict,
max_retries: int = 3,
) -> dict:
"""Try proof strategies in order, backtracking on failure (DML executor phase)."""
strategies = plan_proof(sorry)
attempts = []
for i, strategy in enumerate(strategies[:max_retries]):
prompt = build_strategy_prompt(sorry, strategy)
result = call_model(endpoint, prompt, sorry["id"])
result["strategy_id"] = strategy["id"]
result["strategy_tactic"] = strategy["tactic"]
result["attempt"] = i + 1
attempts.append(result)
# Check if the response looks like a valid proof (not sorry)
if result["success"] and result["response"]:
response = result["response"].strip()
if "sorry" not in response.lower() or response.startswith("sorry -- TODO"):
# Looks like a real proof attempt
result["looks_valid"] = True
break
else:
result["looks_valid"] = False
# Backtrack: try next strategy
continue
else:
result["looks_valid"] = False
continue
# Return the best attempt
best = attempts[-1] if attempts else {
"sorry_id": sorry["id"],
"endpoint": endpoint["name"],
"response": None,
"elapsed_s": 0,
"success": False,
"error": "No attempts made",
"strategy_id": None,
"attempt": 0,
"looks_valid": False,
}
best["total_attempts"] = len(attempts)
best["all_attempts"] = [
{
"strategy": a.get("strategy_id"),
"success": a["success"],
"elapsed_s": a["elapsed_s"],
"looks_valid": a.get("looks_valid", False),
"preview": (a.get("response") or "")[:100],
}
for a in attempts
]
return best
# ============================================================
# Main fusion logic
# ============================================================
def run_fusion(
sorries: list[dict],
endpoints: dict[str, dict],
output_dir: Path,
max_retries: int = 3,
max_workers: int = 6,
difficulty_split: bool = False,
) -> dict:
"""Run parallel DML-style fusion across all endpoints and sorries."""
output_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
results = []
tasks = []
# Difficulty-aware routing: when enabled and all 3 standard endpoints are
# present, route each sorry to the endpoint best suited to its difficulty.
# easy → qfox (GPU, fast iteration)
# medium → neon-step (StepFun, strong math reasoning)
# hard → neon-qwen (Qwen3.6-35B, large context)
# Fallback: each endpoint still tries the sorry if its primary isn't present.
difficulty_map = {
# easy: GPU first (fast), fallback to step
"easy": ["qfox", "neon-step", "neon-deepseek"],
# medium: StepFun strong at math reasoning; GPU backup
"medium": ["neon-step", "qfox", "neon-deepseek"],
# hard: purpose-built Lean prover first, then FreeLLMAPI overflow, then GPU
"hard": ["neon-deepseek", "freellmapi", "qfox"],
}
use_split = difficulty_split and len(endpoints) >= 2
for sorry in sorries:
if use_split:
preferred = difficulty_map.get(sorry.get("difficulty", "hard"), list(endpoints.keys()))
# Assign to the first preferred endpoint that's available
ep_key = next((k for k in preferred if k in endpoints), list(endpoints.keys())[0])
tasks.append((ep_key, endpoints[ep_key], sorry))
else:
for ep_key, ep_config in endpoints.items():
tasks.append((ep_key, ep_config, sorry))
print(f"=== ChentsovFinite DML Fusion Run ===")
print(f"Timestamp: {timestamp}")
print(f"Sorries: {len(sorries)}")
print(f"Endpoints: {len(endpoints)}")
print(f"Max retries per sorry: {max_retries}")
print(f"Total tasks: {len(tasks)}")
print(f"Max workers: {max_workers}")
print("---")
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {}
for ep_key, ep_config, sorry in tasks:
future = executor.submit(
execute_with_backtracking, sorry, ep_config, max_retries
)
futures[future] = (ep_key, sorry)
for future in as_completed(futures):
ep_key, sorry = futures[future]
result = future.result()
result["sorry_name"] = sorry["name"]
result["difficulty"] = sorry["difficulty"]
result["endpoint_key"] = ep_key
results.append(result)
valid = "VALID" if result.get("looks_valid") else "SORRY"
attempts = result.get("total_attempts", 0)
print(
f" [{ep_key}] SORRY {result['sorry_id']:2d} ({sorry['name']:30s}) "
f"{result['elapsed_s']:6.1f}s {attempts} attempts {valid}"
)
# Save results
output_file = output_dir / f"chentsov_fusion_{timestamp}.json"
with open(output_file, "w") as f:
json.dump(
{
"timestamp": timestamp,
"sorries": len(sorries),
"endpoints": list(endpoints.keys()),
"max_retries": max_retries,
"results": results,
},
f,
indent=2,
)
# Generate summary
summary_file = output_dir / f"chentsov_fusion_{timestamp}_summary.md"
with open(summary_file, "w") as f:
f.write(f"# ChentsovFinite DML Fusion Results — {timestamp}\n\n")
f.write(f"**Sorries attacked:** {len(sorries)}\n")
f.write(f"**Endpoints:** {', '.join(endpoints.keys())}\n")
f.write(f"**Max retries:** {max_retries}\n\n")
# Stats
valid_count = sum(1 for r in results if r.get("looks_valid"))
total_attempts = sum(r.get("total_attempts", 0) for r in results)
f.write(f"**Valid proofs found:** {valid_count}/{len(results)}\n")
f.write(f"**Total attempts:** {total_attempts}\n\n")
by_sorry = {}
for r in results:
sid = r["sorry_id"]
if sid not in by_sorry:
by_sorry[sid] = []
by_sorry[sid].append(r)
for sid in sorted(by_sorry.keys()):
sorry_info = next(s for s in sorries if s["id"] == sid)
f.write(f"## SORRY {sid}: {sorry_info['name']} ({sorry_info['difficulty']})\n\n")
for r in sorted(by_sorry[sid], key=lambda x: x["elapsed_s"]):
valid = "VALID" if r.get("looks_valid") else "SORRY"
f.write(f"### {r['endpoint']}{r['elapsed_s']}s — {valid}\n\n")
f.write(f"Strategy: `{r.get('strategy_id')}` ({r.get('total_attempts', 0)} attempts)\n\n")
if r.get("all_attempts"):
f.write("| # | Strategy | Valid | Time | Preview |\n")
f.write("|---|----------|-------|------|----------|\n")
for a in r["all_attempts"]:
v = "Y" if a["looks_valid"] else "N"
f.write(f"| {a.get('attempt', '?')} | {a['strategy']} | {v} | {a['elapsed_s']}s | `{a['preview'][:60]}...` |\n")
f.write("\n")
if r["response"]:
f.write(f"```lean\n{r['response']}\n```\n\n")
else:
f.write(f"Error: {r['error']}\n\n")
print(f"---")
print(f"Results: {output_file}")
print(f"Summary: {summary_file}")
print(f"Valid proofs: {valid_count}/{len(results)}")
return {"output_file": str(output_file), "summary_file": str(summary_file)}
# ============================================================
# Main
# ============================================================
def main():
parser = argparse.ArgumentParser(description="ChentsovFinite DML fusion")
parser.add_argument("--sorries", type=str, default="all")
parser.add_argument("--output", type=str, default="/tmp/chentsov_fusion")
parser.add_argument("--endpoints", type=str, default="qfox,neon-qwen")
parser.add_argument("--max-retries", type=int, default=3)
parser.add_argument("--workers", type=int, default=6)
parser.add_argument("--difficulty-split", action="store_true",
help="Route easy→qfox, medium→neon-step, hard→neon-qwen (17 tasks) "
"instead of all endpoints trying all sorries (51 tasks)")
args = parser.parse_args()
if args.sorries == "all":
selected = SORRIES
else:
ids = [int(x.strip()) for x in args.sorries.split(",")]
selected = [s for s in SORRIES if s["id"] in ids]
ep_keys = [x.strip() for x in args.endpoints.split(",")]
selected_eps = {k: ENDPOINTS[k] for k in ep_keys if k in ENDPOINTS}
if not selected_eps:
print(f"Error: no valid endpoints in {args.endpoints}")
print(f"Available: {', '.join(ENDPOINTS.keys())}")
sys.exit(1)
run_fusion(selected, selected_eps, Path(args.output), args.max_retries, args.workers,
difficulty_split=args.difficulty_split)
if __name__ == "__main__":
main()