File size: 38,351 Bytes
d909bd8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# Strict Lean 4 verifier (Track L / P1)

A reward and an evaluator for formal proofs that cannot be gamed by "it compiles". Code:
`align/verify/lean/` (stdlib only), installer `scripts/lean_setup.sh`, ceiling test
`scripts/lean_ceiling_test.py` (+ frozen case tables `scripts/lean_ceiling_cases.py`), tests
`tests/test_lean_verifier.py`. The Lean toolchain is an external binary
(`docs/DEPENDENCY_POLICY.md`, "External binary: Lean 4 toolchain"), reached only through
`subprocess`.

Status (2026-10-12): implemented, tested without Lean (173 tests, fake REPL), tested against the
real toolchain (19 slow tests), mutation-tested (40 hand mutants), and **ceiling-tested**:

| | |
|---|---|
| Known-good accepted | **177 / 178 = 99.44 %** (Wilson 95 % [96.9 %, 99.9 %]); floor 98 % -> PASS |
| Adversarial accepted | **0 / 61** -> PASS (a compile-only grader would have paid 51) |
| Throughput, 8 workers | 0.57 proofs/s on a heavy unfiltered mix; 4.4 proofs/s on the median mix (GRPO shape) |
| Memory per warm REPL | 0.64-0.65 GiB PSS with 8 workers (0.24 GiB private); 0.85 GiB peak under load |
| Install | 5.57 GiB, 3 min; `import Mathlib` 3.2 s warm / 39.7 s cold |

Section 5 holds the numbers; every figure there is copied from a file under `.tmp/lean_ceiling/`.
Section 4 is the pre-registration. Section 6 lists what this verifier does not promise.

## 1. The pin

| | |
|---|---|
| Lean | `leanprover/lean4:v4.9.0-rc1` (`lean --version`: 4.9.0-rc1, commit `be6c4894e0a6`) |
| Mathlib | `leanprover-community/mathlib4@d1d1e4b72c6e009edd97cabe1d661b021fe8eeae` (2024-06-12) |
| REPL | `leanprover-community/repl@v4.9.0-rc1` (`7df77c0a1c91`), built against the same toolchain |
| elan | 4.2.4 |
| Location | `/apps/development/rlhf/lean/` (`elan/`, `workspace/` = Lake project requiring Mathlib at the rev above, `repl/`, `PINS.json`) |

**Provenance (how "the Goedel-Prover-V2 pin" was determined, not assumed).** Goedel-Prover-V2's
README says "we follow DeepSeek-Prover-V1.5, which uses Lean 4 version 4.9 and the corresponding
Mathlib". The repo's `mathlib4` git submodule is `xinhjBrant/mathlib4` at commit
`2f65ba7f1a9144b20c8e7358513548e317d26de1`. That commit is a single child (message `align`,
2024-08-07, Huajian Xin of DeepSeek) of upstream `d1d1e4b72c6e`, and changes only
`lakefile.lean` and `lake-manifest.json` to add `xinhjBrant/repl@deepseek` as a Lake dependency;
its `lean-toolchain` is `leanprover/lean4:v4.9.0-rc1`. So "Lean 4.9" in that README is
nominal: the binary is **4.9.0-rc1**, not 4.9.0.

**Deviation, and why it is safe.** We pin the *upstream parent* and build the REPL separately.
Mathlib's prebuilt `.olean` cache is keyed on a hash that includes `lakefile.lean`, so the
DeepSeek child commit would not hit the cache and would force a multi-hour Mathlib build. The
parent's Mathlib sources, and therefore the `.olean`s, are byte-identical to what the child
compiles; the only difference is which REPL fork the lakefile names. We use upstream
`repl@v4.9.0-rc1`, whose protocol (`cmd`/`env`) is all this package needs. The fork's extra
options (`allTactics`, `ast`, `premises`) are not used. Install cost with the cache: 3 minutes,
5.8 GB (Section 5).

**Version drift is a correctness issue.** SafeVerify keeps separate branches for 4.9.0 and
4.15.0; PutnamBench moved to 4.33.1 in 2026-09. Proofs verified here say nothing about 4.33.
Every number this verifier produces must be reported with this pin and the verifier tier.

## 2. Architecture and what each layer defends

```
completion text ──extract_lean_code──▶ code ──static_check──▶ Plan
                     (reward.py)             (strict.py: lexer, bans, header, target)
                                                   │ reject: no Lean touched
                                                   ▼
   ReplPool.acquire ─▶ Worker: [import once] ─▶ [audit prelude] ─▶ [header opens]
   (repl_pool.py)                                                       │
                          ref = `theorem _align_ref <given signature> := sorry` ──▶ env R
                          candidate body, run in env R ──▶ env C   (errors / sorry ⇒ reject)
                          `#align_audit "<nonce>" 1 <target> _align_ref` in env C ──▶ verdict JSON
