paperproof-mcp 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.
- paperproof_mcp/__init__.py +0 -0
- paperproof_mcp/__main__.py +9 -0
- paperproof_mcp/converter.py +375 -0
- paperproof_mcp/lean_parsers/v4.25.0.lean +532 -0
- paperproof_mcp/lean_parsers/v4.27.0.lean +532 -0
- paperproof_mcp/lean_runner.py +251 -0
- paperproof_mcp/natural.py +198 -0
- paperproof_mcp/server.py +214 -0
- paperproof_mcp-0.1.0.dist-info/METADATA +160 -0
- paperproof_mcp-0.1.0.dist-info/RECORD +13 -0
- paperproof_mcp-0.1.0.dist-info/WHEEL +4 -0
- paperproof_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- paperproof_mcp-0.1.0.dist-info/licenses/LICENSE +29 -0
|
File without changes
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Faithful port of Paperproof's app/src/services/converter.ts.
|
|
3
|
+
|
|
4
|
+
Turns a LeanProofTree (list of ProofStep dicts, exactly as emitted by
|
|
5
|
+
Paperproof's BetterParser) into a ConvertedProofTree (boxes + tactics + arrows).
|
|
6
|
+
The input dicts use Paperproof's Lean JSON field names.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
FAKE_POSITION = {"line": 0, "character": 0}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _Converter:
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
self.box_id = 1
|
|
18
|
+
self.tactic_id = 1
|
|
19
|
+
self.pretty: dict[str, Any] = {"boxes": [], "tactics": [], "equivalentIds": {}}
|
|
20
|
+
|
|
21
|
+
def _new_box_id(self) -> str:
|
|
22
|
+
v = str(self.box_id)
|
|
23
|
+
self.box_id += 1
|
|
24
|
+
return v
|
|
25
|
+
|
|
26
|
+
def _new_tactic_id(self) -> str:
|
|
27
|
+
v = str(self.tactic_id)
|
|
28
|
+
self.tactic_id += 1
|
|
29
|
+
return v
|
|
30
|
+
|
|
31
|
+
def get_displayed_id(self, id_: Optional[str]) -> Optional[str]:
|
|
32
|
+
if id_ is None:
|
|
33
|
+
return None
|
|
34
|
+
for displayed_id, inferiors in self.pretty["equivalentIds"].items():
|
|
35
|
+
if any(inferior == id_ for inferior in inferiors):
|
|
36
|
+
return displayed_id
|
|
37
|
+
return id_
|
|
38
|
+
|
|
39
|
+
def add_to_equivalent_ids(self, before_id: str, after_id: str) -> None:
|
|
40
|
+
key = self.get_displayed_id(before_id)
|
|
41
|
+
existing = self.pretty["equivalentIds"].get(key)
|
|
42
|
+
if existing is not None:
|
|
43
|
+
existing.append(after_id)
|
|
44
|
+
else:
|
|
45
|
+
self.pretty["equivalentIds"][before_id] = [after_id]
|
|
46
|
+
|
|
47
|
+
def get_box_by_goal_id(self, goal_id: str) -> Optional[dict]:
|
|
48
|
+
displayed = self.get_displayed_id(goal_id)
|
|
49
|
+
for box in self.pretty["boxes"]:
|
|
50
|
+
if any(g["id"] == displayed for g in box["goalNodes"]):
|
|
51
|
+
return box
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
def create_new_box(self, parent_id: str) -> dict:
|
|
55
|
+
new_box = {
|
|
56
|
+
"id": self._new_box_id(),
|
|
57
|
+
"parentId": parent_id,
|
|
58
|
+
"goalNodes": [],
|
|
59
|
+
"hypLayers": [],
|
|
60
|
+
"hypTables": [],
|
|
61
|
+
}
|
|
62
|
+
self.pretty["boxes"].append(new_box)
|
|
63
|
+
return new_box
|
|
64
|
+
|
|
65
|
+
def _weird_situation(self, hyp_after: dict) -> None:
|
|
66
|
+
# A hyp changed name/type but kept its id: mutate the already-drawn node.
|
|
67
|
+
hyp_after_id = self.get_displayed_id(hyp_after["id"])
|
|
68
|
+
for box in self.pretty["boxes"]:
|
|
69
|
+
for layer in box["hypLayers"]:
|
|
70
|
+
for node in layer["hypNodes"]:
|
|
71
|
+
if node["id"] == hyp_after_id:
|
|
72
|
+
node["name"] = hyp_after["username"]
|
|
73
|
+
node["text"] = hyp_after["type"]
|
|
74
|
+
|
|
75
|
+
def draw_new_hypothesis_layer(
|
|
76
|
+
self, hyps_before: list[dict], hyps_after: list[dict]
|
|
77
|
+
) -> tuple[list[dict], list[dict]]:
|
|
78
|
+
pretty_hyp_nodes: list[dict] = []
|
|
79
|
+
pretty_hyp_arrows: list[dict] = []
|
|
80
|
+
hyps_after_matched: list[dict] = []
|
|
81
|
+
hyps_before_matched: list[dict] = []
|
|
82
|
+
|
|
83
|
+
# 1. Hyps clearly connected to a particular previous hyp.
|
|
84
|
+
for hyp_after in hyps_after:
|
|
85
|
+
hyp_before_by_id = next(
|
|
86
|
+
(h for h in hyps_before if h["id"] == hyp_after["id"]), None
|
|
87
|
+
)
|
|
88
|
+
hyp_before_by_name = next(
|
|
89
|
+
(
|
|
90
|
+
hb
|
|
91
|
+
for hb in hyps_before
|
|
92
|
+
if hb["username"] == hyp_after["username"]
|
|
93
|
+
and not any(
|
|
94
|
+
h["id"] == hb["id"] and h["id"] != hyp_after["id"]
|
|
95
|
+
for h in hyps_after
|
|
96
|
+
)
|
|
97
|
+
),
|
|
98
|
+
None,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if hyp_before_by_id:
|
|
102
|
+
hyps_after_matched.append(hyp_after)
|
|
103
|
+
hyps_before_matched.append(hyp_before_by_id)
|
|
104
|
+
same_name = hyp_after["username"] == hyp_before_by_id["username"]
|
|
105
|
+
same_type = hyp_after["type"] == hyp_before_by_id["type"]
|
|
106
|
+
if same_name and same_type:
|
|
107
|
+
pass
|
|
108
|
+
else:
|
|
109
|
+
self._weird_situation(hyp_after)
|
|
110
|
+
elif hyp_before_by_name:
|
|
111
|
+
hyps_after_matched.append(hyp_after)
|
|
112
|
+
hyps_before_matched.append(hyp_before_by_name)
|
|
113
|
+
same_id = hyp_after["id"] == hyp_before_by_name["id"]
|
|
114
|
+
same_type = hyp_after["type"] == hyp_before_by_name["type"]
|
|
115
|
+
if same_id and same_type:
|
|
116
|
+
pass
|
|
117
|
+
elif same_id and not same_type:
|
|
118
|
+
self._weird_situation(hyp_after)
|
|
119
|
+
elif not same_id and same_type:
|
|
120
|
+
self.add_to_equivalent_ids(hyp_before_by_name["id"], hyp_after["id"])
|
|
121
|
+
else:
|
|
122
|
+
pretty_hyp_nodes.append(
|
|
123
|
+
{
|
|
124
|
+
"text": hyp_after["type"],
|
|
125
|
+
"name": hyp_after["username"],
|
|
126
|
+
"id": hyp_after["id"],
|
|
127
|
+
"isProof": hyp_after["isProof"],
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
pretty_hyp_arrows.append(
|
|
131
|
+
{
|
|
132
|
+
"fromId": hyp_before_by_name["id"],
|
|
133
|
+
"toIds": [hyp_after["id"]],
|
|
134
|
+
"shardId": "temporary",
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# 2. Hyps that disappeared / appeared from nowhere.
|
|
139
|
+
matched_before_ids = {h["id"] for h in hyps_before_matched}
|
|
140
|
+
matched_after_ids = {id(h) for h in hyps_after_matched}
|
|
141
|
+
hyps_before_disappeared = [
|
|
142
|
+
h for h in hyps_before if h["id"] not in matched_before_ids
|
|
143
|
+
]
|
|
144
|
+
hyps_after_appeared = [
|
|
145
|
+
h for h in hyps_after if id(h) not in matched_after_ids
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
if not hyps_before_disappeared and not hyps_after_appeared:
|
|
149
|
+
pass
|
|
150
|
+
elif not hyps_before_disappeared and hyps_after_appeared:
|
|
151
|
+
for hyp_after in hyps_after_appeared:
|
|
152
|
+
pretty_hyp_nodes.append(
|
|
153
|
+
{
|
|
154
|
+
"text": hyp_after["type"],
|
|
155
|
+
"name": hyp_after["username"],
|
|
156
|
+
"id": hyp_after["id"],
|
|
157
|
+
"isProof": hyp_after["isProof"],
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
pretty_hyp_arrows.append(
|
|
161
|
+
{
|
|
162
|
+
"fromId": None,
|
|
163
|
+
"toIds": [h["id"] for h in hyps_after_appeared],
|
|
164
|
+
"shardId": "temporary",
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
elif hyps_before_disappeared and not hyps_after_appeared:
|
|
168
|
+
pass
|
|
169
|
+
else:
|
|
170
|
+
branching_hyp_before = hyps_before_disappeared[0]
|
|
171
|
+
for hyp_after in hyps_after_appeared:
|
|
172
|
+
pretty_hyp_nodes.append(
|
|
173
|
+
{
|
|
174
|
+
"text": hyp_after["type"],
|
|
175
|
+
"name": hyp_after["username"],
|
|
176
|
+
"id": hyp_after["id"],
|
|
177
|
+
"isProof": hyp_after["isProof"],
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
pretty_hyp_arrows.append(
|
|
181
|
+
{
|
|
182
|
+
"fromId": branching_hyp_before["id"],
|
|
183
|
+
"toIds": [h["id"] for h in hyps_after_appeared],
|
|
184
|
+
"shardId": "temporary",
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return pretty_hyp_nodes, pretty_hyp_arrows
|
|
189
|
+
|
|
190
|
+
def handle_tactic_app(self, tactic: dict) -> None:
|
|
191
|
+
goal_before = tactic["goalBefore"]
|
|
192
|
+
current_box = self.get_box_by_goal_id(goal_before["id"])
|
|
193
|
+
if not current_box:
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
goals_after = list(tactic["goalsAfter"])
|
|
197
|
+
pretty_goal_arrows: list[dict] = []
|
|
198
|
+
by_box_ids: list[str] = []
|
|
199
|
+
have_box_ids: list[str] = []
|
|
200
|
+
|
|
201
|
+
# CASE_1: `have`-like, spawnedGoals become haveBoxes.
|
|
202
|
+
if len(goals_after) == 1 and goals_after[0]["type"] == goal_before["type"]:
|
|
203
|
+
self.add_to_equivalent_ids(goal_before["id"], tactic["goalsAfter"][0]["id"])
|
|
204
|
+
boxes = []
|
|
205
|
+
for goal in tactic["spawnedGoals"]:
|
|
206
|
+
new_box = self.create_new_box("haveBox")
|
|
207
|
+
new_box["goalNodes"].append(
|
|
208
|
+
{"text": goal["type"], "name": goal["username"], "id": goal["id"]}
|
|
209
|
+
)
|
|
210
|
+
boxes.append(new_box)
|
|
211
|
+
have_box_ids = [b["id"] for b in boxes]
|
|
212
|
+
else:
|
|
213
|
+
# CASE_2: anonymous `by` calls, spawnedGoals become byBoxes.
|
|
214
|
+
if len(tactic["goalsAfter"]) >= 1 and len(tactic["spawnedGoals"]) >= 1:
|
|
215
|
+
boxes = []
|
|
216
|
+
for goal in tactic["spawnedGoals"]:
|
|
217
|
+
new_box = self.create_new_box("byBox")
|
|
218
|
+
new_box["goalNodes"].append(
|
|
219
|
+
{"text": goal["type"], "name": goal["username"], "id": goal["id"]}
|
|
220
|
+
)
|
|
221
|
+
boxes.append(new_box)
|
|
222
|
+
by_box_ids = [b["id"] for b in boxes]
|
|
223
|
+
# CASE_3: `calc` subgoals.
|
|
224
|
+
else:
|
|
225
|
+
for goal in tactic["spawnedGoals"]:
|
|
226
|
+
goals_after.append({**goal, "isSpawned": True})
|
|
227
|
+
|
|
228
|
+
for goal in goals_after:
|
|
229
|
+
pretty_goal_arrows.append(
|
|
230
|
+
{"fromId": goal_before["id"], "toId": goal["id"]}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
pretty_hyp_arrows: list[dict] = []
|
|
234
|
+
tactic_id = self._new_tactic_id()
|
|
235
|
+
for goal in goals_after:
|
|
236
|
+
pretty_hyp_nodes, arrows_for_child = self.draw_new_hypothesis_layer(
|
|
237
|
+
goal_before["hyps"], goal["hyps"]
|
|
238
|
+
)
|
|
239
|
+
pretty_hyp_arrows.extend(arrows_for_child)
|
|
240
|
+
|
|
241
|
+
box = (
|
|
242
|
+
self.create_new_box(current_box["id"])
|
|
243
|
+
if len(goals_after) > 1
|
|
244
|
+
else current_box
|
|
245
|
+
)
|
|
246
|
+
last_goal = box["goalNodes"][-1] if box["goalNodes"] else None
|
|
247
|
+
if not last_goal or last_goal["text"] != goal["type"]:
|
|
248
|
+
box["goalNodes"].append(
|
|
249
|
+
{"text": goal["type"], "name": goal["username"], "id": goal["id"]}
|
|
250
|
+
)
|
|
251
|
+
if pretty_hyp_nodes:
|
|
252
|
+
box["hypLayers"].append(
|
|
253
|
+
{"tacticId": tactic_id, "hypNodes": pretty_hyp_nodes}
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
self.pretty["tactics"].append(
|
|
257
|
+
{
|
|
258
|
+
"id": tactic_id,
|
|
259
|
+
"text": tactic["tacticString"],
|
|
260
|
+
"dependsOnIds": tactic["tacticDependsOn"],
|
|
261
|
+
"goalArrows": pretty_goal_arrows,
|
|
262
|
+
"hypArrows": pretty_hyp_arrows,
|
|
263
|
+
"successGoalId": goal_before["id"] if len(goals_after) == 0 else None,
|
|
264
|
+
"haveBoxIds": have_box_ids,
|
|
265
|
+
"byBoxIds": by_box_ids,
|
|
266
|
+
"position": tactic["position"],
|
|
267
|
+
"theorems": tactic["theorems"],
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def draw_initial_goal(self, lean_proof_tree: list[dict]) -> None:
|
|
272
|
+
tactic_id = self._new_tactic_id()
|
|
273
|
+
initial_goal = lean_proof_tree[0]["goalBefore"]
|
|
274
|
+
hyp_nodes = [
|
|
275
|
+
{
|
|
276
|
+
"text": hyp["type"],
|
|
277
|
+
"name": hyp["username"],
|
|
278
|
+
"id": hyp["id"],
|
|
279
|
+
"isProof": hyp["isProof"],
|
|
280
|
+
}
|
|
281
|
+
for hyp in initial_goal["hyps"]
|
|
282
|
+
]
|
|
283
|
+
initial_box = {
|
|
284
|
+
"id": self._new_box_id(),
|
|
285
|
+
"parentId": None,
|
|
286
|
+
"goalNodes": [
|
|
287
|
+
{
|
|
288
|
+
"text": initial_goal["type"],
|
|
289
|
+
"name": initial_goal["username"],
|
|
290
|
+
"id": initial_goal["id"],
|
|
291
|
+
}
|
|
292
|
+
],
|
|
293
|
+
"hypLayers": [{"tacticId": tactic_id, "hypNodes": hyp_nodes}]
|
|
294
|
+
if hyp_nodes
|
|
295
|
+
else [],
|
|
296
|
+
"hypTables": [],
|
|
297
|
+
}
|
|
298
|
+
self.pretty["boxes"].append(initial_box)
|
|
299
|
+
self.pretty["tactics"].append(
|
|
300
|
+
{
|
|
301
|
+
"id": tactic_id,
|
|
302
|
+
"text": "init",
|
|
303
|
+
"dependsOnIds": [],
|
|
304
|
+
"goalArrows": [],
|
|
305
|
+
"hypArrows": [
|
|
306
|
+
{
|
|
307
|
+
"fromId": None,
|
|
308
|
+
"toIds": [n["id"] for n in hyp_nodes],
|
|
309
|
+
"shardId": "temporary",
|
|
310
|
+
}
|
|
311
|
+
],
|
|
312
|
+
"haveBoxIds": [],
|
|
313
|
+
"byBoxIds": [],
|
|
314
|
+
"position": {"start": FAKE_POSITION, "stop": FAKE_POSITION},
|
|
315
|
+
"theorems": [],
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def postprocess(self) -> None:
|
|
320
|
+
for tactic in self.pretty["tactics"]:
|
|
321
|
+
tactic["goalArrows"] = [
|
|
322
|
+
{
|
|
323
|
+
"fromId": self.get_displayed_id(a["fromId"]),
|
|
324
|
+
"toId": self.get_displayed_id(a["toId"]),
|
|
325
|
+
}
|
|
326
|
+
for a in tactic["goalArrows"]
|
|
327
|
+
]
|
|
328
|
+
tactic["hypArrows"] = [
|
|
329
|
+
{
|
|
330
|
+
"shardId": str(index),
|
|
331
|
+
"fromId": self.get_displayed_id(a["fromId"]),
|
|
332
|
+
"toIds": [self.get_displayed_id(t) for t in a["toIds"]],
|
|
333
|
+
}
|
|
334
|
+
for index, a in enumerate(tactic["hypArrows"])
|
|
335
|
+
]
|
|
336
|
+
tactic["dependsOnIds"] = [
|
|
337
|
+
self.get_displayed_id(i) for i in tactic["dependsOnIds"]
|
|
338
|
+
]
|
|
339
|
+
if tactic.get("successGoalId"):
|
|
340
|
+
tactic["successGoalId"] = self.get_displayed_id(tactic["successGoalId"])
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _remove_particular_hyps_from_tactic(tactic: dict) -> None:
|
|
344
|
+
# Universe hyps are `variable` noise (common in Mathlib) with nothing to do
|
|
345
|
+
# with the proof.
|
|
346
|
+
goals = [*tactic["goalsAfter"], tactic["goalBefore"], *tactic["spawnedGoals"]]
|
|
347
|
+
for goal in goals:
|
|
348
|
+
goal["hyps"] = [h for h in goal["hyps"] if h["isProof"] != "universe"]
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _filter_backtracking_steps(steps: list[dict]) -> list[dict]:
|
|
352
|
+
# Steps can be generated by backtracking (e.g. `first | ... | ...`); keep only
|
|
353
|
+
# the last step from each goal.
|
|
354
|
+
result: list[dict] = []
|
|
355
|
+
from_goals = [s["goalBefore"]["id"] for s in steps]
|
|
356
|
+
for i in range(len(steps)):
|
|
357
|
+
if from_goals[i] not in from_goals[i + 1:]:
|
|
358
|
+
result.append(steps[i])
|
|
359
|
+
return result
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def converter(lean_proof_tree: list[dict]) -> dict:
|
|
363
|
+
c = _Converter()
|
|
364
|
+
if not lean_proof_tree:
|
|
365
|
+
return c.pretty
|
|
366
|
+
|
|
367
|
+
for tactic in lean_proof_tree:
|
|
368
|
+
_remove_particular_hyps_from_tactic(tactic)
|
|
369
|
+
lean_proof_tree = _filter_backtracking_steps(lean_proof_tree)
|
|
370
|
+
|
|
371
|
+
c.draw_initial_goal(lean_proof_tree)
|
|
372
|
+
for tactic in lean_proof_tree:
|
|
373
|
+
c.handle_tactic_app(tactic)
|
|
374
|
+
c.postprocess()
|
|
375
|
+
return c.pretty
|