rocq-solid-mcp 1.0.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.
- rocq_solid_mcp/__init__.py +6 -0
- rocq_solid_mcp/cli.py +63 -0
- rocq_solid_mcp/config.py +146 -0
- rocq_solid_mcp/manager.py +1082 -0
- rocq_solid_mcp/models.py +767 -0
- rocq_solid_mcp/notifications.py +225 -0
- rocq_solid_mcp/pipeline.py +91 -0
- rocq_solid_mcp/pretty.py +167 -0
- rocq_solid_mcp/rocq_client.py +980 -0
- rocq_solid_mcp/server.py +156 -0
- rocq_solid_mcp/session.py +84 -0
- rocq_solid_mcp-1.0.0.dist-info/METADATA +184 -0
- rocq_solid_mcp-1.0.0.dist-info/RECORD +17 -0
- rocq_solid_mcp-1.0.0.dist-info/WHEEL +5 -0
- rocq_solid_mcp-1.0.0.dist-info/entry_points.txt +3 -0
- rocq_solid_mcp-1.0.0.dist-info/licenses/LICENSE +21 -0
- rocq_solid_mcp-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NotificationStore:
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self._diagnostics: dict[tuple[str, int | None], list[dict[str, Any]]] = {}
|
|
11
|
+
self._diagnostic_events: dict[tuple[str, int | None], asyncio.Event] = {}
|
|
12
|
+
self._latest_versions_by_uri: dict[str, int | None] = {}
|
|
13
|
+
self._file_progress: dict[tuple[str, int | None], dict[str, Any]] = {}
|
|
14
|
+
self._latest_progress_versions_by_uri: dict[str, int | None] = {}
|
|
15
|
+
self._file_perf_data: dict[tuple[str, int | None], dict[str, Any]] = {}
|
|
16
|
+
self._perf_events: dict[tuple[str, int | None], asyncio.Event] = {}
|
|
17
|
+
self._latest_perf_versions_by_uri: dict[str, int | None] = {}
|
|
18
|
+
self._execution_info: dict[
|
|
19
|
+
tuple[str, int | None], tuple[int, dict[str, Any]]
|
|
20
|
+
] = {}
|
|
21
|
+
self._execution_ranges: dict[tuple[str, int | None], list[dict[str, Any]]] = {}
|
|
22
|
+
self._execution_events: dict[tuple[str, int | None], asyncio.Event] = {}
|
|
23
|
+
self._execution_sequence = 0
|
|
24
|
+
|
|
25
|
+
def handle(self, method: str, params: Any | None) -> None:
|
|
26
|
+
if method == "textDocument/publishDiagnostics" and isinstance(params, dict):
|
|
27
|
+
self._handle_publish_diagnostics(params)
|
|
28
|
+
if method == "$/coq/fileProgress" and isinstance(params, dict):
|
|
29
|
+
self._handle_file_progress(params)
|
|
30
|
+
if method == "$/coq/filePerfData" and isinstance(params, dict):
|
|
31
|
+
self._handle_file_perf_data(params)
|
|
32
|
+
if method == "$/coq/executionInformation" and isinstance(params, dict):
|
|
33
|
+
self._handle_execution_info(params)
|
|
34
|
+
|
|
35
|
+
def clear(self, uri: str, version: int) -> None:
|
|
36
|
+
# A session serializes calls for one URI, so starting a new version makes
|
|
37
|
+
# all notification payloads and waiter events for older versions stale.
|
|
38
|
+
for mapping in (
|
|
39
|
+
self._diagnostics,
|
|
40
|
+
self._diagnostic_events,
|
|
41
|
+
self._file_progress,
|
|
42
|
+
self._file_perf_data,
|
|
43
|
+
self._perf_events,
|
|
44
|
+
self._execution_info,
|
|
45
|
+
self._execution_ranges,
|
|
46
|
+
self._execution_events,
|
|
47
|
+
):
|
|
48
|
+
for key in [key for key in mapping if key[0] == uri]:
|
|
49
|
+
mapping.pop(key, None)
|
|
50
|
+
self._latest_versions_by_uri.pop(uri, None)
|
|
51
|
+
self._latest_progress_versions_by_uri.pop(uri, None)
|
|
52
|
+
self._latest_perf_versions_by_uri.pop(uri, None)
|
|
53
|
+
|
|
54
|
+
def _clear_stale_perf_data(self, uri: str, version: int | None) -> None:
|
|
55
|
+
for key in list(self._file_perf_data):
|
|
56
|
+
if key[0] == uri and key[1] != version:
|
|
57
|
+
self._file_perf_data.pop(key, None)
|
|
58
|
+
self._perf_events.pop(key, None)
|
|
59
|
+
|
|
60
|
+
def _clear_stale_execution_data(self, uri: str, version: int | None) -> None:
|
|
61
|
+
for key in list(self._execution_info):
|
|
62
|
+
if key[0] == uri and key[1] != version:
|
|
63
|
+
self._execution_info.pop(key, None)
|
|
64
|
+
self._execution_ranges.pop(key, None)
|
|
65
|
+
self._execution_events.pop(key, None)
|
|
66
|
+
|
|
67
|
+
def latest_file_progress(self, uri: str, version: int) -> dict[str, Any] | None:
|
|
68
|
+
key = (uri, version)
|
|
69
|
+
if key in self._file_progress:
|
|
70
|
+
return self._file_progress[key]
|
|
71
|
+
|
|
72
|
+
latest = self._latest_progress_versions_by_uri.get(uri)
|
|
73
|
+
if latest == version:
|
|
74
|
+
return self._file_progress.get((uri, latest))
|
|
75
|
+
return self._file_progress.get((uri, None))
|
|
76
|
+
|
|
77
|
+
def latest_file_perf_data(self, uri: str, version: int) -> dict[str, Any] | None:
|
|
78
|
+
key = (uri, version)
|
|
79
|
+
if key in self._file_perf_data:
|
|
80
|
+
return self._file_perf_data[key]
|
|
81
|
+
|
|
82
|
+
latest = self._latest_perf_versions_by_uri.get(uri)
|
|
83
|
+
if latest == version:
|
|
84
|
+
return self._file_perf_data.get((uri, latest))
|
|
85
|
+
return self._file_perf_data.get((uri, None))
|
|
86
|
+
|
|
87
|
+
async def wait_for_file_perf_data(
|
|
88
|
+
self, uri: str, version: int, settle_seconds: float = 0.25
|
|
89
|
+
) -> dict[str, Any] | None:
|
|
90
|
+
key = (uri, version)
|
|
91
|
+
if self.latest_file_perf_data(uri, version) is None:
|
|
92
|
+
event = self._perf_events.setdefault(key, asyncio.Event())
|
|
93
|
+
with suppress(TimeoutError):
|
|
94
|
+
await asyncio.wait_for(event.wait(), timeout=settle_seconds)
|
|
95
|
+
return self.latest_file_perf_data(uri, version)
|
|
96
|
+
|
|
97
|
+
def latest_execution_sequence(self, uri: str, version: int) -> int:
|
|
98
|
+
info = self._execution_info.get((uri, version))
|
|
99
|
+
return info[0] if info is not None else 0
|
|
100
|
+
|
|
101
|
+
def latest_execution_info(self, uri: str, version: int) -> dict[str, Any] | None:
|
|
102
|
+
info = self._execution_info.get((uri, version))
|
|
103
|
+
return info[1] if info is not None else None
|
|
104
|
+
|
|
105
|
+
def execution_ranges(self, uri: str, version: int) -> list[dict[str, Any]]:
|
|
106
|
+
ranges = self._execution_ranges.get((uri, version))
|
|
107
|
+
if ranges is not None:
|
|
108
|
+
return list(ranges)
|
|
109
|
+
return list(self._execution_ranges.get((uri, None), []))
|
|
110
|
+
|
|
111
|
+
async def wait_for_execution_info(
|
|
112
|
+
self,
|
|
113
|
+
uri: str,
|
|
114
|
+
version: int,
|
|
115
|
+
*,
|
|
116
|
+
after_sequence: int,
|
|
117
|
+
timeout: float | None,
|
|
118
|
+
) -> tuple[int, dict[str, Any]] | None:
|
|
119
|
+
key = (uri, version)
|
|
120
|
+
event = self._execution_events.setdefault(key, asyncio.Event())
|
|
121
|
+
while True:
|
|
122
|
+
event.clear()
|
|
123
|
+
info = self._execution_info.get(key)
|
|
124
|
+
if info is not None and info[0] > after_sequence:
|
|
125
|
+
return info
|
|
126
|
+
with suppress(TimeoutError):
|
|
127
|
+
await asyncio.wait_for(event.wait(), timeout=timeout)
|
|
128
|
+
continue
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def has_error_diagnostic(self, uri: str, version: int) -> bool:
|
|
132
|
+
# Exact-version lookup only: stale errors from a previous document
|
|
133
|
+
# version must not arm the after-error deadline.
|
|
134
|
+
diagnostics = self._diagnostics.get((uri, version), [])
|
|
135
|
+
return any(
|
|
136
|
+
isinstance(diagnostic, dict) and diagnostic.get("severity") == 1
|
|
137
|
+
for diagnostic in diagnostics
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
async def wait_for_error_diagnostic(self, uri: str, version: int) -> None:
|
|
141
|
+
key = (uri, version)
|
|
142
|
+
event = self._diagnostic_events.setdefault(key, asyncio.Event())
|
|
143
|
+
while True:
|
|
144
|
+
event.clear()
|
|
145
|
+
if self.has_error_diagnostic(uri, version):
|
|
146
|
+
return
|
|
147
|
+
await event.wait()
|
|
148
|
+
|
|
149
|
+
async def wait_for_diagnostics(
|
|
150
|
+
self, uri: str, version: int, settle_seconds: float = 0.25
|
|
151
|
+
) -> list[dict[str, Any]]:
|
|
152
|
+
key = (uri, version)
|
|
153
|
+
if key not in self._diagnostics:
|
|
154
|
+
event = self._diagnostic_events.setdefault(key, asyncio.Event())
|
|
155
|
+
with suppress(TimeoutError):
|
|
156
|
+
await asyncio.wait_for(event.wait(), timeout=settle_seconds)
|
|
157
|
+
|
|
158
|
+
if key in self._diagnostics:
|
|
159
|
+
return self._diagnostics[key]
|
|
160
|
+
|
|
161
|
+
latest = self._latest_versions_by_uri.get(uri)
|
|
162
|
+
if latest == version:
|
|
163
|
+
return self._diagnostics.get((uri, latest), [])
|
|
164
|
+
return self._diagnostics.get((uri, None), [])
|
|
165
|
+
|
|
166
|
+
def _handle_publish_diagnostics(self, params: dict[str, Any]) -> None:
|
|
167
|
+
uri = params.get("uri")
|
|
168
|
+
version = params.get("version")
|
|
169
|
+
diagnostics = params.get("diagnostics", [])
|
|
170
|
+
if isinstance(uri, str) and isinstance(diagnostics, list):
|
|
171
|
+
key = (uri, version)
|
|
172
|
+
self._diagnostics[key] = diagnostics
|
|
173
|
+
self._latest_versions_by_uri[uri] = version
|
|
174
|
+
if event := self._diagnostic_events.get(key):
|
|
175
|
+
event.set()
|
|
176
|
+
|
|
177
|
+
def _handle_file_progress(self, params: dict[str, Any]) -> None:
|
|
178
|
+
text_document = params.get("textDocument")
|
|
179
|
+
if not isinstance(text_document, dict):
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
uri = text_document.get("uri")
|
|
183
|
+
version = text_document.get("version")
|
|
184
|
+
if not isinstance(uri, str):
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
self._file_progress[(uri, version)] = params
|
|
188
|
+
self._latest_progress_versions_by_uri[uri] = version
|
|
189
|
+
|
|
190
|
+
def _handle_file_perf_data(self, params: dict[str, Any]) -> None:
|
|
191
|
+
text_document = params.get("textDocument")
|
|
192
|
+
if not isinstance(text_document, dict):
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
uri = text_document.get("uri")
|
|
196
|
+
version = text_document.get("version")
|
|
197
|
+
timings = params.get("timings")
|
|
198
|
+
if not isinstance(uri, str) or not isinstance(timings, list):
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
key = (uri, version)
|
|
202
|
+
self._clear_stale_perf_data(uri, version)
|
|
203
|
+
self._file_perf_data[key] = params
|
|
204
|
+
self._latest_perf_versions_by_uri[uri] = version
|
|
205
|
+
if event := self._perf_events.get(key):
|
|
206
|
+
event.set()
|
|
207
|
+
|
|
208
|
+
def _handle_execution_info(self, params: dict[str, Any]) -> None:
|
|
209
|
+
text_document = params.get("textDocument")
|
|
210
|
+
if not isinstance(text_document, dict):
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
uri = text_document.get("uri")
|
|
214
|
+
version = text_document.get("version")
|
|
215
|
+
range_ = params.get("range")
|
|
216
|
+
if not isinstance(uri, str) or not isinstance(range_, dict):
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
self._execution_sequence += 1
|
|
220
|
+
key = (uri, version)
|
|
221
|
+
self._clear_stale_execution_data(uri, version)
|
|
222
|
+
self._execution_info[key] = (self._execution_sequence, params)
|
|
223
|
+
self._execution_ranges.setdefault(key, []).append(range_)
|
|
224
|
+
if event := self._execution_events.get(key):
|
|
225
|
+
event.set()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .models import Diagnostic, is_empty_value, source_text_for_range
|
|
7
|
+
from .pretty import normalize_proof_state
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_diagnostics(
|
|
11
|
+
raw: list[dict[str, Any]],
|
|
12
|
+
document: dict[str, Any] | None,
|
|
13
|
+
*,
|
|
14
|
+
content: str | None = None,
|
|
15
|
+
supplemental_proof_states: dict[str, Any] | None = None,
|
|
16
|
+
) -> list[Diagnostic]:
|
|
17
|
+
diagnostic_range_keys = {
|
|
18
|
+
_range_key(_diagnostic_sentence_range(diagnostic)) for diagnostic in raw
|
|
19
|
+
}
|
|
20
|
+
proof_states = _proof_states_by_range(document, diagnostic_range_keys)
|
|
21
|
+
_merge_supplemental_proof_states(proof_states, supplemental_proof_states)
|
|
22
|
+
diagnostics: list[Diagnostic] = []
|
|
23
|
+
for diagnostic in raw:
|
|
24
|
+
sentence_range = _diagnostic_sentence_range(diagnostic)
|
|
25
|
+
diagnostics.append(
|
|
26
|
+
Diagnostic.from_lsp(
|
|
27
|
+
diagnostic,
|
|
28
|
+
proof_state=proof_states.get(_range_key(sentence_range)),
|
|
29
|
+
sentence=(
|
|
30
|
+
source_text_for_range(content, sentence_range)
|
|
31
|
+
if content is not None
|
|
32
|
+
else None
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
return diagnostics
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _proof_states_by_range(
|
|
40
|
+
document: dict[str, Any] | None,
|
|
41
|
+
wanted_range_keys: set[str],
|
|
42
|
+
) -> dict[str, Any]:
|
|
43
|
+
if not document:
|
|
44
|
+
return {}
|
|
45
|
+
states: dict[str, Any] = {}
|
|
46
|
+
for span in document.get("spans", []):
|
|
47
|
+
if not isinstance(span, dict):
|
|
48
|
+
continue
|
|
49
|
+
goals = span.get("goals")
|
|
50
|
+
range_ = span.get("range")
|
|
51
|
+
if goals is not None and isinstance(range_, dict):
|
|
52
|
+
key = _range_key(range_)
|
|
53
|
+
if key not in wanted_range_keys:
|
|
54
|
+
continue
|
|
55
|
+
normalized = normalize_proof_state(goals)
|
|
56
|
+
if not is_empty_value(normalized):
|
|
57
|
+
states[key] = normalized
|
|
58
|
+
return states
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _merge_supplemental_proof_states(
|
|
62
|
+
states: dict[str, Any],
|
|
63
|
+
supplemental: dict[str, Any] | None,
|
|
64
|
+
) -> None:
|
|
65
|
+
if not supplemental:
|
|
66
|
+
return
|
|
67
|
+
for key, proof_state in supplemental.items():
|
|
68
|
+
normalized = normalize_proof_state(proof_state)
|
|
69
|
+
if not isinstance(normalized, dict) or is_empty_value(normalized):
|
|
70
|
+
continue
|
|
71
|
+
current = states.get(key)
|
|
72
|
+
if isinstance(current, dict):
|
|
73
|
+
for field, value in normalized.items():
|
|
74
|
+
if field not in current and not is_empty_value(value):
|
|
75
|
+
current[field] = value
|
|
76
|
+
continue
|
|
77
|
+
states[key] = normalized
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _diagnostic_sentence_range(diagnostic: Any) -> dict[str, Any]:
|
|
81
|
+
if not isinstance(diagnostic, dict):
|
|
82
|
+
return {}
|
|
83
|
+
data = diagnostic.get("data")
|
|
84
|
+
if isinstance(data, dict) and isinstance(data.get("sentenceRange"), dict):
|
|
85
|
+
return data["sentenceRange"]
|
|
86
|
+
range_ = diagnostic.get("range")
|
|
87
|
+
return range_ if isinstance(range_, dict) else {}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _range_key(range_: dict[str, Any]) -> str:
|
|
91
|
+
return json.dumps(range_, sort_keys=True, separators=(",", ":"))
|
rocq_solid_mcp/pretty.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .models import is_empty_value, pruned_dict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def pp_to_text(value: Any) -> str:
|
|
9
|
+
"""Render rocq-lsp's serialized Pp tree into readable plain text."""
|
|
10
|
+
if value is None:
|
|
11
|
+
return ""
|
|
12
|
+
if isinstance(value, str):
|
|
13
|
+
return value
|
|
14
|
+
if not isinstance(value, list) or not value:
|
|
15
|
+
return str(value)
|
|
16
|
+
|
|
17
|
+
tag = value[0]
|
|
18
|
+
if tag == "Pp_empty":
|
|
19
|
+
return ""
|
|
20
|
+
if tag == "Pp_string":
|
|
21
|
+
return str(value[1]) if len(value) > 1 else ""
|
|
22
|
+
if tag == "Pp_sized_string":
|
|
23
|
+
return str(value[2]) if len(value) > 2 else ""
|
|
24
|
+
if tag == "Pp_glue":
|
|
25
|
+
return "".join(pp_to_text(item) for item in _list_at(value, 1))
|
|
26
|
+
if tag in {"Pp_box", "Pp_tag"}:
|
|
27
|
+
return pp_to_text(value[2]) if len(value) > 2 else ""
|
|
28
|
+
if tag == "Pp_print_break":
|
|
29
|
+
indent = value[2] if len(value) > 2 and isinstance(value[2], int) else 0
|
|
30
|
+
return "\n" + (" " * max(indent, 0))
|
|
31
|
+
if tag == "Pp_force_newline":
|
|
32
|
+
return "\n"
|
|
33
|
+
if tag == "Pp_comment":
|
|
34
|
+
return "\n".join(str(item) for item in _list_at(value, 1))
|
|
35
|
+
return str(value)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def normalize_proof_state(value: Any) -> Any:
|
|
39
|
+
"""Turn rocq-lsp goal answers into agent-facing JSON."""
|
|
40
|
+
if _is_pp(value):
|
|
41
|
+
return pp_to_text(value)
|
|
42
|
+
if isinstance(value, list):
|
|
43
|
+
return [
|
|
44
|
+
normalized
|
|
45
|
+
for item in value
|
|
46
|
+
if not is_empty_value(normalized := normalize_proof_state(item))
|
|
47
|
+
]
|
|
48
|
+
if not isinstance(value, dict):
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
if _looks_like_goal(value):
|
|
52
|
+
return _normalize_goal(value)
|
|
53
|
+
if _looks_like_goal_config(value):
|
|
54
|
+
return _normalize_goal_config(value)
|
|
55
|
+
if _looks_like_message(value):
|
|
56
|
+
return _normalize_message(value)
|
|
57
|
+
|
|
58
|
+
result: dict[str, Any] = {}
|
|
59
|
+
for key, item in value.items():
|
|
60
|
+
if key in {"textDocument", "position", "range", "error", "program"}:
|
|
61
|
+
continue
|
|
62
|
+
normalized = normalize_proof_state(item)
|
|
63
|
+
if key == "goals" and isinstance(normalized, dict):
|
|
64
|
+
result.update(normalized)
|
|
65
|
+
continue
|
|
66
|
+
if not is_empty_value(normalized):
|
|
67
|
+
result[key] = normalized
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _normalize_goal(value: dict[str, Any]) -> dict[str, Any]:
|
|
72
|
+
info = value.get("info") if isinstance(value.get("info"), dict) else {}
|
|
73
|
+
return pruned_dict(
|
|
74
|
+
{
|
|
75
|
+
"name": normalize_proof_state(info.get("name")),
|
|
76
|
+
"hypotheses": _normalize_hypotheses(value.get("hyps", [])),
|
|
77
|
+
"conclusion": normalize_proof_state(value.get("ty")),
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _normalize_goal_config(value: dict[str, Any]) -> dict[str, Any]:
|
|
83
|
+
goals = normalize_proof_state(value.get("goals", []))
|
|
84
|
+
given_up = normalize_proof_state(value.get("given_up", []))
|
|
85
|
+
result = {
|
|
86
|
+
"goals": goals,
|
|
87
|
+
"bullet": normalize_proof_state(value.get("bullet")),
|
|
88
|
+
"shelved_goals": normalize_proof_state(value.get("shelf", [])),
|
|
89
|
+
}
|
|
90
|
+
if given_up != goals:
|
|
91
|
+
result["given_up_goals"] = given_up
|
|
92
|
+
return pruned_dict(result)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _normalize_message(value: dict[str, Any]) -> dict[str, Any]:
|
|
96
|
+
result = dict(value)
|
|
97
|
+
if "text" in result:
|
|
98
|
+
result["text"] = normalize_proof_state(result["text"])
|
|
99
|
+
return pruned_dict(result)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _normalize_hypotheses(value: Any) -> list[str]:
|
|
103
|
+
if not isinstance(value, list):
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
hypotheses: list[str] = []
|
|
107
|
+
for item in value:
|
|
108
|
+
hypothesis = _normalize_hypothesis(item)
|
|
109
|
+
if hypothesis:
|
|
110
|
+
hypotheses.append(hypothesis)
|
|
111
|
+
return hypotheses
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _normalize_hypothesis(value: Any) -> str | None:
|
|
115
|
+
if not isinstance(value, dict):
|
|
116
|
+
normalized = normalize_proof_state(value)
|
|
117
|
+
return str(normalized) if normalized is not None else None
|
|
118
|
+
|
|
119
|
+
names = _hypothesis_names(value.get("names", []))
|
|
120
|
+
ty = normalize_proof_state(value.get("ty"))
|
|
121
|
+
definition = normalize_proof_state(value.get("def"))
|
|
122
|
+
if not names:
|
|
123
|
+
return str(ty) if ty is not None else None
|
|
124
|
+
|
|
125
|
+
prefix = " ".join(names)
|
|
126
|
+
if definition is not None and ty is not None:
|
|
127
|
+
return f"{prefix} := {definition} : {ty}"
|
|
128
|
+
if ty is not None:
|
|
129
|
+
return f"{prefix} : {ty}"
|
|
130
|
+
if definition is not None:
|
|
131
|
+
return f"{prefix} := {definition}"
|
|
132
|
+
return prefix
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _hypothesis_names(value: Any) -> list[str]:
|
|
136
|
+
normalized = normalize_proof_state(value)
|
|
137
|
+
if isinstance(normalized, list):
|
|
138
|
+
return [str(item) for item in normalized if item is not None]
|
|
139
|
+
if normalized is None:
|
|
140
|
+
return []
|
|
141
|
+
return [str(normalized)]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _is_pp(value: Any) -> bool:
|
|
145
|
+
return (
|
|
146
|
+
isinstance(value, list)
|
|
147
|
+
and len(value) > 0
|
|
148
|
+
and isinstance(value[0], str)
|
|
149
|
+
and value[0].startswith("Pp_")
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _looks_like_goal(value: dict[str, Any]) -> bool:
|
|
154
|
+
return "hyps" in value and "ty" in value
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _looks_like_goal_config(value: dict[str, Any]) -> bool:
|
|
158
|
+
return "goals" in value and "stack" in value and "shelf" in value
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _looks_like_message(value: dict[str, Any]) -> bool:
|
|
162
|
+
return "text" in value and "level" in value
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _list_at(value: list[Any], index: int) -> list[Any]:
|
|
166
|
+
item = value[index] if len(value) > index else []
|
|
167
|
+
return item if isinstance(item, list) else []
|