```

| Layer | Rejects | Independent of the lexer? |
|---|---|---|
| `extract_lean_code` | no fence / truncated (unclosed) fence / prose | n/a |
| header equality | any change to imports, `open`s, options (token-for-token vs the reference header) | yes (token compare) |
| lexical bans | `sorry`/`admit`, `native_decide`, `unsafe`/`partial`, `implemented_by`/`extern`/`export`, `axiom`/`opaque`, `macro`/`elab`/`syntax`/`notation`/`run_cmd`/`initialize`, any `#cmd`, `namespace`/`section`/`variable`/`attribute`/`local`, `instance`, `def`/`abbrev` (unless `allow_helper_defs`), `Lean.*`/`IO.*` identifiers, `exact?`/`apply?`, non-whitelisted `set_option` (incl. `maxRecDepth` > 16384), attributes that change compilation/elaboration | no (this is the lexer) |
| target rules | target missing, declared twice, `private`/`protected`/attribute on it, signature tokens differ from the reference's (strict mode) | no |
| compile | any `error`; any `sorry` warning; non-empty `sorries` | yes (Lean) |
| **audit: axioms** | any axiom outside {`propext`, `Quot.sound`, `Classical.choice`}: catches `sorryAx`, `Lean.ofReduceBool`/`trustCompiler` (`native_decide`), user axioms, even if the lexer were fooled | yes |
| **audit: statement bridge** | the kernel is asked to check `theorem _align_bridge : <type of _align_ref> := <candidate>`. `_align_ref` was elaborated *before* the candidate existed, so a definition-hole (instance, `variable`, shadowing) that makes the same text mean something else fails here | yes |
| **audit: kernel recheck** | every local theorem re-added with `debug.skipKernelTC` forced off | yes |
| **audit: meta-declaration scan** | a NEW local constant whose type/value mentions `Lean.Elab/Meta/Macro/Parser/PrettyPrinter/Syntax/Environment/Core/Compiler`, `IO`, `unsafeCast`: the footprint of an `elab`/`macro` that slipped past the lexer | yes |
| audit: kind | the target must be a `theorem` (not a `def` that happens to inhabit the type) | yes |

The audit verdict is the one info message tagged `ALIGN_AUDIT <random nonce>`, produced by a
**separate REPL call** after the candidate ran. The candidate's own output cannot carry the
nonce (it is generated after the candidate text is fixed), so it cannot forge a verdict.

Reward semantics (`reward.py`): `accuracy ∈ {0,1}`, 1 only if every layer passes. Diagnostics
(`StrictResult.reasons`, `.diagnostics`) are returned separately and never folded into the
reward: shaped partial credit ("compiles but has `sorry`") is exactly the gradient a policy
follows to the exploit (Leanabell-V2 also found shaped rewards slightly worse than binary).
Plug-in points, none of which change the loss or `CompositeReward`:

```python
from align.verify.lean import ReplPool, StrictVerifier, LeanAccuracy, find_toolchain
pool = ReplPool.from_toolchain(find_toolchain(), preflight=True)   # host rules + worker count
acc = LeanAccuracy(StrictVerifier(pool))
accuracy, result = acc.score(formal_statement, completion_text)    # float in {0,1}, diagnostics
terms, result = acc.reward_terms(composite, formal_statement, completion_text, fmt, n_tokens)
outcomes = acc.verify_batch(statements, completions)               # OutcomeVerifier surface
```

**Failure semantics.** A proof that times out, exhausts memory or kills its REPL scores 0 (that
is a property of the proof). A *dead verifier* must not: `LeanInfraError` is raised when the
base environment cannot be built or when more than half of a batch (of at least 8) died for
infrastructure reasons. Otherwise a broken toolchain would feed the policy an all-zero reward
that reads as "the model got worse" (the 0/100 perfect-policy incident, mirrored).

Failure taxonomy (`StrictResult.reasons`): `no_code`, `parse_error`, `unsupported_statement`,
`header_mismatch`, `target_missing`, `target_modifier`, `statement_mismatch`, `banned:<x>`,
`compile_error`, `sorry`, `not_theorem`, `axiom:<name>`, `bridge_failed` (defeq mode),
`kernel_recheck_failed`, `meta_decl`, `skip_kernel_tc`, `timeout`, `crash`, `memory`,
`output_too_large`, `protocol`, `reference_invalid`.

