Spaces:
Running
Running
ship: SZLHOLDINGS/lean-kernel — live Lean v4.13.0 + Mathlib kernel, 7 API endpoints, honest build status
Browse files- Dockerfile +48 -0
- README.md +32 -5
- app/index.html +209 -0
- app/lean_numbers.py +156 -0
- app/server.py +293 -0
- app/theorem_scan.py +86 -0
- data/reference-vectors.json +201 -0
- nginx.conf +24 -0
- requirements.txt +2 -0
- start.sh +11 -0
Dockerfile
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SZLHOLDINGS/lean-kernel — live Lean/Lake verification kernel
|
| 2 |
+
# Ubuntu 24.04 + elan + Lean v4.13.0 + Mathlib v4.13.0 + nginx + FastAPI
|
| 3 |
+
FROM ubuntu:24.04
|
| 4 |
+
|
| 5 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
| 6 |
+
ENV PATH="/root/.elan/bin:${PATH}"
|
| 7 |
+
ENV ELAN_BIN="/root/.elan/bin"
|
| 8 |
+
ENV LUTAR_REPO="/opt/lutar-lean"
|
| 9 |
+
|
| 10 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 11 |
+
git curl ca-certificates nginx python3 python3-pip python3-venv \
|
| 12 |
+
build-essential \
|
| 13 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 14 |
+
|
| 15 |
+
# --- elan (Lean toolchain manager), SHA-pinned to the same commit CI uses ---
|
| 16 |
+
RUN ELAN_SHA=3d5138e1526a569a23901b8ee559032793cf445e \
|
| 17 |
+
&& EXPECTED=4bacca9502cb89736fe63d2685abc2947cfbf34dc87673504f1bb4c43eda9264 \
|
| 18 |
+
&& curl -sSfL "https://raw.githubusercontent.com/leanprover/elan/$ELAN_SHA/elan-init.sh" -o /tmp/elan-init.sh \
|
| 19 |
+
&& echo "$EXPECTED /tmp/elan-init.sh" | sha256sum -c \
|
| 20 |
+
&& sh /tmp/elan-init.sh -y --default-toolchain none \
|
| 21 |
+
&& rm /tmp/elan-init.sh
|
| 22 |
+
|
| 23 |
+
# --- clone lutar-lean at pinned main; install the v4.13.0 toolchain ---
|
| 24 |
+
RUN git clone --depth 1 https://github.com/szl-holdings/lutar-lean.git ${LUTAR_REPO} \
|
| 25 |
+
&& cd ${LUTAR_REPO} \
|
| 26 |
+
&& cat lean-toolchain \
|
| 27 |
+
&& lake --version
|
| 28 |
+
|
| 29 |
+
# --- resolve deps + warm Mathlib cache (best-effort; build still works without) ---
|
| 30 |
+
# `lake update -R` generates lake-manifest.json; `lake exe cache get` pulls
|
| 31 |
+
# prebuilt Mathlib oleans so the on-demand `lake build` is fast & green.
|
| 32 |
+
RUN cd ${LUTAR_REPO} \
|
| 33 |
+
&& lake update -R \
|
| 34 |
+
&& (lake exe cache get || echo "WARN: mathlib cache fetch incomplete; build will compile from source on demand")
|
| 35 |
+
|
| 36 |
+
# --- python service deps ---
|
| 37 |
+
COPY requirements.txt /opt/requirements.txt
|
| 38 |
+
RUN pip3 install --no-cache-dir --break-system-packages -r /opt/requirements.txt
|
| 39 |
+
|
| 40 |
+
# --- app + data + nginx ---
|
| 41 |
+
COPY app/ /opt/app/
|
| 42 |
+
COPY data/ /opt/app/data/
|
| 43 |
+
COPY nginx.conf /etc/nginx/sites-available/default
|
| 44 |
+
COPY start.sh /opt/start.sh
|
| 45 |
+
RUN chmod +x /opt/start.sh
|
| 46 |
+
|
| 47 |
+
EXPOSE 7860
|
| 48 |
+
CMD ["/opt/start.sh"]
|
README.md
CHANGED
|
@@ -1,10 +1,37 @@
|
|
| 1 |
---
|
| 2 |
-
title: Lean Kernel
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: indigo
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Lean Kernel — Lutar Invariant Λ
|
| 3 |
+
emoji: 🔏
|
| 4 |
colorFrom: indigo
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: true
|
| 9 |
+
license: apache-2.0
|
| 10 |
+
short_description: Live Lean v4.13.0 kernel for the Lutar Invariant
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# SZLHOLDINGS/lean-kernel
|
| 14 |
+
|
| 15 |
+
Live verification kernel for [szl-holdings/lutar-lean](https://github.com/szl-holdings/lutar-lean) — the Lean 4 formalization of the **Lutar Invariant Λ**.
|
| 16 |
+
|
| 17 |
+
**Λ definition (canonical):** `Λ_k(x) = (∏ xᵢ)^(1/k)` — the unweighted geometric mean of the axis vector (equivalently, the weighted geomean with all Egyptian unit-fraction weights `1/k`). Source of truth: `Lutar/Invariant.lean` and `ouroboros/docs/lambda-spec.md`.
|
| 18 |
+
|
| 19 |
+
This Space pins **Lean v4.13.0 + Mathlib v4.13.0** (from the repo's `lean-toolchain` + `lakefile.lean`) and exposes a live API + UI.
|
| 20 |
+
|
| 21 |
+
## Endpoints
|
| 22 |
+
|
| 23 |
+
| Method | Path | Purpose |
|
| 24 |
+
|--------|------|---------|
|
| 25 |
+
| GET | `/api/lean/healthz` | build status + commit + toolchain |
|
| 26 |
+
| GET | `/api/lean/numbers` | canonical numbers, **live** from `lean_numbers.py` against the deployed commit |
|
| 27 |
+
| GET | `/api/lean/theorems` | full theorem table — every decl with status PROVEN/AXIOM/SORRY + `file:line` |
|
| 28 |
+
| POST | `/api/lean/verify` | verify a Λ-receipt `{"axes":[…],"lambda":…}`; returns Y/N + confirming theorem |
|
| 29 |
+
| GET | `/api/lean/build` | trigger `lake build`, stream output via SSE, final pass/fail + timing |
|
| 30 |
+
| GET | `/api/lean/vectors` | the 10 golden reference vectors |
|
| 31 |
+
| POST | `/api/lean/vectors/exercise` | recompute each vector's Λ against current Lean, pass/fail |
|
| 32 |
+
|
| 33 |
+
## Honesty
|
| 34 |
+
|
| 35 |
+
Numbers are **never hardcoded** — every figure is recomputed live from the deployed commit. If `lake build` fails (e.g. Mathlib cache or disk is unavailable on the box), `healthz` and the UI report **BUILD FAIL** with the verbatim error tail. Nothing is faked green.
|
| 36 |
+
|
| 37 |
+
Doctrine v10. Author: Stephen P. Lutar Jr. (ORCID 0009-0001-0110-4173), SZL Holdings.
|
app/index.html
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8"/>
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
| 6 |
+
<title>SZLHOLDINGS · lean-kernel — live Lean/Lake verification</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root{--bg:#0b0e14;--panel:#121722;--ink:#e6edf3;--mut:#8b97a7;--ok:#3fb950;--bad:#f85149;--ax:#d29922;--acc:#58a6ff;--line:#222b38}
|
| 9 |
+
*{box-sizing:border-box}
|
| 10 |
+
body{margin:0;font:14px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;background:var(--bg);color:var(--ink)}
|
| 11 |
+
header{padding:22px 26px;border-bottom:1px solid var(--line);background:linear-gradient(180deg,#0e1320,#0b0e14)}
|
| 12 |
+
h1{margin:0;font-size:20px;letter-spacing:.3px}
|
| 13 |
+
.sub{color:var(--mut);margin-top:4px;font-size:12.5px}
|
| 14 |
+
main{max-width:1100px;margin:0 auto;padding:22px 18px 60px}
|
| 15 |
+
.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
| 16 |
+
@media(max-width:780px){.grid{grid-template-columns:1fr}}
|
| 17 |
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px;margin-bottom:16px}
|
| 18 |
+
.card h2{margin:0 0 10px;font-size:14px;color:var(--acc);text-transform:uppercase;letter-spacing:.6px}
|
| 19 |
+
.pill{display:inline-block;padding:2px 9px;border-radius:999px;font-size:12px;font-weight:600}
|
| 20 |
+
.pill.ok{background:rgba(63,185,80,.15);color:var(--ok)}
|
| 21 |
+
.pill.bad{background:rgba(248,81,73,.15);color:var(--bad)}
|
| 22 |
+
.pill.ax{background:rgba(210,153,34,.15);color:var(--ax)}
|
| 23 |
+
.pill.un{background:rgba(139,151,167,.15);color:var(--mut)}
|
| 24 |
+
button{background:#1f6feb;color:#fff;border:0;border-radius:7px;padding:8px 14px;font:inherit;cursor:pointer}
|
| 25 |
+
button:hover{background:#388bfd}
|
| 26 |
+
button.ghost{background:#1c2333;color:var(--ink);border:1px solid var(--line)}
|
| 27 |
+
pre{background:#0a0d13;border:1px solid var(--line);border-radius:8px;padding:10px;max-height:280px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:12px}
|
| 28 |
+
table{width:100%;border-collapse:collapse;font-size:12px}
|
| 29 |
+
th,td{text-align:left;padding:5px 8px;border-bottom:1px solid var(--line)}
|
| 30 |
+
th{color:var(--mut);position:sticky;top:0;background:var(--panel)}
|
| 31 |
+
.tblwrap{max-height:420px;overflow:auto;border:1px solid var(--line);border-radius:8px}
|
| 32 |
+
.num{font-size:26px;font-weight:700}
|
| 33 |
+
.kv{display:flex;justify-content:space-between;padding:3px 0;border-bottom:1px dashed var(--line)}
|
| 34 |
+
.kv span:first-child{color:var(--mut)}
|
| 35 |
+
textarea{width:100%;min-height:120px;background:#0a0d13;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px;font:inherit}
|
| 36 |
+
a{color:var(--acc)}
|
| 37 |
+
.row{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px}
|
| 38 |
+
.stat{display:flex;gap:18px;flex-wrap:wrap}
|
| 39 |
+
.stat>div{min-width:90px}
|
| 40 |
+
code{background:#0a0d13;padding:1px 5px;border-radius:4px}
|
| 41 |
+
</style>
|
| 42 |
+
</head>
|
| 43 |
+
<body>
|
| 44 |
+
<header>
|
| 45 |
+
<h1>SZLHOLDINGS · lean-kernel</h1>
|
| 46 |
+
<div class="sub">Live Lean v4.13.0 + Mathlib v4.13.0 kernel for <a href="https://github.com/szl-holdings/lutar-lean">szl-holdings/lutar-lean</a> · the Lutar Invariant Λ. Numbers are computed live from <code>lean_numbers.py</code> against the deployed commit — never hardcoded. If the build fails, this page shows it honestly.</div>
|
| 47 |
+
</header>
|
| 48 |
+
<main>
|
| 49 |
+
|
| 50 |
+
<div class="card" id="health">
|
| 51 |
+
<h2>Kernel status</h2>
|
| 52 |
+
<div class="row"><span class="pill un" id="hpill">loading…</span>
|
| 53 |
+
<span class="sub" id="hmeta"></span></div>
|
| 54 |
+
<div class="kv"><span>repo</span><span id="hrepo">—</span></div>
|
| 55 |
+
<div class="kv"><span>commit</span><span id="hsha">—</span></div>
|
| 56 |
+
<div class="kv"><span>toolchain</span><span id="htc">—</span></div>
|
| 57 |
+
<div class="kv"><span>last lake build</span><span id="hbuild">—</span></div>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
<div class="grid">
|
| 61 |
+
<div class="card">
|
| 62 |
+
<h2>Canonical numbers (live)</h2>
|
| 63 |
+
<div class="stat">
|
| 64 |
+
<div><div class="num" id="ndecl">—</div><div class="sub">declarations</div></div>
|
| 65 |
+
<div><div class="num" id="naxr">—</div><div class="sub">axioms (raw)</div></div>
|
| 66 |
+
<div><div class="num" id="naxu">—</div><div class="sub">axioms (unique)</div></div>
|
| 67 |
+
<div><div class="num" id="nsor">—</div><div class="sub">sorries (raw)</div></div>
|
| 68 |
+
<div><div class="num" id="nsnc">—</div><div class="sub">sorries (non-comment)</div></div>
|
| 69 |
+
</div>
|
| 70 |
+
<div class="sub" id="nmeta" style="margin-top:8px"></div>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<div class="card">
|
| 74 |
+
<h2>lake build (on demand)</h2>
|
| 75 |
+
<div class="row"><button onclick="runBuild()" id="bbtn">Run lake build</button>
|
| 76 |
+
<span class="pill un" id="bpill">idle</span>
|
| 77 |
+
<span class="sub" id="btime"></span></div>
|
| 78 |
+
<pre id="blog">Press “Run lake build” to kernel-check the whole library live.
|
| 79 |
+
(First run on a cold box downloads the Mathlib cache and can take minutes;
|
| 80 |
+
if disk/cache is unavailable the failure is shown here verbatim.)</pre>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
<div class="card">
|
| 85 |
+
<h2>Theorem table (PROVEN / AXIOM / SORRY)</h2>
|
| 86 |
+
<div class="row">
|
| 87 |
+
<span class="pill ok" id="tp">PROVEN —</span>
|
| 88 |
+
<span class="pill ax" id="ta">AXIOM ���</span>
|
| 89 |
+
<span class="pill bad" id="ts">SORRY —</span>
|
| 90 |
+
<input id="tfilter" placeholder="filter by name/file…" oninput="renderTheorems()"
|
| 91 |
+
style="margin-left:auto;background:#0a0d13;color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:6px 9px;font:inherit"/>
|
| 92 |
+
</div>
|
| 93 |
+
<div class="tblwrap">
|
| 94 |
+
<table><thead><tr><th>status</th><th>kind</th><th>name</th><th>file:line</th></tr></thead>
|
| 95 |
+
<tbody id="tbody"><tr><td colspan="4" class="sub">loading…</td></tr></tbody></table>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
<div class="grid">
|
| 100 |
+
<div class="card">
|
| 101 |
+
<h2>Λ-receipt verifier playground</h2>
|
| 102 |
+
<div class="sub">POST a receipt <code>{"axes":[...], "lambda":…}</code>. The kernel recomputes Λ = (∏xᵢ)^(1/k) and names the confirming theorem.</div>
|
| 103 |
+
<textarea id="vin">{"axes":[0.9,0.9,0.9,0.9,0.9,0.9,0.9,0.9,0.9],"lambda":0.9}</textarea>
|
| 104 |
+
<div class="row" style="margin-top:8px"><button onclick="doVerify()">Verify receipt</button></div>
|
| 105 |
+
<pre id="vout">—</pre>
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
<div class="card">
|
| 109 |
+
<h2>10 golden reference vectors</h2>
|
| 110 |
+
<div class="row"><button class="ghost" onclick="loadVectors()">Load vectors</button>
|
| 111 |
+
<button onclick="exerciseVectors()">Exercise vs current Lean</button>
|
| 112 |
+
<span class="pill un" id="vpill">—</span></div>
|
| 113 |
+
<pre id="vecout">—</pre>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
|
| 117 |
+
<div class="card">
|
| 118 |
+
<h2>API</h2>
|
| 119 |
+
<div class="sub">
|
| 120 |
+
<code>GET /api/lean/healthz</code> ·
|
| 121 |
+
<code>GET /api/lean/numbers</code> ·
|
| 122 |
+
<code>GET /api/lean/theorems</code> ·
|
| 123 |
+
<code>POST /api/lean/verify</code> ·
|
| 124 |
+
<code>GET /api/lean/build</code> (SSE) ·
|
| 125 |
+
<code>GET /api/lean/vectors</code> ·
|
| 126 |
+
<code>POST /api/lean/vectors/exercise</code>
|
| 127 |
+
</div>
|
| 128 |
+
</div>
|
| 129 |
+
|
| 130 |
+
</main>
|
| 131 |
+
<script>
|
| 132 |
+
const J = async (u,o)=> (await fetch(u,o)).json();
|
| 133 |
+
let THEOREMS=[];
|
| 134 |
+
|
| 135 |
+
async function loadHealth(){
|
| 136 |
+
try{
|
| 137 |
+
const h = await J('api/lean/healthz');
|
| 138 |
+
const p=document.getElementById('hpill');
|
| 139 |
+
const b=h.build||{};
|
| 140 |
+
if(b.status==='pass'){p.className='pill ok';p.textContent='BUILD PASS';}
|
| 141 |
+
else if(b.status==='fail'){p.className='pill bad';p.textContent='BUILD FAIL';}
|
| 142 |
+
else {p.className='pill un';p.textContent='BUILD NOT YET RUN';}
|
| 143 |
+
document.getElementById('hrepo').textContent=h.repo+(h.repo_present?'':' (missing!)');
|
| 144 |
+
document.getElementById('hsha').textContent=(h.repo_sha||'').slice(0,12);
|
| 145 |
+
document.getElementById('htc').textContent=h.toolchain;
|
| 146 |
+
document.getElementById('hbuild').textContent=b.status==='unknown'?'not run yet':
|
| 147 |
+
`${b.status} · exit ${b.exit_code} · ${b.duration_s}s`;
|
| 148 |
+
}catch(e){document.getElementById('hpill').textContent='healthz error';}
|
| 149 |
+
}
|
| 150 |
+
async function loadNumbers(){
|
| 151 |
+
try{
|
| 152 |
+
const d = await J('api/lean/numbers'); const n=d.numbers||{};
|
| 153 |
+
document.getElementById('ndecl').textContent=n.declarations??'—';
|
| 154 |
+
document.getElementById('naxr').textContent=n.axioms_raw??'—';
|
| 155 |
+
document.getElementById('naxu').textContent=n.axioms_unique??'—';
|
| 156 |
+
document.getElementById('nsor').textContent=n.sorries_raw??'—';
|
| 157 |
+
document.getElementById('nsnc').textContent=n.sorries_noncomment??'—';
|
| 158 |
+
document.getElementById('nmeta').textContent=`live · sha ${(d.sha||'').slice(0,12)} · measured ${d.measured_at_utc||''}`;
|
| 159 |
+
}catch(e){document.getElementById('nmeta').textContent='numbers error: '+e;}
|
| 160 |
+
}
|
| 161 |
+
async function loadTheorems(){
|
| 162 |
+
try{
|
| 163 |
+
const d = await J('api/lean/theorems'); THEOREMS=d.items||[]; const s=d.summary||{};
|
| 164 |
+
document.getElementById('tp').textContent='PROVEN '+(s.proven??'—');
|
| 165 |
+
document.getElementById('ta').textContent='AXIOM '+(s.axiom??'—');
|
| 166 |
+
document.getElementById('ts').textContent='SORRY '+(s.sorry??'—');
|
| 167 |
+
renderTheorems();
|
| 168 |
+
}catch(e){document.getElementById('tbody').innerHTML='<tr><td colspan=4 class=sub>error: '+e+'</td></tr>';}
|
| 169 |
+
}
|
| 170 |
+
function renderTheorems(){
|
| 171 |
+
const f=(document.getElementById('tfilter').value||'').toLowerCase();
|
| 172 |
+
const rows=THEOREMS.filter(x=>!f||x.name.toLowerCase().includes(f)||x.file.toLowerCase().includes(f))
|
| 173 |
+
.map(x=>{const cls=x.status==='PROVEN'?'ok':x.status==='AXIOM'?'ax':'bad';
|
| 174 |
+
return `<tr><td><span class="pill ${cls}">${x.status}</span></td><td>${x.kind}</td><td>${x.name}</td><td class=sub>${x.file}:${x.line}</td></tr>`;}).join('');
|
| 175 |
+
document.getElementById('tbody').innerHTML=rows||'<tr><td colspan=4 class=sub>no matches</td></tr>';
|
| 176 |
+
}
|
| 177 |
+
function runBuild(){
|
| 178 |
+
const log=document.getElementById('blog'); const pill=document.getElementById('bpill');
|
| 179 |
+
log.textContent=''; pill.className='pill un'; pill.textContent='running…';
|
| 180 |
+
document.getElementById('bbtn').disabled=true;
|
| 181 |
+
const es=new EventSource('api/lean/build');
|
| 182 |
+
es.onmessage=(ev)=>{const d=JSON.parse(ev.data);
|
| 183 |
+
if(d.event==='line'){log.textContent+=d.line+'\n';log.scrollTop=log.scrollHeight;}
|
| 184 |
+
else if(d.event==='start'){log.textContent+='$ lake build\n';}
|
| 185 |
+
else if(d.event==='done'){pill.className='pill '+(d.status==='pass'?'ok':'bad');
|
| 186 |
+
pill.textContent='BUILD '+d.status.toUpperCase();
|
| 187 |
+
document.getElementById('btime').textContent=`exit ${d.exit_code} · ${d.duration_s}s`;
|
| 188 |
+
es.close();document.getElementById('bbtn').disabled=false;loadHealth();}
|
| 189 |
+
else if(d.event==='error'){pill.className='pill bad';pill.textContent='ERROR';
|
| 190 |
+
log.textContent+='ERROR: '+d.msg+'\n';es.close();document.getElementById('bbtn').disabled=false;}
|
| 191 |
+
};
|
| 192 |
+
es.onerror=()=>{pill.className='pill bad';pill.textContent='stream error';es.close();document.getElementById('bbtn').disabled=false;};
|
| 193 |
+
}
|
| 194 |
+
async function doVerify(){
|
| 195 |
+
let body; try{body=JSON.parse(document.getElementById('vin').value);}catch(e){document.getElementById('vout').textContent='invalid JSON';return;}
|
| 196 |
+
const r=await J('api/lean/verify',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)});
|
| 197 |
+
document.getElementById('vout').textContent=JSON.stringify(r,null,2);
|
| 198 |
+
}
|
| 199 |
+
async function loadVectors(){const d=await J('api/lean/vectors');document.getElementById('vecout').textContent=JSON.stringify(d,null,2);}
|
| 200 |
+
async function exerciseVectors(){
|
| 201 |
+
const r=await J('api/lean/vectors/exercise',{method:'POST'});
|
| 202 |
+
const p=document.getElementById('vpill');
|
| 203 |
+
p.className='pill '+(r.all_pass?'ok':'bad');p.textContent=r.all_pass?('ALL '+r.count+' PASS'):'SOME FAIL';
|
| 204 |
+
document.getElementById('vecout').textContent=JSON.stringify(r,null,2);
|
| 205 |
+
}
|
| 206 |
+
loadHealth();loadNumbers();loadTheorems();
|
| 207 |
+
</script>
|
| 208 |
+
</body>
|
| 209 |
+
</html>
|
app/lean_numbers.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Canonical Lean corpus counter for szl-holdings/lutar-lean.
|
| 3 |
+
|
| 4 |
+
Trust Tier 1 reproducibility script (TRUST.md TODO). Clones (or reads) the
|
| 5 |
+
lutar-lean repo at its current `main` HEAD and emits a single canonical JSON of
|
| 6 |
+
declaration / axiom / sorry counts so that public surfaces stop drifting.
|
| 7 |
+
|
| 8 |
+
The methods here are *fixed and documented* so any agent or reviewer gets the
|
| 9 |
+
same numbers from the same SHA. This file is the source of truth for the
|
| 10 |
+
"Live numbers" line, replacing hand-maintained figures.
|
| 11 |
+
|
| 12 |
+
Usage:
|
| 13 |
+
python lean_numbers.py --repo-path /path/to/lutar-lean [--out lean_numbers.json]
|
| 14 |
+
python lean_numbers.py --clone --out .github/data/lean_numbers.json
|
| 15 |
+
|
| 16 |
+
Counting methods (documented, stable):
|
| 17 |
+
- declarations: lines in Lutar/ and Main.lean whose initial token is one of
|
| 18 |
+
{theorem, lemma, def, abbrev, instance, structure, inductive, class},
|
| 19 |
+
optionally preceded by the `noncomputable` and/or `private` modifier.
|
| 20 |
+
- axioms_raw: lines whose initial token is `axiom` (after optional modifiers).
|
| 21 |
+
- axioms_unique: distinct axiom names among those.
|
| 22 |
+
- sorries_raw: total `\\bsorry\\b` token occurrences across all .lean files.
|
| 23 |
+
- sorries_noncomment: occurrences excluding lines that are pure line-comments
|
| 24 |
+
(leading `--`) — i.e. `sorry` that is live proof text.
|
| 25 |
+
- sorries_putnam / sorries_baseline: split by whether the file is under Putnam/.
|
| 26 |
+
"""
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
import argparse
|
| 30 |
+
import json
|
| 31 |
+
import os
|
| 32 |
+
import re
|
| 33 |
+
import subprocess
|
| 34 |
+
import sys
|
| 35 |
+
import tempfile
|
| 36 |
+
from datetime import datetime, timezone
|
| 37 |
+
|
| 38 |
+
LUTAR_LEAN_URL = "https://github.com/szl-holdings/lutar-lean.git"
|
| 39 |
+
|
| 40 |
+
DECL_RE = re.compile(
|
| 41 |
+
r"^(?:private\s+)?(?:noncomputable\s+)?(?:private\s+)?"
|
| 42 |
+
r"(theorem|lemma|def|abbrev|instance|structure|inductive|class)\b"
|
| 43 |
+
)
|
| 44 |
+
AXIOM_RE = re.compile(r"^(?:private\s+)?axiom\s+([A-Za-z_][A-Za-z0-9_']*)")
|
| 45 |
+
SORRY_RE = re.compile(r"\bsorry\b")
|
| 46 |
+
COMMENT_LINE_RE = re.compile(r"^\s*--")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def iter_lean_files(root: str):
|
| 50 |
+
base = os.path.join(root, "Lutar")
|
| 51 |
+
for dirpath, _dirs, files in os.walk(base):
|
| 52 |
+
for fn in files:
|
| 53 |
+
if fn.endswith(".lean"):
|
| 54 |
+
yield os.path.join(dirpath, fn)
|
| 55 |
+
main = os.path.join(root, "Main.lean")
|
| 56 |
+
if os.path.exists(main):
|
| 57 |
+
yield main
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def count(root: str) -> dict:
|
| 61 |
+
declarations = 0
|
| 62 |
+
axioms_raw = 0
|
| 63 |
+
axiom_names: set[str] = set()
|
| 64 |
+
sorries_raw = 0
|
| 65 |
+
sorries_noncomment = 0
|
| 66 |
+
sorries_putnam = 0
|
| 67 |
+
sorries_baseline = 0
|
| 68 |
+
|
| 69 |
+
for path in iter_lean_files(root):
|
| 70 |
+
is_putnam = f"{os.sep}Putnam{os.sep}" in path
|
| 71 |
+
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
| 72 |
+
for line in fh:
|
| 73 |
+
if DECL_RE.match(line):
|
| 74 |
+
declarations += 1
|
| 75 |
+
m = AXIOM_RE.match(line)
|
| 76 |
+
if m:
|
| 77 |
+
axioms_raw += 1
|
| 78 |
+
axiom_names.add(m.group(1))
|
| 79 |
+
hits = len(SORRY_RE.findall(line))
|
| 80 |
+
if hits:
|
| 81 |
+
sorries_raw += hits
|
| 82 |
+
if not COMMENT_LINE_RE.match(line):
|
| 83 |
+
sorries_noncomment += hits
|
| 84 |
+
if is_putnam:
|
| 85 |
+
sorries_putnam += hits
|
| 86 |
+
else:
|
| 87 |
+
sorries_baseline += hits
|
| 88 |
+
|
| 89 |
+
return {
|
| 90 |
+
"declarations": declarations,
|
| 91 |
+
"axioms_raw": axioms_raw,
|
| 92 |
+
"axioms_unique": len(axiom_names),
|
| 93 |
+
"axiom_names": sorted(axiom_names),
|
| 94 |
+
"sorries_raw": sorries_raw,
|
| 95 |
+
"sorries_noncomment": sorries_noncomment,
|
| 96 |
+
"sorries_putnam": sorries_putnam,
|
| 97 |
+
"sorries_baseline": sorries_baseline,
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def git_head_sha(root: str) -> str:
|
| 102 |
+
try:
|
| 103 |
+
out = subprocess.check_output(
|
| 104 |
+
["git", "-C", root, "rev-parse", "HEAD"], text=True
|
| 105 |
+
).strip()
|
| 106 |
+
return out
|
| 107 |
+
except Exception:
|
| 108 |
+
return "unknown"
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def main() -> int:
|
| 112 |
+
ap = argparse.ArgumentParser(description="Canonical lutar-lean corpus counter.")
|
| 113 |
+
ap.add_argument("--repo-path", help="Path to an existing lutar-lean checkout.")
|
| 114 |
+
ap.add_argument("--clone", action="store_true", help="Shallow-clone main first.")
|
| 115 |
+
ap.add_argument("--ref", default="main", help="Branch/SHA to count (with --clone).")
|
| 116 |
+
ap.add_argument("--out", help="Write JSON here (default: stdout).")
|
| 117 |
+
args = ap.parse_args()
|
| 118 |
+
|
| 119 |
+
tmp = None
|
| 120 |
+
repo_path = args.repo_path
|
| 121 |
+
if args.clone or not repo_path:
|
| 122 |
+
tmp = tempfile.mkdtemp(prefix="lutar-lean-")
|
| 123 |
+
subprocess.check_call(
|
| 124 |
+
["git", "clone", "--depth", "1", "--branch", args.ref, LUTAR_LEAN_URL, tmp]
|
| 125 |
+
)
|
| 126 |
+
repo_path = tmp
|
| 127 |
+
|
| 128 |
+
if not os.path.isdir(os.path.join(repo_path, "Lutar")):
|
| 129 |
+
print(f"error: {repo_path} has no Lutar/ directory", file=sys.stderr)
|
| 130 |
+
return 2
|
| 131 |
+
|
| 132 |
+
sha = git_head_sha(repo_path)
|
| 133 |
+
numbers = count(repo_path)
|
| 134 |
+
payload = {
|
| 135 |
+
"schema": "szl.lean_numbers/v1",
|
| 136 |
+
"repo": "szl-holdings/lutar-lean",
|
| 137 |
+
"ref": args.ref,
|
| 138 |
+
"sha": sha,
|
| 139 |
+
"measured_at_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
| 140 |
+
"method": "see .github/scripts/lean_numbers.py docstring (fixed grep-equivalent regexes)",
|
| 141 |
+
"numbers": numbers,
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
out_json = json.dumps(payload, indent=2) + "\n"
|
| 145 |
+
if args.out:
|
| 146 |
+
os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True)
|
| 147 |
+
with open(args.out, "w", encoding="utf-8") as fh:
|
| 148 |
+
fh.write(out_json)
|
| 149 |
+
print(f"wrote {args.out} @ {sha}")
|
| 150 |
+
else:
|
| 151 |
+
sys.stdout.write(out_json)
|
| 152 |
+
return 0
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
if __name__ == "__main__":
|
| 156 |
+
raise SystemExit(main())
|
app/server.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""SZLHOLDINGS/lean-kernel — live Lean/Lake verification kernel.
|
| 3 |
+
|
| 4 |
+
FastAPI service that:
|
| 5 |
+
- clones / uses the pinned lutar-lean repo,
|
| 6 |
+
- runs `lake build` on demand (streaming),
|
| 7 |
+
- serves canonical numbers LIVE from lean_numbers.py,
|
| 8 |
+
- exposes the theorem table (PROVEN/AXIOM/SORRY + file:line),
|
| 9 |
+
- verifies Λ-receipts against the canonical geometric-mean definition,
|
| 10 |
+
- serves + exercises the 10 golden reference vectors.
|
| 11 |
+
|
| 12 |
+
HONESTY: if `lake build` fails (e.g. no Mathlib cache / low disk on the box),
|
| 13 |
+
healthz and the UI report the failure verbatim. Nothing is faked green.
|
| 14 |
+
"""
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import asyncio
|
| 18 |
+
import json
|
| 19 |
+
import math
|
| 20 |
+
import os
|
| 21 |
+
import subprocess
|
| 22 |
+
import time
|
| 23 |
+
from pathlib import Path
|
| 24 |
+
|
| 25 |
+
from fastapi import FastAPI, Request
|
| 26 |
+
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
|
| 27 |
+
|
| 28 |
+
APP_DIR = Path(__file__).resolve().parent
|
| 29 |
+
REPO_DIR = Path(os.environ.get("LUTAR_REPO", "/opt/lutar-lean"))
|
| 30 |
+
DATA_DIR = APP_DIR / "data"
|
| 31 |
+
ELAN_BIN = os.environ.get("ELAN_BIN", "/root/.elan/bin")
|
| 32 |
+
os.environ["PATH"] = f"{ELAN_BIN}:{os.environ.get('PATH','')}"
|
| 33 |
+
|
| 34 |
+
REF_VECTORS_PATH = REPO_DIR / "reference-vectors.json"
|
| 35 |
+
if not REF_VECTORS_PATH.exists():
|
| 36 |
+
REF_VECTORS_PATH = DATA_DIR / "reference-vectors.json"
|
| 37 |
+
|
| 38 |
+
# Cache of last build result (build is expensive; numbers are recomputed live).
|
| 39 |
+
_BUILD_STATE: dict = {"status": "unknown", "ran_at": None, "duration_s": None,
|
| 40 |
+
"exit_code": None, "tail": ""}
|
| 41 |
+
|
| 42 |
+
app = FastAPI(title="SZLHOLDINGS lean-kernel", version="1.0.0")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# --------------------------------------------------------------------------
|
| 46 |
+
# Canonical Λ — geometric mean (matches Lutar/Invariant.lean & lambda-spec.md)
|
| 47 |
+
# --------------------------------------------------------------------------
|
| 48 |
+
def compute_lambda(values: list[float]) -> float:
|
| 49 |
+
if not values:
|
| 50 |
+
return 0.0
|
| 51 |
+
for v in values:
|
| 52 |
+
if not math.isfinite(v) or v < 0:
|
| 53 |
+
return 0.0
|
| 54 |
+
if v == 0:
|
| 55 |
+
return 0.0
|
| 56 |
+
k = len(values)
|
| 57 |
+
log_sum = sum(math.log(v) for v in values)
|
| 58 |
+
return math.exp(log_sum / k)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _run(cmd: list[str], cwd: Path, timeout: int = 60) -> subprocess.CompletedProcess:
|
| 62 |
+
return subprocess.run(cmd, cwd=str(cwd), capture_output=True, text=True,
|
| 63 |
+
timeout=timeout)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _repo_sha() -> str:
|
| 67 |
+
try:
|
| 68 |
+
r = _run(["git", "rev-parse", "HEAD"], REPO_DIR, timeout=15)
|
| 69 |
+
return r.stdout.strip() or "unknown"
|
| 70 |
+
except Exception:
|
| 71 |
+
return "unknown"
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# --------------------------------------------------------------------------
|
| 75 |
+
# GET /api/lean/healthz
|
| 76 |
+
# --------------------------------------------------------------------------
|
| 77 |
+
@app.get("/api/lean/healthz")
|
| 78 |
+
def healthz():
|
| 79 |
+
return JSONResponse({
|
| 80 |
+
"ok": True,
|
| 81 |
+
"service": "SZLHOLDINGS/lean-kernel",
|
| 82 |
+
"repo": "szl-holdings/lutar-lean",
|
| 83 |
+
"repo_present": REPO_DIR.exists(),
|
| 84 |
+
"repo_sha": _repo_sha(),
|
| 85 |
+
"toolchain": _toolchain(),
|
| 86 |
+
"build": _BUILD_STATE,
|
| 87 |
+
"ts": int(time.time()),
|
| 88 |
+
})
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _toolchain() -> str:
|
| 92 |
+
tc = REPO_DIR / "lean-toolchain"
|
| 93 |
+
if tc.exists():
|
| 94 |
+
return tc.read_text().strip()
|
| 95 |
+
return "unknown"
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
# --------------------------------------------------------------------------
|
| 99 |
+
# GET /api/lean/numbers — LIVE from lean_numbers.py against current commit
|
| 100 |
+
# --------------------------------------------------------------------------
|
| 101 |
+
@app.get("/api/lean/numbers")
|
| 102 |
+
def numbers():
|
| 103 |
+
try:
|
| 104 |
+
r = _run(["python3", str(APP_DIR / "lean_numbers.py"),
|
| 105 |
+
"--repo-path", str(REPO_DIR)], APP_DIR, timeout=60)
|
| 106 |
+
data = json.loads(r.stdout)
|
| 107 |
+
data["live"] = True
|
| 108 |
+
return JSONResponse(data)
|
| 109 |
+
except Exception as e:
|
| 110 |
+
return JSONResponse({"error": str(e), "live": False}, status_code=500)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
# --------------------------------------------------------------------------
|
| 114 |
+
# GET /api/lean/theorems — full classified table
|
| 115 |
+
# --------------------------------------------------------------------------
|
| 116 |
+
@app.get("/api/lean/theorems")
|
| 117 |
+
def theorems():
|
| 118 |
+
try:
|
| 119 |
+
r = _run(["python3", str(APP_DIR / "theorem_scan.py"), str(REPO_DIR)],
|
| 120 |
+
APP_DIR, timeout=60)
|
| 121 |
+
return JSONResponse(json.loads(r.stdout))
|
| 122 |
+
except Exception as e:
|
| 123 |
+
return JSONResponse({"error": str(e)}, status_code=500)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
# --------------------------------------------------------------------------
|
| 127 |
+
# POST /api/lean/verify — verify a Λ-receipt against the canonical definition
|
| 128 |
+
# --------------------------------------------------------------------------
|
| 129 |
+
@app.post("/api/lean/verify")
|
| 130 |
+
async def verify(request: Request):
|
| 131 |
+
try:
|
| 132 |
+
body = await request.json()
|
| 133 |
+
except Exception:
|
| 134 |
+
return JSONResponse({"verified": False, "reason": "invalid JSON"},
|
| 135 |
+
status_code=400)
|
| 136 |
+
|
| 137 |
+
axes = body.get("axes")
|
| 138 |
+
claimed = body.get("lambda", body.get("Lambda"))
|
| 139 |
+
if axes is None:
|
| 140 |
+
return JSONResponse({"verified": False, "reason": "missing 'axes'"},
|
| 141 |
+
status_code=400)
|
| 142 |
+
if isinstance(axes, dict):
|
| 143 |
+
values = [float(v) for v in axes.values()]
|
| 144 |
+
elif isinstance(axes, list):
|
| 145 |
+
values = [float(v) for v in axes]
|
| 146 |
+
else:
|
| 147 |
+
return JSONResponse({"verified": False, "reason": "'axes' must be list or object"},
|
| 148 |
+
status_code=400)
|
| 149 |
+
|
| 150 |
+
recomputed = compute_lambda(values)
|
| 151 |
+
if claimed is None:
|
| 152 |
+
return JSONResponse({
|
| 153 |
+
"verified": None,
|
| 154 |
+
"recomputed_lambda": recomputed,
|
| 155 |
+
"reason": "no claimed lambda to check; returning canonical recompute",
|
| 156 |
+
"theorem": "Lutar.Invariant.Λ_def (closed form (∏xᵢ)^(1/k))",
|
| 157 |
+
})
|
| 158 |
+
tol_abs, tol_rel = 1e-12, 1e-9
|
| 159 |
+
diff = abs(recomputed - float(claimed))
|
| 160 |
+
ok = diff <= tol_abs + tol_rel * max(abs(recomputed), abs(float(claimed)))
|
| 161 |
+
# which theorem confirms it
|
| 162 |
+
if all(v == values[0] for v in values):
|
| 163 |
+
thm = "Lutar.a3_normalize_proof (Λ k (const c) = c)"
|
| 164 |
+
elif min(values) == 0:
|
| 165 |
+
thm = "Lutar.Invariant.Λ_def + zero-pinning (any axis 0 ⇒ Λ=0)"
|
| 166 |
+
else:
|
| 167 |
+
thm = "Lutar.Invariant.Λ_def (closed-form geomean) + Lutar.min_le_Λ/Λ_le_max bound"
|
| 168 |
+
return JSONResponse({
|
| 169 |
+
"verified": bool(ok),
|
| 170 |
+
"claimed_lambda": float(claimed),
|
| 171 |
+
"recomputed_lambda": recomputed,
|
| 172 |
+
"abs_diff": diff,
|
| 173 |
+
"tolerance": {"abs": tol_abs, "rel": tol_rel},
|
| 174 |
+
"theorem": thm,
|
| 175 |
+
"definition": "Λ_k(x) = (∏ xᵢ)^(1/k) (unweighted geometric mean)",
|
| 176 |
+
})
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
# --------------------------------------------------------------------------
|
| 180 |
+
# GET /api/lean/build — trigger `lake build`, stream output (SSE)
|
| 181 |
+
# --------------------------------------------------------------------------
|
| 182 |
+
@app.get("/api/lean/build")
|
| 183 |
+
async def build(stream: bool = True):
|
| 184 |
+
if not stream:
|
| 185 |
+
res = await _do_build_collect()
|
| 186 |
+
return JSONResponse(res)
|
| 187 |
+
|
| 188 |
+
async def gen():
|
| 189 |
+
start = time.time()
|
| 190 |
+
yield _sse({"event": "start", "msg": "lake build starting", "cwd": str(REPO_DIR)})
|
| 191 |
+
if not REPO_DIR.exists():
|
| 192 |
+
yield _sse({"event": "error", "msg": "repo not present"})
|
| 193 |
+
return
|
| 194 |
+
proc = await asyncio.create_subprocess_exec(
|
| 195 |
+
f"{ELAN_BIN}/lake", "build",
|
| 196 |
+
cwd=str(REPO_DIR),
|
| 197 |
+
stdout=asyncio.subprocess.PIPE,
|
| 198 |
+
stderr=asyncio.subprocess.STDOUT,
|
| 199 |
+
env={**os.environ},
|
| 200 |
+
)
|
| 201 |
+
tail = []
|
| 202 |
+
assert proc.stdout
|
| 203 |
+
async for raw in proc.stdout:
|
| 204 |
+
line = raw.decode(errors="replace").rstrip("\n")
|
| 205 |
+
tail.append(line)
|
| 206 |
+
tail[:] = tail[-200:]
|
| 207 |
+
yield _sse({"event": "line", "line": line})
|
| 208 |
+
rc = await proc.wait()
|
| 209 |
+
dur = round(time.time() - start, 2)
|
| 210 |
+
status = "pass" if rc == 0 else "fail"
|
| 211 |
+
_BUILD_STATE.update({"status": status, "ran_at": int(time.time()),
|
| 212 |
+
"duration_s": dur, "exit_code": rc,
|
| 213 |
+
"tail": "\n".join(tail[-40:])})
|
| 214 |
+
yield _sse({"event": "done", "status": status, "exit_code": rc,
|
| 215 |
+
"duration_s": dur})
|
| 216 |
+
|
| 217 |
+
return StreamingResponse(gen(), media_type="text/event-stream")
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
async def _do_build_collect() -> dict:
|
| 221 |
+
start = time.time()
|
| 222 |
+
if not REPO_DIR.exists():
|
| 223 |
+
return {"status": "fail", "reason": "repo not present"}
|
| 224 |
+
proc = await asyncio.create_subprocess_exec(
|
| 225 |
+
f"{ELAN_BIN}/lake", "build",
|
| 226 |
+
cwd=str(REPO_DIR),
|
| 227 |
+
stdout=asyncio.subprocess.PIPE,
|
| 228 |
+
stderr=asyncio.subprocess.STDOUT,
|
| 229 |
+
env={**os.environ},
|
| 230 |
+
)
|
| 231 |
+
out, _ = await proc.communicate()
|
| 232 |
+
rc = proc.returncode
|
| 233 |
+
dur = round(time.time() - start, 2)
|
| 234 |
+
status = "pass" if rc == 0 else "fail"
|
| 235 |
+
tail = "\n".join(out.decode(errors="replace").splitlines()[-40:])
|
| 236 |
+
_BUILD_STATE.update({"status": status, "ran_at": int(time.time()),
|
| 237 |
+
"duration_s": dur, "exit_code": rc, "tail": tail})
|
| 238 |
+
return {"status": status, "exit_code": rc, "duration_s": dur, "tail": tail}
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def _sse(obj: dict) -> str:
|
| 242 |
+
return f"data: {json.dumps(obj)}\n\n"
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
# --------------------------------------------------------------------------
|
| 246 |
+
# GET /api/lean/vectors — the 10 golden reference vectors
|
| 247 |
+
# --------------------------------------------------------------------------
|
| 248 |
+
@app.get("/api/lean/vectors")
|
| 249 |
+
def vectors():
|
| 250 |
+
try:
|
| 251 |
+
return JSONResponse(json.loads(REF_VECTORS_PATH.read_text()))
|
| 252 |
+
except Exception as e:
|
| 253 |
+
return JSONResponse({"error": str(e)}, status_code=500)
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
# --------------------------------------------------------------------------
|
| 257 |
+
# POST /api/lean/vectors/exercise — recompute each vector's Λ, pass/fail vs pinned
|
| 258 |
+
# --------------------------------------------------------------------------
|
| 259 |
+
@app.post("/api/lean/vectors/exercise")
|
| 260 |
+
def exercise_vectors():
|
| 261 |
+
try:
|
| 262 |
+
spec = json.loads(REF_VECTORS_PATH.read_text())
|
| 263 |
+
except Exception as e:
|
| 264 |
+
return JSONResponse({"error": str(e)}, status_code=500)
|
| 265 |
+
tol_abs = spec.get("toleranceAbs", 1e-12)
|
| 266 |
+
tol_rel = spec.get("toleranceRel", 1e-9)
|
| 267 |
+
results = []
|
| 268 |
+
all_pass = True
|
| 269 |
+
for v in spec.get("vectors", []):
|
| 270 |
+
recomputed = compute_lambda([float(a) for a in v["axes"]])
|
| 271 |
+
pinned = float(v["lambda"])
|
| 272 |
+
diff = abs(recomputed - pinned)
|
| 273 |
+
ok = diff <= tol_abs + tol_rel * max(abs(recomputed), abs(pinned))
|
| 274 |
+
all_pass = all_pass and ok
|
| 275 |
+
results.append({"id": v["id"], "pinned_lambda": pinned,
|
| 276 |
+
"recomputed_lambda": recomputed, "abs_diff": diff,
|
| 277 |
+
"pass": ok})
|
| 278 |
+
return JSONResponse({
|
| 279 |
+
"definition": spec.get("formula", "Λ_k(x) = (∏ xᵢ)^(1/k)"),
|
| 280 |
+
"tolerance": {"abs": tol_abs, "rel": tol_rel},
|
| 281 |
+
"all_pass": all_pass,
|
| 282 |
+
"count": len(results),
|
| 283 |
+
"results": results,
|
| 284 |
+
})
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
# --------------------------------------------------------------------------
|
| 288 |
+
# UI
|
| 289 |
+
# --------------------------------------------------------------------------
|
| 290 |
+
@app.get("/", response_class=HTMLResponse)
|
| 291 |
+
@app.get("/api/lean/", response_class=HTMLResponse)
|
| 292 |
+
def ui():
|
| 293 |
+
return (APP_DIR / "index.html").read_text()
|
app/theorem_scan.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Scan the lutar-lean corpus for theorems/lemmas and classify each as
|
| 3 |
+
PROVEN / AXIOM / SORRY with file:line. Honest by construction:
|
| 4 |
+
|
| 5 |
+
- AXIOM: a line whose initial token (after optional private modifier) is `axiom`.
|
| 6 |
+
- SORRY: a theorem/lemma/def whose proof body contains a non-comment `sorry`
|
| 7 |
+
token before the next top-level declaration.
|
| 8 |
+
- PROVEN: a theorem/lemma with no `sorry` in its body and not an axiom.
|
| 9 |
+
|
| 10 |
+
This is a static classifier (no kernel run). When the kernel build is GREEN,
|
| 11 |
+
PROVEN means the elaborator accepted the proof; SORRY entries are the open
|
| 12 |
+
obligations the kernel admits via the `sorry` axiom; AXIOM entries are explicit
|
| 13 |
+
postulates. The classification is conservative: any ambiguity is reported as the
|
| 14 |
+
weaker status (SORRY > AXIOM > PROVEN in honesty ordering is not assumed; we just
|
| 15 |
+
flag the literal tokens present).
|
| 16 |
+
"""
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
import json, os, re, sys
|
| 19 |
+
|
| 20 |
+
DECL_RE = re.compile(
|
| 21 |
+
r"^(?P<indent>\s*)(?:@\[[^\]]*\]\s*)?(?:private\s+)?(?:noncomputable\s+)?(?:private\s+)?"
|
| 22 |
+
r"(?P<kind>theorem|lemma|def|abbrev|instance)\s+(?P<name>[A-Za-z_][A-Za-z0-9_'.]*)"
|
| 23 |
+
)
|
| 24 |
+
AXIOM_RE = re.compile(r"^\s*(?:private\s+)?axiom\s+(?P<name>[A-Za-z_][A-Za-z0-9_'.]*)")
|
| 25 |
+
SORRY_RE = re.compile(r"\bsorry\b")
|
| 26 |
+
COMMENT_LINE_RE = re.compile(r"^\s*--")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def iter_lean_files(root: str):
|
| 30 |
+
for base in ("Lutar", "TH8"):
|
| 31 |
+
d = os.path.join(root, base)
|
| 32 |
+
if os.path.isdir(d):
|
| 33 |
+
for dp, _dirs, files in os.walk(d):
|
| 34 |
+
for fn in files:
|
| 35 |
+
if fn.endswith(".lean"):
|
| 36 |
+
yield os.path.join(dp, fn)
|
| 37 |
+
for top in ("Main.lean", "MainRef.lean", "Lutar.lean", "RefVectors.lean"):
|
| 38 |
+
p = os.path.join(root, top)
|
| 39 |
+
if os.path.exists(p):
|
| 40 |
+
yield p
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def scan(root: str) -> list[dict]:
|
| 44 |
+
out = []
|
| 45 |
+
for path in sorted(iter_lean_files(root)):
|
| 46 |
+
rel = os.path.relpath(path, root)
|
| 47 |
+
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
| 48 |
+
lines = fh.readlines()
|
| 49 |
+
# First pass: axioms
|
| 50 |
+
for i, line in enumerate(lines):
|
| 51 |
+
m = AXIOM_RE.match(line)
|
| 52 |
+
if m:
|
| 53 |
+
out.append({"name": m.group("name"), "kind": "axiom",
|
| 54 |
+
"status": "AXIOM", "file": rel, "line": i + 1})
|
| 55 |
+
# Second pass: decls + body sorry detection
|
| 56 |
+
decl_idxs = [i for i, ln in enumerate(lines) if DECL_RE.match(ln)]
|
| 57 |
+
for j, i in enumerate(decl_idxs):
|
| 58 |
+
m = DECL_RE.match(lines[i])
|
| 59 |
+
name = m.group("name")
|
| 60 |
+
kind = m.group("kind")
|
| 61 |
+
end = decl_idxs[j + 1] if j + 1 < len(decl_idxs) else len(lines)
|
| 62 |
+
body = lines[i:end]
|
| 63 |
+
has_sorry = any(SORRY_RE.search(ln) and not COMMENT_LINE_RE.match(ln)
|
| 64 |
+
for ln in body)
|
| 65 |
+
# only count theorem/lemma as "theorems"; def/abbrev/instance noted too
|
| 66 |
+
status = "SORRY" if has_sorry else "PROVEN"
|
| 67 |
+
out.append({"name": name, "kind": kind, "status": status,
|
| 68 |
+
"file": rel, "line": i + 1})
|
| 69 |
+
return out
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def summarize(items: list[dict]) -> dict:
|
| 73 |
+
theorems = [x for x in items if x["kind"] in ("theorem", "lemma")]
|
| 74 |
+
return {
|
| 75 |
+
"total_declarations": len(items),
|
| 76 |
+
"theorems_and_lemmas": len(theorems),
|
| 77 |
+
"proven": sum(1 for x in theorems if x["status"] == "PROVEN"),
|
| 78 |
+
"sorry": sum(1 for x in items if x["status"] == "SORRY"),
|
| 79 |
+
"axiom": sum(1 for x in items if x["status"] == "AXIOM"),
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
if __name__ == "__main__":
|
| 84 |
+
root = sys.argv[1] if len(sys.argv) > 1 else "."
|
| 85 |
+
items = scan(root)
|
| 86 |
+
print(json.dumps({"summary": summarize(items), "items": items}, indent=2))
|
data/reference-vectors.json
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema": "https://szlholdings.com/schemas/lutar-reference-vectors-v1.json",
|
| 3 |
+
"formula": "Λ_k(x) = (∏ xᵢ)^(1/k)",
|
| 4 |
+
"k": 9,
|
| 5 |
+
"generatedAt": "2026-05-12T06:51:49.794Z",
|
| 6 |
+
"source": "@workspace/ouroboros-invariant lutarInvariant9 (TS canonical)",
|
| 7 |
+
"toleranceAbs": 1e-12,
|
| 8 |
+
"toleranceRel": 1e-9,
|
| 9 |
+
"vectors": [
|
| 10 |
+
{
|
| 11 |
+
"id": "uniform-0_9",
|
| 12 |
+
"axes": [
|
| 13 |
+
0.9,
|
| 14 |
+
0.9,
|
| 15 |
+
0.9,
|
| 16 |
+
0.9,
|
| 17 |
+
0.9,
|
| 18 |
+
0.9,
|
| 19 |
+
0.9,
|
| 20 |
+
0.9,
|
| 21 |
+
0.9
|
| 22 |
+
],
|
| 23 |
+
"lambda": 0.9,
|
| 24 |
+
"bound": {
|
| 25 |
+
"lower": 0.9,
|
| 26 |
+
"upper": 0.9
|
| 27 |
+
}
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"id": "uniform-0_5",
|
| 31 |
+
"axes": [
|
| 32 |
+
0.5,
|
| 33 |
+
0.5,
|
| 34 |
+
0.5,
|
| 35 |
+
0.5,
|
| 36 |
+
0.5,
|
| 37 |
+
0.5,
|
| 38 |
+
0.5,
|
| 39 |
+
0.5,
|
| 40 |
+
0.5
|
| 41 |
+
],
|
| 42 |
+
"lambda": 0.5,
|
| 43 |
+
"bound": {
|
| 44 |
+
"lower": 0.5,
|
| 45 |
+
"upper": 0.5
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"id": "uniform-0_1",
|
| 50 |
+
"axes": [
|
| 51 |
+
0.1,
|
| 52 |
+
0.1,
|
| 53 |
+
0.1,
|
| 54 |
+
0.1,
|
| 55 |
+
0.1,
|
| 56 |
+
0.1,
|
| 57 |
+
0.1,
|
| 58 |
+
0.1,
|
| 59 |
+
0.1
|
| 60 |
+
],
|
| 61 |
+
"lambda": 0.10000000000000006,
|
| 62 |
+
"bound": {
|
| 63 |
+
"lower": 0.1,
|
| 64 |
+
"upper": 0.1
|
| 65 |
+
}
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"id": "ascending",
|
| 69 |
+
"axes": [
|
| 70 |
+
0.1,
|
| 71 |
+
0.2,
|
| 72 |
+
0.3,
|
| 73 |
+
0.4,
|
| 74 |
+
0.5,
|
| 75 |
+
0.6,
|
| 76 |
+
0.7,
|
| 77 |
+
0.8,
|
| 78 |
+
0.9
|
| 79 |
+
],
|
| 80 |
+
"lambda": 0.4147166274396913,
|
| 81 |
+
"bound": {
|
| 82 |
+
"lower": 0.1,
|
| 83 |
+
"upper": 0.9
|
| 84 |
+
}
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"id": "descending",
|
| 88 |
+
"axes": [
|
| 89 |
+
0.9,
|
| 90 |
+
0.8,
|
| 91 |
+
0.7,
|
| 92 |
+
0.6,
|
| 93 |
+
0.5,
|
| 94 |
+
0.4,
|
| 95 |
+
0.3,
|
| 96 |
+
0.2,
|
| 97 |
+
0.1
|
| 98 |
+
],
|
| 99 |
+
"lambda": 0.41471662743969134,
|
| 100 |
+
"bound": {
|
| 101 |
+
"lower": 0.1,
|
| 102 |
+
"upper": 0.9
|
| 103 |
+
}
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"id": "one-zero",
|
| 107 |
+
"axes": [
|
| 108 |
+
0.9,
|
| 109 |
+
0.9,
|
| 110 |
+
0.9,
|
| 111 |
+
0.9,
|
| 112 |
+
0,
|
| 113 |
+
0.9,
|
| 114 |
+
0.9,
|
| 115 |
+
0.9,
|
| 116 |
+
0.9
|
| 117 |
+
],
|
| 118 |
+
"lambda": 0,
|
| 119 |
+
"bound": {
|
| 120 |
+
"lower": 0,
|
| 121 |
+
"upper": 0.9
|
| 122 |
+
}
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
"id": "all-ones",
|
| 126 |
+
"axes": [
|
| 127 |
+
1,
|
| 128 |
+
1,
|
| 129 |
+
1,
|
| 130 |
+
1,
|
| 131 |
+
1,
|
| 132 |
+
1,
|
| 133 |
+
1,
|
| 134 |
+
1,
|
| 135 |
+
1
|
| 136 |
+
],
|
| 137 |
+
"lambda": 1,
|
| 138 |
+
"bound": {
|
| 139 |
+
"lower": 1,
|
| 140 |
+
"upper": 1
|
| 141 |
+
}
|
| 142 |
+
},
|
| 143 |
+
{
|
| 144 |
+
"id": "tiny-perturbation",
|
| 145 |
+
"axes": [
|
| 146 |
+
0.5,
|
| 147 |
+
0.5,
|
| 148 |
+
0.5,
|
| 149 |
+
0.5,
|
| 150 |
+
0.5,
|
| 151 |
+
0.5,
|
| 152 |
+
0.5,
|
| 153 |
+
0.5,
|
| 154 |
+
0.500001
|
| 155 |
+
],
|
| 156 |
+
"lambda": 0.5000001111110124,
|
| 157 |
+
"bound": {
|
| 158 |
+
"lower": 0.5,
|
| 159 |
+
"upper": 0.500001
|
| 160 |
+
}
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"id": "ground-truth-mirror-eval",
|
| 164 |
+
"axes": [
|
| 165 |
+
0.95,
|
| 166 |
+
0.95,
|
| 167 |
+
0.95,
|
| 168 |
+
0.95,
|
| 169 |
+
0.95,
|
| 170 |
+
0.95,
|
| 171 |
+
0.98,
|
| 172 |
+
0.99,
|
| 173 |
+
0.96
|
| 174 |
+
],
|
| 175 |
+
"lambda": 0.9587808437701262,
|
| 176 |
+
"bound": {
|
| 177 |
+
"lower": 0.95,
|
| 178 |
+
"upper": 0.99
|
| 179 |
+
}
|
| 180 |
+
},
|
| 181 |
+
{
|
| 182 |
+
"id": "noisy-borderline",
|
| 183 |
+
"axes": [
|
| 184 |
+
0.45,
|
| 185 |
+
0.46,
|
| 186 |
+
0.47,
|
| 187 |
+
0.48,
|
| 188 |
+
0.49,
|
| 189 |
+
0.5,
|
| 190 |
+
0.51,
|
| 191 |
+
0.52,
|
| 192 |
+
0.53
|
| 193 |
+
],
|
| 194 |
+
"lambda": 0.48931852413764076,
|
| 195 |
+
"bound": {
|
| 196 |
+
"lower": 0.45,
|
| 197 |
+
"upper": 0.53
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
]
|
| 201 |
+
}
|
nginx.conf
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
server {
|
| 2 |
+
listen 7860 default_server;
|
| 3 |
+
server_name _;
|
| 4 |
+
client_max_body_size 4m;
|
| 5 |
+
|
| 6 |
+
# SSE / streaming build endpoint needs buffering off + long timeout
|
| 7 |
+
location /api/lean/build {
|
| 8 |
+
proxy_pass http://127.0.0.1:8000;
|
| 9 |
+
proxy_http_version 1.1;
|
| 10 |
+
proxy_set_header Connection "";
|
| 11 |
+
proxy_buffering off;
|
| 12 |
+
proxy_cache off;
|
| 13 |
+
proxy_read_timeout 3600s;
|
| 14 |
+
add_header X-Accel-Buffering no;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
location / {
|
| 18 |
+
proxy_pass http://127.0.0.1:8000;
|
| 19 |
+
proxy_http_version 1.1;
|
| 20 |
+
proxy_set_header Host $host;
|
| 21 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 22 |
+
proxy_read_timeout 120s;
|
| 23 |
+
}
|
| 24 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.6
|
| 2 |
+
uvicorn[standard]==0.34.0
|
start.sh
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -e
|
| 3 |
+
export PATH="/root/.elan/bin:${PATH}"
|
| 4 |
+
echo "[start] toolchain: $(cat ${LUTAR_REPO:-/opt/lutar-lean}/lean-toolchain 2>/dev/null)"
|
| 5 |
+
echo "[start] lean: $(lean --version 2>/dev/null || echo missing)"
|
| 6 |
+
# Launch FastAPI (uvicorn) on 8000; nginx proxies 7860 -> 8000.
|
| 7 |
+
cd /opt/app
|
| 8 |
+
uvicorn server:app --host 127.0.0.1 --port 8000 --workers 1 &
|
| 9 |
+
sleep 2
|
| 10 |
+
echo "[start] starting nginx on 7860"
|
| 11 |
+
nginx -g 'daemon off;'
|