betterwithage commited on
Commit
5520f8e
·
verified ·
1 Parent(s): 129288c

ship: SZLHOLDINGS/lean-kernel — live Lean v4.13.0 + Mathlib kernel, 7 API endpoints, honest build status

Browse files
Files changed (10) hide show
  1. Dockerfile +48 -0
  2. README.md +32 -5
  3. app/index.html +209 -0
  4. app/lean_numbers.py +156 -0
  5. app/server.py +293 -0
  6. app/theorem_scan.py +86 -0
  7. data/reference-vectors.json +201 -0
  8. nginx.conf +24 -0
  9. requirements.txt +2 -0
  10. 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: indigo
6
  sdk: docker
7
- pinned: false
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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;'