Statement modes. `strict` (default): signature tokens equal the reference's *and* the kernel
bridge passes. `defeq`: bridge only, so an alpha-renamed or restated-but-defeq signature is
accepted. The bridge is checked by the kernel (not the elaborator's `isDefEq`), so "accepted"
means "the candidate's constant has a type the kernel accepts as the reference's type".

## 3. REPL pool: measured facts the design rests on

| Fact (measured on this box, Lean 4.9.0-rc1 + Mathlib pin) | Consequence in code |
|---|---|
| The REPL **does not flush stdout when it is a pipe** (only a pty line-buffers); the client blocks forever | binary wrapped in `stdbuf -o0 -e0` (`tests::test_wrapped_argv_*`) |
| `lake env` prints **cwd-relative** paths (`././.lake/...`) | absolutised once in `toolchain.build_child_env`; child gets a minimal environment (no tokens) |
| A **second fresh `import Mathlib` in a live process** cannot re-mmap the `.olean`s and reads them into the heap: `Pss_Anon` 0.25 → **4.14 GiB** | one import per process; every header variant is layered on it (`split_imports`, `Worker.header_env`); a different import set recycles the process |
| RSS is ~3.65 GiB but 3.40 GiB of it is the shared, read-only `.olean` mapping; private heap is ~0.25 GiB | the watchdog caps **`Pss_Anon`**, not RSS; `plan_workers` budgets shared-once + n × cap |
| `Nat.pow exponent is too big` and `lean::stack_space_exception` **abort the process** (`rc=1`, `rc=-6`) | `ReplCrash` with the stderr tail; the next `acquire` respawns; scored 0 |
| `debug.skipKernelTC` is an **unknown option** on 4.9.0-rc1 | the whitelist + the audit's `skip_tc` check are guards for a pin upgrade, not a live exploit here |
| Lean 4.9 has **no public `collectAxioms`** (`#print axioms` is private) | `alignCollectAxioms` in the prelude (same traversal) |
| `variable (h : False)` *used in a proof body* is added to the theorem's type in 4.9 (pre-4.11 behaviour) | `variable` is banned lexically and the bridge catches it independently |

Limits per worker (defaults, `ReplLimits`): anonymous-memory cap 4 GiB (watchdog polls
`smaps_rollup` every 0.5 s), `prlimit --as=16GiB --fsize=256MiB --core=0`, `nice +5`, output cap
16 MiB, per-proof wall budget 180 s (compile + audit), SIGKILL of the whole process group on any
violation, `MALLOC_ARENA_MAX=2`. Workers recycle after 1000 commands. Default worker count is
`min(8, cores // 4)` = 8 on this box; `ReplPool.from_toolchain(preflight=True)` applies the
`/apps/CLAUDE.md` checks (≥ 45 GB `MemAvailable`, load < 2× cores, disk queue < 64) through
`align.utils.guard.preflight_host_check` and then shrinks the worker count so that, even with
every worker at its cap, `MemAvailable` stays above the 45 GB floor. The ceiling-test script
takes the same exclusive heavy-job `flock` as the trainers (a Lean pool is a heavy job).

## 4. Pre-registration (written BEFORE the registered ceiling run)

Registered 2026-10-12 20:12 IST, before any run of `scripts/lean_ceiling_test.py` without
`--allow-unregistered`. The case tables (`HEADER`, `STATEMENTS`, `ADVERSARIAL`, `RESTATED` in
`scripts/lean_ceiling_cases.py`) are frozen by hash:

PREREG_TABLE_SHA256: `61636c7136eb647801c588b00a35bd8bcc6ef4db126c21ea75769092883ff7e2`

`python scripts/lean_ceiling_test.py --verify-prereg` recomputes it; the registered run refuses
to start on a mismatch.

**Honest note on process.** The tables were developed against this verifier (about six exploratory
runs on 1-2 workers, `--exploratory --allow-unregistered`) so that every adversarial case is
*live* (it would be paid by a compile-only grader, which the run also measures) and every
restated proof compiles. Those exploratory runs informed case *authoring* only; thresholds
below were fixed before the registered run, and any verifier change after registration that
alters a verdict is reported as an amendment in Section 7, with the pre-amendment matrix kept.

**Configuration under test** = shipped defaults: `StrictConfig()` (strict statement mode, no helper
defs, kernel recheck ON, 180 s budget per proof) and `ReplLimits()` (4 GiB anonymous cap, 16 GiB
`--as` backstop), 8 workers, `lean_workbook` compile-only filter budget 120 s.

**Known-good set.** (a) Public verified proofs: 6 contiguous slices of 25 rows at offsets
0, 5000, 10000, 15000, 20000, 25000 of `Goedel-LM/Lean-workbook-proofs` (29,750 proofs verified
under the Lean 4.9 / DeepSeek-Prover-V1.5 Mathlib environment), fetched over HTTP, never the
full dataset. (b) 30 hand-restated Mathlib-style theorems with proofs. The formal statement
given to the verifier is derived mechanically from each proof file (header + last theorem's
signature cut at the signature-ending `:=` + `sorry`). **Filter:** an independent compile-only
pass (same REPL, no strictness: no `error` message and no `sorry` warning) defines "compatible
with the pin"; items failing it are excluded *and listed*. Scored population = compile-clean
items. **Pass:** ≥ 98 % accepted and ≥ 50 items. (The roadmap's P1 row says ≥ 99 %; the task
fixed 98 %, which is what is registered. Both are reported.) Any rejected known-good item is
listed with its reasons; rejections caused by the policy bans (e.g. a genuine `native_decide`
in a public proof) count as failures, because that is the cost the RL stage would pay.

**Adversarial set.** ≥ 30 hand-built cases, all registered (61): categories sorry (7), admit (2),
extra axiom (4), native_decide (3), changed statement (6), weakened hypothesis (3), definition
hole (4), environment manipulation via elab/macro/run_cmd/initialize/#eval/#exit (9), theorem
proved for a different name (6), timeout/crash/memory bombs (3), banned constructs (6), header
tampering (4), no code (4). Each is a *completion* (prose + fenced block) so fence extraction,
static gate, REPL compile and audit are all exercised. **Pass:** 0 accepted. Bomb cases run on
a dedicated 1-worker pool (1 GiB anonymous cap, 30 s budget) so they terminate promptly; the
code path is the same. Reported alongside, not part of the pass criterion: how many cases a
compile-only grader would pay; and which cases the Lean-side audit rejects *on its own* with
the lexical gate disabled.

**Confusion matrix** (reported verbatim): rows = truth (good / bad), columns = verdict
(accept / reject); good = compile-clean known-good, bad = adversarial.

**Measurements** (reported, no threshold): install disk size; `import Mathlib` cold
(page cache evicted with `posix_fadvise(DONTNEED)`) and warm; per-REPL PSS / Pss_Anon / RSS from
`/proc/<pid>/smaps_rollup` with Mathlib imported, for one process and for 8 concurrent workers;
throughput (proofs/s) at the worker count used, with the worker set warm, on unique statements
and in the GRPO shape (12 statements × 8 samples); latency p50/p90/p99; PSS sampled during the
run.

## 5. Results

All numbers below are copied from `.tmp/lean_ceiling/registered/result.json` and
`.tmp/lean_ceiling/registered.log` (the registered run: hash verified, `prereg_registered=true`,
8 workers, host preflight passed, heavy-job lock taken after waiting for another track's job
to release it, started ~20:13 IST, 2026-10-12). Host: 48 cores; `MemAvailable` never fell below
136.9 GB in the samples taken during the strict known-good pass.

### 5.1 Ceiling test: confusion matrix (verbatim)

```
                      verdict ACCEPT   verdict REJECT   total
truth GOOD proofs                177                1     178
truth BAD  proofs                  0               61      61
```

| Criterion (pre-registered) | Measured | Verdict |
|---|---|---|
| known-good accepted >= 98 %, n >= 50 | **177 / 178 = 99.44 %** (Wilson 95 % [96.89 %, 99.90 %]); n = 178 | **PASS** (also >= the roadmap's 99 %) |
| adversarial accepted = 0, n >= 30 | **0 / 61** | **PASS** |
| gate 5 of the roadmap: strict false-negative rate > 5 % blocks P2 | 0.56 % (upper Wilson 3.1 %) | not blocked |

**Known-good population.** 180 candidates (150 Lean-Workbook-proofs rows from dataset revision
`b731852af8d8`, raw file sha256 `55932494...aebef`; 30 restated). The independent compile-only
filter kept 178: **148 + 30**. The 2 excluded (`lean_workbook_10036`, `lean_workbook_plus_64037`)
were excluded because the compile-only pass hit its **120 s budget** (`why: timeout`), not because
of a compile error, so "version-incompatible" would be the wrong label: they are slow proofs.
All 30 restated proofs were accepted.

**The one rejected known-good proof: `lean_workbook_51088`, reason `timeout`** (180 s strict
budget). It is a nlinarith proof with nine hints. Follow-up (exploratory, 1 worker, not part of
the registered numbers), same proof, same pool code, only the REPL environment layers differ:

| environment the body runs in | wall time |
|---|---|
| imports + header (what the compile-only filter uses) | 99.9 s (filter: 104.1 s) |
| + reference theorem `_align_ref` | 225.8 s |
| + audit prelude | 513.6 s |
| + both (= the strict path) | 516.5 s (timed out at 400 s in a separate strict run) |

On `lean_workbook_10009` (20 s) the same four environments take 19.9 / 20.8 / 20.1 / 20.0 s. So
this is not a systematic strict-tier slowdown; it is the well-known run-time chaos of a borderline
`nlinarith` (the certificate search is sensitive to state it inherits from earlier commands) hitting
a 180 s wall-clock budget. It is a genuine false negative of the *reward* (such a proof would score
0), it is bounded at one in 178 here, and raising the budget to Goedel's 300 s would not have saved
it (517 s). The honest reading: wall-clock-bounded verification of proofs this close to the limit is
flaky in BOTH directions, so for evaluation (not training) report results at a stated budget.

**Adversarial population (61 cases, all rejected).** First reason per case: 58 rejected by the
static gate (no Lean call), 3 by the REPL layer (the bombs). By category (cases / accepted):
sorry 7/0, admit 2/0, extra_axiom 4/0, native_decide 3/0, changed_statement 6/0,
weakened_hypothesis 3/0, definition_hole 4/0, env_manipulation 9/0, wrong_name 6/0,
timeout_bomb 3/0 (timeout, `rc=-6` stack-overflow crash, anonymous-memory kill at 1110 MiB over a
1024 MiB cap), banned_construct 6/0, header_tamper 4/0, no_code 4/0.

* **A compile-only grader would have paid 51** of the 54 code-bearing, non-bomb cases
  (each case's own text compiled with no strictness). The 3 it would not: `header_missing` (no
  Mathlib), `duplicate_target` (Lean itself rejects a duplicate name) and `skipKernelTC_option`
  (unknown option on this Lean). The 3 bombs and the 4 `no_code` cases are not applicable. So
  51 are live exploits, not strawmen.
* **Semantic layer alone** (lexical gate OFF, so each exploit actually executes inside Lean, with
  `kernel_recheck` on): rejected **45 of the 54** code-bearing non-bomb cases by the Lean-side audit
  alone (`sorry`, `axiom:bad`, `axiom:Lean.ofReduceBool`, `axiom:evil` after an `elab` added an
  axiom, `statement_mismatch` for the instance hole, the `variable` hole, shadowing via
  `macro_rules`, changed constants/hypotheses, `meta_decl` for `initialize`, `target_missing`
  for wrong names/namespaces/private targets, `header_mismatch`). The 9 it accepted are the
  *policy-only* set, where the proof of the stated theorem is valid and the lexical ban is the
  only thing stopping it: `eval_side_effect`, `exit_after_theorem`, `attribute_on_target`,
  `unsafe_implemented_by`, `partial_def`, `extern_attr`, `search_tactic_exact_q`,
  `maxRecDepth_abuse`, `helper_def_with_proof`. None is a soundness gap (the theorem is proved);
  they are rejected to keep the attack surface small, and each costs real-world proofs that use
  them (Section 6, item 5).

### 5.2 Install and process measurements

| Quantity | Measured |
|---|---|
| Install time (`lean_setup.sh`, cache hit) | 3 min 9 s (18:23:06 -> 18:26:15); an idempotent re-run takes ~10 s |
| Disk, total | 5,981,549,078 B = **5.57 GiB**: elan + toolchain 0.93 GiB, workspace 4.34 GiB (of which Mathlib package 4.11 GiB), REPL 94 MiB |
| `import Mathlib` + `Aesop`, **cold** (page cache for all 11,171 `.olean/.ilean/.so` files = 4.19 GiB evicted with `posix_fadvise(DONTNEED)`) | **39.7 s** |
| same, **warm** (3 runs) | 3.19, 3.20, 3.19 s (median **3.19 s**) |
| 8 workers warmed in parallel | 3.38 s (warm cache) |
| One REPL after the import (alone on the box) | RSS 3439 MiB, **PSS 3437 MiB**, `Pss_Anon` **244 MiB**, `Pss_File` 3193 MiB, 1 thread, VmSize 4071 MiB |
| **Per-worker PSS with 8 workers all holding Mathlib** | **643-653 MiB** each (mean 648; RSS 3.4-3.5 GiB each) = 244 MiB anonymous + 3193 MiB / 8 shared. Sum of PSS **5.06 GiB**; sum of RSS **26.8 GiB** (the double count the PSS figure removes) |
| Per-worker PSS **during** the 178-proof run (sampled every 0.5 s) | mean 717-792 MiB, max **873 MiB**; `Pss_Anon` max 404 MiB; RSS max 3.65 GiB |
| Worker growth over 1042 commands | none that approached any limit: recycled 0 times (limit 1000/worker) |

`fincore` was not usable for confirming the eviction (its output did not parse into a residency
fraction: the script printed `None` and `4096.0`; ignore those two fields in `result.json`). The 12x
cold/warm gap is the evidence that the eviction worked.

Model implied by the measurements: `PSS(n workers) ~ 0.24 GiB + 3.12 GiB / n` per worker, total
`~ 3.12 + 0.24 n` GiB. Eight workers cost ~5 GiB of RAM in total. The worst case (all eight at
their 4 GiB anonymous cap) is 3.12 + 8 x 4 = 35 GiB, which is what `plan_workers` budgets against
the 45 GB floor, so the pool cannot push `MemAvailable` under the floor on this host. **Roadmap gate 5** ("above ~4 GB per warm REPL the pool
shrinks below ~8 workers") is not triggered: 0.65 GiB per warm REPL, 0.85 GiB peak under load.

### 5.3 Throughput (8 workers, warm)

| Shape | Proofs | Wall | Proofs/s | Notes |
|---|---|---|---|---|
| 178 unique statements (the known-good set, strict tier incl. kernel recheck) | 178 | 310.9 s | **0.573** | latency p50 1.97 s, p90 21.4 s, p99 158.6 s, max 180.3 s (the timeout), mean 9.76 s |
| GRPO shape: 12 median-latency statements x 8 identical samples | 96 | 21.9 s | **4.38** | reference environment and baseline audit reused per worker; all 96 accepted |

The first number is dominated by the heavy tail of Lean-Workbook `nlinarith` proofs (mean 11.7 s,
median 2.65 s for those; the 30 restated proofs: median 0.18 s, max 1.5 s) and by one 180 s
straggler: the ideal packing of the work is 178 x 9.76 s / 8 = 217 s. It is **not** a verifier
overhead: the compile-only filter took 273 s for 180 candidates on the same 8 workers, i.e. the
strict tier added ~14 % (audit 0.03-0.7 s per proof; kernel recheck +0.04-0.12 s on 20-43 s
proofs). Against the research report's guess of "1-3 proofs/s for 8-16 workers (low confidence)":
**0.57/s on a heavy, unfiltered population and 4.4/s on a median population**; either way
verification stays far above generation (~0.1-0.17 proofs/s per the report), so it is not the
binding constraint at this worker count.

### 5.4 Test and mutation evidence

* `pytest tests/test_lean_verifier.py -m "not slow"`: 173 passed (no Lean needed; fake REPL
  binary speaks the real protocol, including the audit message).
* `pytest tests/test_lean_verifier.py -m slow`: 19 passed on the real toolchain (accepts a correct
  proof, rejects eight exploits with the lexical gate OFF, defeq-vs-strict mode, invalid reference,
  timeout + recovery, end-to-end reward path, concurrency, memory shape).
* `tests/test_dependency_firewall.py`: 2 passed (`align/verify/lean/` and the two scripts import
  only stdlib / `align`).

## 6. Residual risks (what this verifier does NOT promise)

1. **Lexer desync.** The lexical gate is a hand-written tokenizer, not Lean's. It follows Lean's
   rules for nested comments, string escapes, raw strings, char literals and `«»`, and on any
   unusual construct falls back to a raw-text scan, but it has not been differentially fuzzed
   against Lean's real tokenizer. Mitigation: layers 3+ (axioms, bridge, kernel recheck,
   meta-declaration scan) do not use it; the semantic-only column of the ceiling test shows
   what they catch alone. What only the lexer stops is listed there (all of it is
   *policy*, not soundness: the proofs are valid for the stated theorem).
2. **No OS sandbox.** The REPL runs as the same user with filesystem access and no
   network namespace. Code can only execute inside the elaborator through meta-programming
   (`elab`/`macro`/`run_cmd`/`#eval`/`initialize`/`unsafe`), all of which are banned lexically
   AND detected by the meta-declaration scan, and the child's environment holds no secrets
   (`toolchain.build_child_env`). A meta-programming escape that evades both would run with the
   verifier's file permissions. For adversarial RL, run the pool as a dedicated unprivileged
   user or under `unshare --net` / a cgroup; that needs privileges this task did not have.
3. **Statement fidelity.** The bridge proves the candidate proves the GIVEN formal
   statement. It says nothing about whether that statement means what the informal problem
   says (miniF2F: formal/informal discrepancies in over half the problems, 16 unprovable
   statements; the report, section 5F). Report miniF2F-v2/PutnamBench with the pin and tier.
4. **Trust base.** The Lean kernel, `propext`, `Quot.sound`, `Classical.choice`, the REPL, and
   the prelude (`AUDIT_PRELUDE`, ~100 lines). The prelude is a trusted program; it is
   exercised by the real-toolchain tests, not formally verified.
5. **Policy rejections are not soundness failures.** `def`/`instance`/`notation`/`namespace`
   helpers, `exact?`, `#eval` and so on are rejected even when the proof of the stated theorem
   is valid. That is deliberate (smaller attack surface) and has a cost: public proofs that use
   them are false negatives. `allow_helper_defs=True` relaxes only `def`/`abbrev`.
6. **Version drift.** Proofs from other Lean/Mathlib versions will fail to compile (counted as
   0), which is correct for this pin and wrong as a statement about the proof.
7. **Throughput numbers are for this population** (short Lean-Workbook-style and restated
   proofs). A prover that emits long `nlinarith`/`decide` proofs will be slower; the audit costs
   ~0.3 s for proofs whose dependency closure reaches real analysis and ~0.03 s for `omega`.

## 7. Amendments

**7.1 Changes between the exploratory runs and the registration (before 20:12 IST; not
amendments, listed so nothing is hidden).** Case-table edits made after seeing exploratory output:
`sorry_branch` (its `all_goals sorry` was dead code, so the proof was valid: moved the `sorry` into
a live branch); `sorry_after_char_quote` / `sorry_after_comment_opener_string` (did not compile;
rebuilt so the `sorry` really sits between two quote-bearing literals); `axiom_private`,
`axiom_reducible_attr` (did not compile; replaced by `axiom_polymorphic`, `axiom_via_simpa`);
`ofReduceBool_term` (`rfl` cannot discharge `Lean.reduceBool`; replaced by
`native_decide_in_helper_lemma`); `local_notation_hole` (a local `+` notation is *ambiguous* with
the builtin one, so it never elaborates; replaced by `variable_hole_hypothesis`);
`rs_default_arg` (did not compile). Verifier changes made before registration, each driven by a
measurement: single import per process (Section 3), the late-answer drain, `kernel_recheck`
default, the `open Lean`-root ban, `notation3`/`binder_predicate`/... bans, 180 s default budget,
`split_imports`/prelude-before-opens layering. The `skipKernelTC_option` case turned out to be
**inert on this pin** (the option does not exist) and was kept, labelled.

**7.2 Retraction.** A first measurement reported the kernel recheck as costing "2.2-3.2x"
(`lean_workbook_10009`: 21 s -> 68 s). That reading came from a run on a host where two other
Lean processes were competing; isolated measurements (`#align_audit` flags 0 vs 1 on the same
environment) show **+0.04 to +0.12 s** against 20-43 s of elaboration. The recheck is therefore ON
by default; the claim was removed from the code comment before registration. (The same episode is
why the headline timings here come from the registered run; the few ad-hoc timings are labelled
exploratory.)

**7.3 Verifier changes after registration:** none. The registered run used the tree that is
described here.

**7.4 Failures found by running against the real toolchain (each has a regression test).**
(1) REPL never flushes a pipe -> `stdbuf`; (2) `lake env` paths are cwd-relative -> absolutised;
(3) a second `import Mathlib` in a live REPL costs +3.9 GiB anonymous and tripped the 4 GiB
watchdog on the first multi-case run -> one import per process, layered headers; (4)
`#print axioms`'s collector is private in 4.9 -> own traversal; (5) with `autoImplicit` on (plain
Lean default) an *unknown identifier* in a statement is silently an implicit variable, so "invalid
reference statement" needs a genuine type error to trigger `reference_invalid`; (6) `sqrt` under
`open Real Nat` is already ambiguous, so a root-level `def sqrt` is not a viable definition hole
here (instances and `variable` are); (7) pool statistics were counted only when an error escaped
`acquire`, not when the caller handled it.

**7.5 Hand mutants** (each removes one fix in a COPY of the package, then runs the non-slow
tests; `.tmp/lean_notes/mutants.py`, final run after the last code change except docs):

| | Mutant (fix removed) | Killed by |
|---|---|---|
| M01 | accept any audit-tagged message (forgeable verdict) | `test_audit_message_without_the_nonce_is_ignored_forged_verdict_regression` |
| M02 | no axiom whitelist check | `test_each_audit_finding_rejects[axiom/sorryax/ofreduce]` |
| M03 | ignore the kernel statement bridge | `test_each_audit_finding_rejects[bridge]` |
| M04 | no meta-baseline subtraction | `test_verdict_accepts_a_clean_audit` |
| M05 | no meta-declaration check | `test_each_audit_finding_rejects[meta]` |
| M06 | ignore `sorry` warnings / `sorries` | `test_each_audit_finding_rejects[sorrywarn/sorrylist]` |
| M07 | header failure scored 0 instead of `LeanInfraError` | `test_header_failure_raises_infra_error_never_a_zero` |
| M08 | candidate runs without the reference env | `test_command_order_and_envs_are_the_contract` |
| M09 | no `stdbuf` | `test_wrapped_argv_uses_stdbuf_prlimit_and_nice` |
| M10 | kill the pid, not the process group | `test_timeout_kills_grandchildren_too_via_process_group` |
| M11 | no memory watchdog | `test_memory_watchdog_caps_anonymous_memory_not_rss` |
| M12 | timeout without killing | `test_timeout_kills_the_process_and_leaves_no_zombie` |
| M13 | tolerate bytes after the terminator | **survives: equivalent mutant** (two concatenated JSON objects fail `json.loads` anyway); the real defence is M39 |
| M14 | first `:=` wins | `test_sig_end_skips_let_in_type_not_the_first_colon_eq` |
| M15 | no char-literal handling | `test_lexer_char_literal_quote_is_not_a_string_start` |
| M16 | block comments do not nest | `test_lexer_drops_line_and_nested_block_comments` |
| M17 | no raw-scan fallback on lexer anomalies | `test_anomaly_triggers_raw_scan_...` |
| M18 | re-import for every header variant | `test_second_header_variant_reuses_the_import_never_imports_twice_regression` |
| M19 | lake paths left cwd-relative | `test_build_child_env_absolutises_...` |
| M20 | no header equality | `test_static_rejections[header...]` |
| M21 | `set_option` whitelist off | `test_banned_constructs_fire[set_option...]` |
| M22 | dead pool emits zeros | `test_reward_raises_when_the_pool_looks_dead_...` |
| M23 | defs accepted as proofs | `test_each_audit_finding_rejects[def]` |
| M24 | kernel recheck never requested | `test_kernel_recheck_is_on_by_default_...` |
| M25 | `skip_tc` ignored | `test_each_audit_finding_rejects[skiptc]` |
| M26 | recheck failure ignored | `test_each_audit_finding_rejects[recheck]` |
| M27 | private/attribute target allowed | `test_static_rejections[target_modifier]` |
| M28 | signature token equality off | `test_static_rejections[statement_mismatch]` |
| M29 | identifier ban list off | `test_banned_constructs_fire` |
| M30 | salvage unclosed fences | `test_extract_unclosed_fence_is_not_salvaged` |
| M31 | first fence instead of last | `test_extract_takes_last_closed_lean_fence_and_ignores_think` |
| M32 | duplicate target allowed | `test_static_target_declared_twice_is_rejected` |
| M33 | ablated verifier usable as a reward | `test_reward_refuses_the_ablated_verifier` |
| M34 | worker never returned to the pool | **hang-killed** (the pool tests block; 90 s watchdog) |
| M35 | no recycling after `max_requests` | `test_worker_recycled_after_max_requests` |
| M36 | unbounded glibc arenas | `test_build_child_env_absolutises_...` |
| M37 | child inherits the whole `lake env` | `test_build_child_env_absolutises_...` (secrets leak assertion) |
| M38 | no output cap | `test_oversized_output_kills_the_process` |
| M39 | late second answer attributed to the next request | `test_late_second_answer_is_detected_...` |
| M40 | `open Lean` makes meta identifiers spellable | `test_banned_constructs_fire[open Lean...]` |

40 mutants: **38 killed by an assertion, 1 killed by a hang (M34), 1 survivor (M13, equivalent)**.
Not covered by any mutant, by construction: the `skip_tc` audit path against the REAL option (it
does not exist on this pin; `test_real_skip_kernel_tc_option_does_not_exist_on_the_pin` fails the
day the pin changes) and the Lean prelude's kernel-recheck loop against a proof that elaborated
with the kernel disabled (no way to produce one on 4.9 without the banned meta-programming).

**7.2 V1 freeze (2026-10-14, branch `phase0/verifier`; PROPOSED FROZEN HASH, merge after chain6 exits).**
Fixes (regressions in `tests/test_lean_hardening.py`, each adversarial proof was accepted or aborted a batch before):
U08-1/2 lexer desync (`\/` and `//` are single tokens; a comment opener glued to any other symbol is refused as
ambiguous; numerals no longer swallow letters: `1macro`; raw strings of any hash count; real-Lean differential fuzz);
U08-40 `s!`/`f!`/`m!` interpolation flagged whatever sits before the quote; U08-4/12 `_align*` names and
`_root_.`/`«»` spellings banned; U08-5 axiom-walk fuel exhaustion reports `_align_fuel_exhausted` (fails closed) and a
missing `axioms` field is `audit_incomplete`; U08-3 `ReplPool.from_toolchain(sandbox=True)` is the default, fails closed
with `SandboxUnavailable`, `sandbox=False` is the explicit opt-out; U08-8 every real REPL runs under the launcher/supervisor
(`ReplProcess.supervise`) that SIGKILLs it when the parent process dies (measured: orphaned before, reaped now);
U08-6 stale env ids after a worker death raise `ReplCrash`; U08-7 lone surrogates reject that item only; U08-17 budget
starts after `acquire()`; U08-18 a bad reference header is `reference_invalid`, not a batch abort; U08-21 any exception
mid-request kills the child; U08-11(codex) unmeasurable memory fails closed after `mem_miss_limit` samples; U08-16
timeout/crash/memory/protocol map to `Outcome.UNDECIDED` (new enum member in `align/eval/verifier.py`, reward stays 0).
Ported from the red-team session: opt-in `independent_recheck` (`recheck.py`), default off.
NOT changed (design choices, math verifier, need a decision): U08-9/10/11/15 (tolerance, texty fallback, unit stripping,
`i`), U08-13/14/22 (extractor ReDoS/format), U08-19/20 (preflight opt-in, pin not enforced at runtime).

Verification: exploratory ceiling run (2 workers, sandboxed): known-good 176/177 accepted (the one reject is a
179 s timeout under host load), adversarial 0/61 accepted. Registered run to do after the chains exit (needs the heavy
lock): `python scripts/lean_ceiling_test.py --workers 8`.
Frozen hash: `sha256` over, for each `align/verify/**/*.py` in sorted path order, `path + NUL + bytes + NUL`:
`6df56348db12d8e255427764f994d0f3ab29d4713e84ddda07c9637c31f12648`.

**7.3 FROZEN (2026-10-19).** Merged verifier (V1 + MV math-checker fixes) hash
`f1dd9e7871ade3a9dd37587c10a7a17025fe8623a735455ec40206607e120b5c` (`scripts/p2/verifier_hash.py`; supersedes the
7.2 proposal, which predates MV). Registered ceiling run (8 workers, heavy lock, sandboxed, `.tmp/restart/ceiling/result.json`):
known-good 178/179 accepted = 0.9944 (Wilson95 [0.9690, 0.9990], floor 0.98) PASS; adversarial 0/61 accepted PASS
(a compile-only grader would accept 51). Every SOTA number from here on is verified under this hash; chain8 refuses to
run a step if `align/verify` changes.

## 8. Operations

```bash
bash scripts/lean_setup.sh            # install / repair (idempotent, ~3 min, 5.8 GB, under /apps)
bash scripts/lean_setup.sh --check    # exit 0 iff the install is complete
source /apps/development/rlhf/lean/env.sh   # only if you want elan/lake in your shell

pytest tests/test_lean_verifier.py -m "not slow"   # no Lean needed (fake REPL binary)
pytest tests/test_lean_verifier.py -m slow         # real toolchain, 2 workers (~1 min)

python scripts/lean_ceiling_test.py --verify-prereg
python scripts/lean_ceiling_test.py --workers 8    # registered run: preflight + heavy-job lock
python scripts/lean_ceiling_test.py --measure-only # install size, cold/warm import, PSS
```

* `ALIGN_LEAN_ROOT` relocates the install. Every cache the toolchain writes is redirected
  under it by `lean_setup.sh`; nothing goes to `$HOME`, `/` or `/tmp`.
* Worker stderr spools to `<root>/tmp/pool/worker<i>.stderr` (truncated per respawn; the tail is
  quoted in `ReplCrash`).
* Before widening the pool: `plan_workers` and `ReplPool.from_toolchain(preflight=True)` are the
  only sanctioned way; never construct more workers than the 45 GB floor allows.
* A new Lean/Mathlib pin is a new verifier: re-run the ceiling test, update `PINS.json`
  provenance in Section 1, and regenerate any cached verdicts.
* Hand mutants (Section 7) are run on a COPY of the package under `.tmp/mutants/`; the repo is
  never edited in place.