leanscreen 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- leanscreen/__init__.py +16 -0
- leanscreen/backtranslate.py +205 -0
- leanscreen/config.py +69 -0
- leanscreen/errors.py +47 -0
- leanscreen/falsify.py +116 -0
- leanscreen/lean_server.py +252 -0
- leanscreen/lints.py +445 -0
- leanscreen/llm.py +457 -0
- leanscreen/logging.py +78 -0
- leanscreen/py.typed +0 -0
- leanscreen/quality.py +524 -0
- leanscreen/score.py +282 -0
- leanscreen/server.py +421 -0
- leanscreen/verify.py +338 -0
- leanscreen-0.1.0.dist-info/METADATA +255 -0
- leanscreen-0.1.0.dist-info/RECORD +19 -0
- leanscreen-0.1.0.dist-info/WHEEL +4 -0
- leanscreen-0.1.0.dist-info/entry_points.txt +2 -0
- leanscreen-0.1.0.dist-info/licenses/LICENSE +105 -0
leanscreen/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""leanscreen — a calibrated faithfulness screen for informal↔Lean 4 pairs.
|
|
2
|
+
|
|
3
|
+
The screen may only reject; it never certifies. See
|
|
4
|
+
:data:`leanscreen.score.CALIBRATION_DISCLOSURE` for exactly what a verdict
|
|
5
|
+
is worth.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from leanscreen.score import (
|
|
9
|
+
CALIBRATION_DISCLOSURE,
|
|
10
|
+
ExternalPair,
|
|
11
|
+
PairScore,
|
|
12
|
+
score_pair,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = ["CALIBRATION_DISCLOSURE", "ExternalPair", "PairScore", "score_pair"]
|
|
16
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Step 7: back-translation review aid for faithfulness checking.
|
|
2
|
+
|
|
3
|
+
A draft that *typechecks* and *passes the vacuity filter* is only "clean", not
|
|
4
|
+
necessarily faithful: it may silently drop a hypothesis, weaken the conclusion,
|
|
5
|
+
or add an assumption. The cheapest way to surface that for a human reviewer is to
|
|
6
|
+
translate the Lean statement BACK to plain English (blind to the original) and
|
|
7
|
+
put the two side by side — mismatches jump out.
|
|
8
|
+
|
|
9
|
+
We make ONE model call per draft that returns a literal back-translation plus a
|
|
10
|
+
flagged verdict. This is an AID, never a gate: the human still decides. It exists
|
|
11
|
+
to make the review bottleneck (the real moat) fast, and to validate that our
|
|
12
|
+
"clean" proxy actually tracks faithfulness.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Protocol
|
|
20
|
+
|
|
21
|
+
from leanscreen.llm import LLMClient, UsageTotals
|
|
22
|
+
|
|
23
|
+
_SYSTEM_PROMPT = (
|
|
24
|
+
"You are a meticulous Lean 4 / mathlib proof engineer doing FAITHFULNESS "
|
|
25
|
+
"review. You are given an informal mathematical statement and a Lean 4 "
|
|
26
|
+
"formalization of it (proof omitted). Do two things:\n"
|
|
27
|
+
"1. Translate the LEAN statement back into plain English LITERALLY — say "
|
|
28
|
+
"exactly what the Lean says, including every hypothesis and the precise "
|
|
29
|
+
"conclusion. Do NOT consult the informal version while doing this; describe "
|
|
30
|
+
"the Lean on its own terms.\n"
|
|
31
|
+
"2. Compare your back-translation to the informal statement and decide whether "
|
|
32
|
+
"the Lean faithfully captures it. Watch for: dropped or weakened hypotheses, a "
|
|
33
|
+
"weakened or trivialized conclusion, extra assumptions the informal didn't "
|
|
34
|
+
"state, free parameters standing in for specific defined objects, and "
|
|
35
|
+
"quantifiers that changed scope.\n"
|
|
36
|
+
"Respond in EXACTLY this format:\n"
|
|
37
|
+
"BACK-TRANSLATION: <your literal English rendering of the Lean>\n"
|
|
38
|
+
"VERDICT: FAITHFUL | MISMATCH | UNCLEAR\n"
|
|
39
|
+
"ISSUES: <none, or a terse list of the specific discrepancies>"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# DEFINITION variant: same blind two-step and the SAME output format, but the
|
|
43
|
+
# comparison rules are definition-shaped (defining conditions, type/arity,
|
|
44
|
+
# degenerate bodies) instead of hypothesis/conclusion-shaped. Calibrate on the
|
|
45
|
+
# human def verdicts (scripts/eval_judge.py --definitions) BEFORE wiring it as
|
|
46
|
+
# a gate anywhere — the aid contract (reject-only, human decides) is unchanged.
|
|
47
|
+
_SYSTEM_PROMPT_DEFINITION = (
|
|
48
|
+
"You are a meticulous Lean 4 / mathlib proof engineer doing FAITHFULNESS "
|
|
49
|
+
"review of a DEFINITION. You are given an informal mathematical definition "
|
|
50
|
+
"and a Lean 4 `def`/`abbrev`/`structure` intended to formalize it. Do two "
|
|
51
|
+
"things:\n"
|
|
52
|
+
"1. Translate the LEAN definition back into plain English LITERALLY — say "
|
|
53
|
+
"exactly what object or predicate it defines, over what arguments and "
|
|
54
|
+
"types, and what its body requires. Do NOT consult the informal version "
|
|
55
|
+
"while doing this; describe the Lean on its own terms.\n"
|
|
56
|
+
"2. Compare your back-translation to the informal definition and decide "
|
|
57
|
+
"whether the Lean defines the SAME concept. Watch for: a defining "
|
|
58
|
+
"condition from the informal that is missing, weakened, or replaced; a "
|
|
59
|
+
"substantive condition the informal never stated; the wrong type, arity, "
|
|
60
|
+
"or domain (a property of one object where the informal defines a "
|
|
61
|
+
"relation, a global constant where the informal defines a family); a "
|
|
62
|
+
"degenerate body (a literal True/False leaf standing in for a condition, "
|
|
63
|
+
"a constant ignoring its arguments); and paper-specific components left "
|
|
64
|
+
"as free unconstrained parameters instead of being defined.\n"
|
|
65
|
+
"Do NOT flag faithful modeling choices: Finset vs Set, a structure vs a "
|
|
66
|
+
"predicate, curried vs tupled arguments, meaning-preserving "
|
|
67
|
+
"generalizations, inlined notation, or an equivalent re-encoding of the "
|
|
68
|
+
"same condition.\n"
|
|
69
|
+
"Respond in EXACTLY this format:\n"
|
|
70
|
+
"BACK-TRANSLATION: <your literal English rendering of the Lean definition>\n"
|
|
71
|
+
"VERDICT: FAITHFUL | MISMATCH | UNCLEAR\n"
|
|
72
|
+
"ISSUES: <none, or a terse list of the specific discrepancies>"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# v2, gated behind ARXIV_FORMALIZE_SELFCHECK_CHECKLIST ('+sc2' cohorts): the
|
|
76
|
+
# docs/faithfulness-patterns.md triage run clause-by-clause. Same blind
|
|
77
|
+
# back-translation step and the SAME output format (everything downstream
|
|
78
|
+
# parses it); only the comparison instructions change. Adopt only if the
|
|
79
|
+
# judge-agreement eval beats the v1 baseline without raising false rejection.
|
|
80
|
+
_SYSTEM_PROMPT_CHECKLIST = (
|
|
81
|
+
"You are a meticulous Lean 4 / mathlib proof engineer doing FAITHFULNESS "
|
|
82
|
+
"review. You are given an informal mathematical statement and a Lean 4 "
|
|
83
|
+
"formalization of it (proof omitted). Do two things:\n"
|
|
84
|
+
"1. Translate the LEAN statement back into plain English LITERALLY — say "
|
|
85
|
+
"exactly what the Lean says, including every hypothesis and the precise "
|
|
86
|
+
"conclusion. Do NOT consult the informal version while doing this; describe "
|
|
87
|
+
"the Lean on its own terms.\n"
|
|
88
|
+
"2. Compare against the informal statement by auditing EACH hypothesis and "
|
|
89
|
+
"EACH conclusion conjunct independently — most clauses being right says "
|
|
90
|
+
"nothing about the rest. Checks:\n"
|
|
91
|
+
"a. Does any hypothesis restate, contain, or one-step-yield the conclusion? "
|
|
92
|
+
"Assuming a premise the informal itself states is fine; assuming the "
|
|
93
|
+
"consequence the lemma exists to derive is a MISMATCH.\n"
|
|
94
|
+
"b. Is any conclusion conjunct true for every object of its type? Watch junk "
|
|
95
|
+
"values: Real.log 0 = 0, division by zero = 0, an ENNReal constant satisfied "
|
|
96
|
+
"by infinity, nat subtraction truncating, nat division flooring.\n"
|
|
97
|
+
"c. Did an informal decoration or index vanish — a torus/quotient norm as "
|
|
98
|
+
"bare abs, a one-sided derivative as deriv, a superscript-n sequence as an "
|
|
99
|
+
"n-free object, a ceiling as nat division? Judge operators by actual mathlib "
|
|
100
|
+
"semantics on that exact type.\n"
|
|
101
|
+
"d. Does the type lack structure the paper's regime uses (ZMod q is a field "
|
|
102
|
+
"only for prime q; commutative renderings of noncommutative claims)?\n"
|
|
103
|
+
"e. Is every stated hypothesis and constraint set present? Dropped "
|
|
104
|
+
"regularity/integrability/continuity or quantifying over everything where "
|
|
105
|
+
"the informal restricts to a constrained set usually makes it false.\n"
|
|
106
|
+
"f. Is a named subject declared but never used (dead parameter)?\n"
|
|
107
|
+
"g. Must a named specific ('all voters', 'the identity') be a concrete term "
|
|
108
|
+
"(univ, 1) rather than a fresh variable? Does every refinement ('depending "
|
|
109
|
+
"only on', 'in particular', multiplicity, 'is exactly') map to a Lean "
|
|
110
|
+
"clause? Any extra Lean conjunct not forced by the informal's wording?\n"
|
|
111
|
+
"h. Is every parameter pinned by a characterizing hypothesis or genuinely "
|
|
112
|
+
"ambient? Mentally instantiate free parameters with something trivial "
|
|
113
|
+
"(empty set, zero function, infinity): if the statement collapses or "
|
|
114
|
+
"falsifies, it is a MISMATCH.\n"
|
|
115
|
+
"NEVER flag these (they are faithful): pinned parameters (a defining "
|
|
116
|
+
"equation or iff); hypothesis strengthening unless the informal explicitly "
|
|
117
|
+
"names the weaker class; added implicit regularity (measurability, "
|
|
118
|
+
"positivity); provably equivalent re-encodings (preimage for image, cleared "
|
|
119
|
+
"denominators, sup-bound as forall); meaning-preserving generalizations; "
|
|
120
|
+
"inlined definitions; consistent reindexing; a faithful render of a claim "
|
|
121
|
+
"that is itself false (faithfulness is not truth).\n"
|
|
122
|
+
"Verdict: a demonstrated defect is MISMATCH; a load-bearing object you "
|
|
123
|
+
"cannot verify from what is shown (truncated informal, off-screen "
|
|
124
|
+
"definition, a convention whose two readings differ) is UNCLEAR even when "
|
|
125
|
+
"everything else is clean; all clauses passing including the never-flag "
|
|
126
|
+
"guards is FAITHFUL.\n"
|
|
127
|
+
"Respond in EXACTLY this format:\n"
|
|
128
|
+
"BACK-TRANSLATION: <your literal English rendering of the Lean>\n"
|
|
129
|
+
"VERDICT: FAITHFUL | MISMATCH | UNCLEAR\n"
|
|
130
|
+
"ISSUES: <none, or a terse list of the specific discrepancies>"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
_BACK_RE = re.compile(
|
|
134
|
+
r"BACK-TRANSLATION:\s*(?P<v>.+?)(?=\n\s*VERDICT:|\Z)", re.DOTALL | re.IGNORECASE
|
|
135
|
+
)
|
|
136
|
+
_VERDICT_RE = re.compile(r"VERDICT:\s*(?P<v>FAITHFUL|MISMATCH|UNCLEAR)", re.IGNORECASE)
|
|
137
|
+
_ISSUES_RE = re.compile(r"ISSUES:\s*(?P<v>.+?)\Z", re.DOTALL | re.IGNORECASE)
|
|
138
|
+
|
|
139
|
+
_VALID_VERDICTS = {"faithful", "mismatch", "unclear"}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True, slots=True)
|
|
143
|
+
class BackReview:
|
|
144
|
+
"""A back-translation review of one draft (an aid, not a verdict of record)."""
|
|
145
|
+
|
|
146
|
+
back_english: str
|
|
147
|
+
verdict: str # "faithful" | "mismatch" | "unclear"
|
|
148
|
+
issues: str
|
|
149
|
+
raw: str
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class ReviewAid(Protocol):
|
|
153
|
+
"""A faithfulness-review aid, so the pre-screen can inject a fake in tests."""
|
|
154
|
+
|
|
155
|
+
def review(self, informal: str, lean_code: str) -> BackReview:
|
|
156
|
+
"""Return a back-translation review for one informal/Lean pair."""
|
|
157
|
+
...
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def build_user_prompt(informal: str, lean_code: str) -> str:
|
|
161
|
+
"""Compose the review prompt for one informal/Lean pair."""
|
|
162
|
+
return f"Informal statement:\n{informal.strip()}\n\nLean formalization:\n{lean_code.strip()}"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def parse_review(response: str) -> BackReview:
|
|
166
|
+
"""Parse the structured response; degrade gracefully if the format slips."""
|
|
167
|
+
back = _BACK_RE.search(response)
|
|
168
|
+
verdict = _VERDICT_RE.search(response)
|
|
169
|
+
issues = _ISSUES_RE.search(response)
|
|
170
|
+
v = verdict.group("v").strip().lower() if verdict else "unclear"
|
|
171
|
+
if v not in _VALID_VERDICTS:
|
|
172
|
+
v = "unclear"
|
|
173
|
+
return BackReview(
|
|
174
|
+
back_english=back.group("v").strip() if back else response.strip(),
|
|
175
|
+
verdict=v,
|
|
176
|
+
issues=issues.group("v").strip() if issues else "",
|
|
177
|
+
raw=response,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class BackTranslator:
|
|
182
|
+
"""Produces a back-translation faithfulness aid via an injected LLM client.
|
|
183
|
+
|
|
184
|
+
``checklist=True`` selects the clause-by-clause v2 prompt (identical output
|
|
185
|
+
format, '+sc2' cohorts); the default stays the calibrated v1 prompt.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def __init__(
|
|
189
|
+
self, client: LLMClient, *, checklist: bool = False, definition: bool = False
|
|
190
|
+
) -> None:
|
|
191
|
+
self._client = client
|
|
192
|
+
if definition:
|
|
193
|
+
self._system = _SYSTEM_PROMPT_DEFINITION
|
|
194
|
+
else:
|
|
195
|
+
self._system = _SYSTEM_PROMPT_CHECKLIST if checklist else _SYSTEM_PROMPT
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def usage(self) -> UsageTotals:
|
|
199
|
+
"""Tokens spent by the aid's client (zero for test fakes)."""
|
|
200
|
+
return getattr(self._client, "usage", UsageTotals())
|
|
201
|
+
|
|
202
|
+
def review(self, informal: str, lean_code: str) -> BackReview:
|
|
203
|
+
"""Return a back-translation review for one informal/Lean pair."""
|
|
204
|
+
response = self._client.complete(self._system, build_user_prompt(informal, lean_code))
|
|
205
|
+
return parse_review(response)
|
leanscreen/config.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Typed settings, populated from ``LEANSCREEN_``-prefixed env vars (or `.env`).
|
|
2
|
+
|
|
3
|
+
Only what the screen needs: where Lean lives, which models judge, and the
|
|
4
|
+
HTTP budget. No database, no storage, no pipeline knobs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from pydantic import Field
|
|
12
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Settings(BaseSettings):
|
|
16
|
+
"""Application settings (env prefix ``LEANSCREEN_``)."""
|
|
17
|
+
|
|
18
|
+
model_config = SettingsConfigDict(
|
|
19
|
+
env_prefix="LEANSCREEN_",
|
|
20
|
+
env_file=".env",
|
|
21
|
+
env_file_encoding="utf-8",
|
|
22
|
+
extra="ignore",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# --- Lean toolchain -----------------------------------------------------
|
|
26
|
+
lean_project_path: Path | None = Field(
|
|
27
|
+
default=None,
|
|
28
|
+
description="A Lean 4 project with mathlib as a dependency (built with `lake build`).",
|
|
29
|
+
)
|
|
30
|
+
lean_repl_path: Path | None = Field(
|
|
31
|
+
default=None,
|
|
32
|
+
description=(
|
|
33
|
+
"Path to the community REPL binary (repl/.lake/build/bin/repl). "
|
|
34
|
+
"With it, mathlib loads once (~100s) and each check takes ~0.1s; "
|
|
35
|
+
"without it, every check pays a full `lake env lean` invocation."
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
lean_timeout_seconds: float = Field(default=180.0, gt=0.0)
|
|
39
|
+
|
|
40
|
+
# --- Anthropic API (check_deep only) ------------------------------------
|
|
41
|
+
anthropic_base_url: str = Field(default="https://api.anthropic.com/v1/messages")
|
|
42
|
+
anthropic_version: str = Field(default="2023-06-01")
|
|
43
|
+
anthropic_model: str = Field(
|
|
44
|
+
default="claude-opus-4-8",
|
|
45
|
+
description="Model for judge A and the counterexample probe (the calibrated default).",
|
|
46
|
+
)
|
|
47
|
+
judge_b_model: str = Field(
|
|
48
|
+
default="claude-fable-5",
|
|
49
|
+
description=(
|
|
50
|
+
"Model for the checklist judge (judge B). The default matches the "
|
|
51
|
+
"calibrated configuration. Fable/Mythos are locked-surface models: "
|
|
52
|
+
"thinking is always on and billed as output tokens, so judge B "
|
|
53
|
+
"gets a 32k max_tokens budget automatically."
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
anthropic_timeout_seconds: float = Field(
|
|
57
|
+
default=600.0,
|
|
58
|
+
gt=0.0,
|
|
59
|
+
description=(
|
|
60
|
+
"HTTP read timeout. Always-thinking models can spend minutes on one "
|
|
61
|
+
"response, and a timeout that fires mid-generation still bills the "
|
|
62
|
+
"full response server-side — keep this generous."
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
max_tokens: int = Field(
|
|
66
|
+
default=4096,
|
|
67
|
+
ge=1,
|
|
68
|
+
description="Response budget for judge A (judge B is sized automatically).",
|
|
69
|
+
)
|
leanscreen/errors.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Typed error hierarchy.
|
|
2
|
+
|
|
3
|
+
Ground rule: no bare excepts, no silent failures. Every failure path raises a
|
|
4
|
+
typed error from this module so callers can branch on *why* something failed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LeanScreenError(Exception):
|
|
11
|
+
"""Base class for every error raised by this package."""
|
|
12
|
+
|
|
13
|
+
#: Stable machine-readable code.
|
|
14
|
+
code: str = "leanscreen_error"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConfigError(LeanScreenError):
|
|
18
|
+
"""Invalid or missing configuration."""
|
|
19
|
+
|
|
20
|
+
code = "config_error"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LLMError(LeanScreenError):
|
|
24
|
+
"""The language-model API failed or returned an unusable response."""
|
|
25
|
+
|
|
26
|
+
code = "llm_error"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class VerifierError(LeanScreenError):
|
|
30
|
+
"""The Lean verifier could not be invoked (toolchain/config problem).
|
|
31
|
+
|
|
32
|
+
Note: a Lean *rejection* of a statement is a normal result, not this error;
|
|
33
|
+
this is raised only when the verifier itself cannot run.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
code = "verifier_error"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CreditsExhaustedError(LeanScreenError):
|
|
40
|
+
"""The API account has no credit — a GLOBAL condition, not a per-call one.
|
|
41
|
+
|
|
42
|
+
Deliberately NOT an LLMError subclass: per-call handlers catch LLMError
|
|
43
|
+
and continue, but an empty account dooms every subsequent call, so this
|
|
44
|
+
must propagate instead of being absorbed.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
code = "credits_exhausted"
|
leanscreen/falsify.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Counterexample falsification probe — shared by the adversarial sweep script
|
|
2
|
+
and the external scoring harness.
|
|
3
|
+
|
|
4
|
+
A statement is FALSIFIED only when the model exhibits concrete values that
|
|
5
|
+
satisfy every hypothesis while the conclusion fails, under real Lean/mathlib
|
|
6
|
+
semantics (junk values included). Objective evidence, not judgment: the probe
|
|
7
|
+
may only ever add a REJECT signal (invariant #4 — nothing here certifies).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from leanscreen.llm import LLMClient
|
|
17
|
+
|
|
18
|
+
FALSIFY_SYSTEM_PROMPT = (
|
|
19
|
+
"You attempt to FALSIFY a Lean 4 theorem statement by constructing an explicit "
|
|
20
|
+
"counterexample. This is a narrow, objective task — NOT a faithfulness judgment, "
|
|
21
|
+
"NOT a style review, NOT a provability assessment. A statement is FALSIFIED only "
|
|
22
|
+
"if you exhibit concrete values for its variables and free parameters such that "
|
|
23
|
+
"every hypothesis is satisfied and the conclusion is false, all under actual "
|
|
24
|
+
"Lean/mathlib semantics.\n"
|
|
25
|
+
"SEARCH ORDER — try each in turn:\n"
|
|
26
|
+
"1. Degenerate instantiations of unpinned parameters: the empty set, the zero "
|
|
27
|
+
"function, a constant function, Set.univ, t = 0, n = 0 or 1, the trivial "
|
|
28
|
+
"group/space. A parameter with no hypothesis linking it to the conclusion's "
|
|
29
|
+
"objects is free — instantiate it adversarially.\n"
|
|
30
|
+
"2. Junk values — these are REAL mathlib semantics and count: Real.log 0 = 0; "
|
|
31
|
+
"x / 0 = 0; an `∃ C : ENNReal` may pick C = ∞ (and ∞ * 0 = 0); nat subtraction "
|
|
32
|
+
"truncates at 0; nat division floors; Real.sqrt of a negative = 0; `deriv` of a "
|
|
33
|
+
"non-differentiable function = 0.\n"
|
|
34
|
+
"3. Boundary cases the hypotheses permit: x = -1 when the hypothesis is -1 ≤ x; "
|
|
35
|
+
"equality cases of ≤; empty index ranges (empty product = 1, empty sum = 0); "
|
|
36
|
+
"k = 0 exponents.\n"
|
|
37
|
+
"4. Extreme but legal values: a constant large enough or small enough to break "
|
|
38
|
+
"an unparameterized inequality.\n"
|
|
39
|
+
"RULES:\n"
|
|
40
|
+
"- The counterexample must be CHECKABLE: state the value of every relevant "
|
|
41
|
+
"variable, verify EACH hypothesis holds (including typeclass assumptions like "
|
|
42
|
+
"[Fact p.Prime] — pick a concrete instance), and show the conclusion fails.\n"
|
|
43
|
+
"- If you cannot satisfy all hypotheses simultaneously, the statement is "
|
|
44
|
+
"vacuously true — that is NOT falsified.\n"
|
|
45
|
+
"- 'I cannot prove it' is NOT falsified. Generality mismatches with the "
|
|
46
|
+
"informal statement are out of scope.\n"
|
|
47
|
+
"- Do not claim falsified on an uncertain computation: if the conclusion's "
|
|
48
|
+
"failure rests on arithmetic you are not sure of, report not falsified.\n"
|
|
49
|
+
"- Prefer the SIMPLEST counterexample that works.\n"
|
|
50
|
+
"- BUDGET your search: brief scratch work is fine, but keep it under ~300 words. "
|
|
51
|
+
"If the search is inconclusive by then, stop and report not falsified. You MUST "
|
|
52
|
+
"end your reply with the JSON verdict object — a reply without it is discarded "
|
|
53
|
+
"and wasted.\n"
|
|
54
|
+
"OUTPUT: end with exactly one JSON object:\n"
|
|
55
|
+
'{"falsified": true|false, '
|
|
56
|
+
'"counterexample": "<explicit assignment of every relevant variable, or empty>", '
|
|
57
|
+
'"check": "<hypotheses hold because ...; conclusion fails because ...>", '
|
|
58
|
+
'"informal_also_false": true|false|null}\n'
|
|
59
|
+
"Set informal_also_false = true ONLY when falsified AND the informal statement "
|
|
60
|
+
"plainly asserts the same false claim (then this may be a faithful render of a "
|
|
61
|
+
"false claim — a different problem than a translation error). Use null when "
|
|
62
|
+
"not falsified."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True, slots=True)
|
|
67
|
+
class FalsifyResult:
|
|
68
|
+
"""One falsification attempt's verdict."""
|
|
69
|
+
|
|
70
|
+
falsified: bool
|
|
71
|
+
counterexample: str
|
|
72
|
+
check: str
|
|
73
|
+
informal_also_false: bool | None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def extract_json(raw: str) -> dict[str, Any] | None:
|
|
77
|
+
"""Last balanced ``{...}`` in ``raw`` that parses as a JSON object.
|
|
78
|
+
|
|
79
|
+
A backward balanced-brace scan, not a regex: the model's prose routinely
|
|
80
|
+
contains Lean braces, and the verdict object is always last.
|
|
81
|
+
"""
|
|
82
|
+
end = raw.rfind("}")
|
|
83
|
+
while end != -1:
|
|
84
|
+
depth = 0
|
|
85
|
+
for i in range(end, -1, -1):
|
|
86
|
+
if raw[i] == "}":
|
|
87
|
+
depth += 1
|
|
88
|
+
elif raw[i] == "{":
|
|
89
|
+
depth -= 1
|
|
90
|
+
if depth == 0:
|
|
91
|
+
try:
|
|
92
|
+
obj = json.loads(raw[i : end + 1])
|
|
93
|
+
except json.JSONDecodeError:
|
|
94
|
+
break
|
|
95
|
+
return obj if isinstance(obj, dict) else None
|
|
96
|
+
end = raw.rfind("}", 0, end)
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def falsify(client: LLMClient, informal: str, lean_code: str) -> FalsifyResult:
|
|
101
|
+
"""Run one falsification attempt; raises ValueError on an unparseable reply."""
|
|
102
|
+
user = f"Informal statement:\n{informal}\n\nLean statement to falsify:\n{lean_code}"
|
|
103
|
+
raw = client.complete(FALSIFY_SYSTEM_PROMPT, user)
|
|
104
|
+
obj = extract_json(raw)
|
|
105
|
+
if obj is None:
|
|
106
|
+
raise ValueError(f"no JSON object in response: {raw[:120]!r}")
|
|
107
|
+
falsified = obj.get("falsified")
|
|
108
|
+
if not isinstance(falsified, bool):
|
|
109
|
+
raise ValueError(f"missing/invalid falsified field: {raw[:120]!r}")
|
|
110
|
+
also = obj.get("informal_also_false")
|
|
111
|
+
return FalsifyResult(
|
|
112
|
+
falsified=falsified,
|
|
113
|
+
counterexample=str(obj.get("counterexample", "")).strip()[:600],
|
|
114
|
+
check=str(obj.get("check", "")).strip()[:800],
|
|
115
|
+
informal_also_false=also if isinstance(also, bool) else None,
|
|
116
|
+
)
|