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.
File without changes
@@ -0,0 +1,9 @@
1
+ from paperproof_mcp.server import mcp
2
+
3
+
4
+ def main():
5
+ mcp.run()
6
+
7
+
8
+ if __name__ == "__main__":
9
+ main()
@@ -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