Add textbook formalization pipeline (extract -> LLM formalize -> elaboration check -> library assembly)

- extract_section.py: slice a book section out of a pdftotext -layout dump and
  list its labeled Theorem/Proposition/Lemma/Corollary/Definition items.
- formalize.py: LLM-formalize a section into Lean 4 candidate statements
  (preferring Mathlib vocabulary) plus a definitions manifest.
- check_statements.py: elaboration-only sanity check against a real
  Mathlib-backed Lean project (must run outside the Cowork sandbox, which has
  no Lean toolchain and can't run macOS binaries anyway).
- build_library.py: assemble solved lean-bench results into a reusable .lean
  file, reusing each winning proof verbatim.
- llm.py: standalone OpenAI-compatible chat client + .env loader, kept
  independent of lean_agent so this pipeline works without it installed.
- Source PDFs and raw/layout text dumps are gitignored (copyrighted material);
  only code and the final short Lean statement datasets are tracked.
This commit is contained in:
2026-09-17 17:04:19 +00:00
parent 99638be637
commit caedf2e6f4
13 changed files with 871 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
import json
import tempfile
import unittest
from pathlib import Path
from textbook.build_library import build
def write_jsonl(path, records):
path.write_text('\n'.join(json.dumps(r) for r in records) + '\n', encoding='utf-8')
class BuildLibraryTests(unittest.TestCase):
def test_build_includes_only_solved_theorems_and_hoists_shared_imports(self):
with tempfile.TemporaryDirectory() as directory:
directory = Path(directory)
dataset = directory / 'dataset.jsonl'
results = directory / 'results.jsonl'
write_jsonl(dataset, [
{"id": "solved-one", "statement": "True", "prelude": "import Mathlib",
"source_label": "Theorem 1", "natural_language": "trivial"},
{"id": "unsolved-one", "statement": "False", "prelude": "import Mathlib",
"source_label": "Theorem 2", "natural_language": "not actually true"},
])
write_jsonl(results, [
{"id": "solved-one", "success": True,
"attempts": [{"proof": "trivial", "success": True, "diagnostics": ""}]},
{"id": "unsolved-one", "success": False, "attempts": []},
])
text, solved, skipped = build(dataset, results)
self.assertEqual(solved, ['solved-one'])
self.assertEqual(skipped, ['unsolved-one'])
self.assertIn('import Mathlib', text)
self.assertEqual(text.count('import Mathlib'), 1) # hoisted once, not per-theorem
self.assertIn('theorem solved_one : True := by', text)
self.assertNotIn('unsolved_one', text)
def test_build_keeps_non_import_prelude_attached_per_theorem(self):
with tempfile.TemporaryDirectory() as directory:
directory = Path(directory)
dataset = directory / 'dataset.jsonl'
results = directory / 'results.jsonl'
write_jsonl(dataset, [
{"id": "custom-def", "statement": "myPred 1", "prelude": "def myPred (n : Nat) := True",
"source_label": "Theorem 3", "natural_language": ""},
])
write_jsonl(results, [
{"id": "custom-def", "success": True,
"attempts": [{"proof": "trivial", "success": True, "diagnostics": ""}]},
])
text, solved, skipped = build(dataset, results)
self.assertIn('def myPred (n : Nat) := True', text)
self.assertEqual(solved, ['custom-def'])
if __name__ == '__main__':
unittest.main()
+49
View File
@@ -0,0 +1,49 @@
import unittest
from textbook.extract_section import extract_section, labeled_statements
FIXTURE = """\
1.6. Divisibility in the Integers
intro text
Proposition 1.6.1.
body
Theorem 1.6.6. There are infinitely many prime numbers.
Proof. blah
36 1. ALGEBRAIC THEMES
Theorem 1.6.6. There are infinitely many prime numbers.
Proof continues after a page break, repeats the label.
1.7. Modular Arithmetic
next section text
"""
class ExtractSectionTests(unittest.TestCase):
def test_extract_section_slices_between_headings(self):
section = extract_section(FIXTURE, '1.6. Divisibility in the Integers', '1.7. Modular Arithmetic')
self.assertIn('Theorem 1.6.6', section)
self.assertNotIn('next section text', section)
def test_extract_section_missing_heading_raises(self):
with self.assertRaises(ValueError):
extract_section(FIXTURE, '1.6. Divisibility in the Integers', '9.9. Does Not Exist')
def test_labeled_statements_dedupes_and_orders(self):
section = extract_section(FIXTURE, '1.6. Divisibility in the Integers', '1.7. Modular Arithmetic')
items = labeled_statements(section, '1.6.')
labels = [item['label'] for item in items]
# Theorem 1.6.6 appears twice (page-break repeat) but must be kept once, and
# book order (1.6.1 before 1.6.6) must be preserved.
self.assertEqual(labels, ['1.6.1', '1.6.6'])
def test_labeled_statements_filters_by_prefix(self):
section = extract_section(FIXTURE, '1.6. Divisibility in the Integers', '1.7. Modular Arithmetic')
items = labeled_statements(section, '1.7.')
self.assertEqual(items, [])
if __name__ == '__main__':
unittest.main()
+58
View File
@@ -0,0 +1,58 @@
import json
import unittest
from textbook.formalize import extract_json_block, validate
VALID_REPLY = """Here is the result:
```json
{
"definitions": [
{"label": "Definition 1.6.3", "concept": "prime number", "mathlib_ref": "Nat.Prime",
"lean_code": null, "notes": ""}
],
"theorems": [
{"id": "goodman-1-6-6", "source_label": "Theorem 1.6.6", "kind": "theorem",
"statement": "\\u2200 n : \\u2115, \\u2203 p, p > n \\u2227 p.Prime",
"prelude": "import Mathlib", "natural_language": "There are infinitely many primes.",
"requires": ["Nat.Prime"], "include_in_bench": true, "exclude_reason": null}
]
}
```
"""
class FormalizeParsingTests(unittest.TestCase):
def test_extract_json_block_from_fenced_reply(self):
data = extract_json_block(VALID_REPLY)
self.assertEqual(set(data), {"definitions", "theorems"})
def test_extract_json_block_without_fence_falls_back_to_whole_text(self):
payload = '{"theorems": [], "definitions": []}'
self.assertEqual(extract_json_block(payload), json.loads(payload))
def test_validate_accepts_well_formed_response(self):
data = extract_json_block(VALID_REPLY)
theorems, definitions = validate(data)
self.assertEqual(len(theorems), 1)
self.assertEqual(len(definitions), 1)
def test_validate_rejects_missing_field(self):
data = {"theorems": [{"id": "x", "source_label": "Theorem 1", "statement": "True"}],
"definitions": []}
with self.assertRaises(ValueError):
validate(data)
def test_validate_rejects_duplicate_ids(self):
entry = {"id": "dup", "source_label": "Theorem 1", "kind": "theorem", "statement": "True",
"prelude": "import Mathlib", "include_in_bench": True}
data = {"theorems": [entry, dict(entry)], "definitions": []}
with self.assertRaises(ValueError):
validate(data)
def test_validate_rejects_non_object_response(self):
with self.assertRaises(ValueError):
validate([])
if __name__ == '__main__':
unittest.main()