speclogician 0.0.0b1__py3-none-any.whl → 0.0.0.dev1__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.
- speclogician/agent/funcs.py +29 -0
- speclogician/cmd/agent_cmd.py +89 -0
- speclogician/cmd/data_cmd.py +24 -0
- speclogician/cmd/model_cmd.py +42 -0
- speclogician/cmd/overlay_cmd.py +30 -0
- speclogician/cmd/scenario_cmd.py +61 -0
- speclogician/cmd/state_cmd.py +52 -0
- speclogician/data/artifact.py +8 -50
- speclogician/data/container.py +18 -384
- speclogician/data/mapping.py +18 -17
- speclogician/data/refs.py +12 -11
- speclogician/data/reports.py +11 -0
- speclogician/data/traces.py +15 -6
- speclogician/llms/llmtools.py +102 -0
- speclogician/llms/overlay.py +264 -0
- speclogician/main.py +36 -102
- speclogician/modeling/__init__.py +0 -31
- speclogician/modeling/component.py +4 -60
- speclogician/modeling/conflict.py +5 -19
- speclogician/modeling/domain.py +93 -280
- speclogician/modeling/model.py +206 -0
- speclogician/modeling/predicates.py +20 -22
- speclogician/modeling/report.py +33 -0
- speclogician/modeling/scenario.py +119 -87
- speclogician/sl_cmd.py +76 -0
- speclogician/state/change.py +98 -378
- speclogician/state/state.py +183 -399
- speclogician/tui/box.tcss +10 -0
- speclogician/tui/tui.py +131 -0
- speclogician/utils/__init__.py +1 -70
- speclogician/utils/imx.py +195 -0
- speclogician/utils/load.py +25 -147
- speclogician/utils/prompt.md +1 -325
- speclogician-0.0.0.dev1.dist-info/METADATA +21 -0
- speclogician-0.0.0.dev1.dist-info/RECORD +43 -0
- speclogician/commands/__init__.py +0 -15
- speclogician/commands/cmd_ch.py +0 -616
- speclogician/commands/cmd_find.py +0 -256
- speclogician/commands/cmd_view.py +0 -202
- speclogician/commands/runner.py +0 -149
- speclogician/commands/utils.py +0 -101
- speclogician/demos/.DS_Store +0 -0
- speclogician/demos/cmd_demo.py +0 -278
- speclogician/demos/loader.py +0 -135
- speclogician/demos/model.py +0 -27
- speclogician/demos/runner.py +0 -51
- speclogician/logic/__init__.py +0 -11
- speclogician/logic/api/__init__.py +0 -29
- speclogician/logic/api/client.py +0 -606
- speclogician/logic/api/decomp.py +0 -67
- speclogician/logic/api/scenario.py +0 -102
- speclogician/logic/api/traces.py +0 -59
- speclogician/logic/lib/__init__.py +0 -19
- speclogician/logic/lib/complement.py +0 -107
- speclogician/logic/lib/domain_model.py +0 -59
- speclogician/logic/lib/predicates.py +0 -151
- speclogician/logic/lib/scenarios.py +0 -369
- speclogician/logic/lib/traces.py +0 -114
- speclogician/logic/lib/transitions.py +0 -104
- speclogician/logic/main.py +0 -246
- speclogician/logic/strings.py +0 -194
- speclogician/logic/utils.py +0 -135
- speclogician/modeling/complement.py +0 -104
- speclogician/modeling/spec.py +0 -306
- speclogician/modeling/spec_stats.py +0 -39
- speclogician/presentation/api.py +0 -244
- speclogician/presentation/builders/_links.py +0 -44
- speclogician/presentation/builders/container.py +0 -53
- speclogician/presentation/builders/data_artifact.py +0 -42
- speclogician/presentation/builders/domain.py +0 -54
- speclogician/presentation/builders/instances_list.py +0 -38
- speclogician/presentation/builders/predicate.py +0 -51
- speclogician/presentation/builders/recommendations.py +0 -41
- speclogician/presentation/builders/scenario.py +0 -41
- speclogician/presentation/builders/scenario_complement.py +0 -82
- speclogician/presentation/builders/smart_find.py +0 -39
- speclogician/presentation/builders/spec.py +0 -39
- speclogician/presentation/builders/state_diff.py +0 -150
- speclogician/presentation/builders/state_instance.py +0 -42
- speclogician/presentation/builders/state_instance_summary.py +0 -84
- speclogician/presentation/builders/trace.py +0 -58
- speclogician/presentation/ctx.py +0 -38
- speclogician/presentation/models/container.py +0 -44
- speclogician/presentation/models/data_artifact.py +0 -33
- speclogician/presentation/models/domain.py +0 -50
- speclogician/presentation/models/instances_list.py +0 -23
- speclogician/presentation/models/predicate.py +0 -60
- speclogician/presentation/models/recommendations.py +0 -34
- speclogician/presentation/models/scenario.py +0 -31
- speclogician/presentation/models/scenario_complement.py +0 -40
- speclogician/presentation/models/smart_find.py +0 -34
- speclogician/presentation/models/spec.py +0 -32
- speclogician/presentation/models/state_diff.py +0 -34
- speclogician/presentation/models/state_instance.py +0 -31
- speclogician/presentation/models/state_instance_summary.py +0 -102
- speclogician/presentation/models/trace.py +0 -42
- speclogician/presentation/preview/__init__.py +0 -13
- speclogician/presentation/preview/cli.py +0 -50
- speclogician/presentation/preview/fixtures/__init__.py +0 -205
- speclogician/presentation/preview/fixtures/artifact_container.py +0 -150
- speclogician/presentation/preview/fixtures/data_artifact.py +0 -144
- speclogician/presentation/preview/fixtures/domain_model.py +0 -162
- speclogician/presentation/preview/fixtures/instances_list.py +0 -162
- speclogician/presentation/preview/fixtures/predicate.py +0 -184
- speclogician/presentation/preview/fixtures/scenario.py +0 -84
- speclogician/presentation/preview/fixtures/scenario_complement.py +0 -81
- speclogician/presentation/preview/fixtures/smart_find.py +0 -140
- speclogician/presentation/preview/fixtures/spec.py +0 -95
- speclogician/presentation/preview/fixtures/state_diff.py +0 -158
- speclogician/presentation/preview/fixtures/state_instance.py +0 -128
- speclogician/presentation/preview/fixtures/state_instance_summary.py +0 -80
- speclogician/presentation/preview/fixtures/trace.py +0 -206
- speclogician/presentation/preview/registry.py +0 -42
- speclogician/presentation/renderers/__init__.py +0 -24
- speclogician/presentation/renderers/container.py +0 -136
- speclogician/presentation/renderers/data_artifact.py +0 -144
- speclogician/presentation/renderers/domain.py +0 -123
- speclogician/presentation/renderers/instances_list.py +0 -120
- speclogician/presentation/renderers/predicate.py +0 -180
- speclogician/presentation/renderers/recommendations.py +0 -90
- speclogician/presentation/renderers/scenario.py +0 -94
- speclogician/presentation/renderers/scenario_complement.py +0 -59
- speclogician/presentation/renderers/smart_find.py +0 -307
- speclogician/presentation/renderers/spec.py +0 -105
- speclogician/presentation/renderers/state_diff.py +0 -102
- speclogician/presentation/renderers/state_instance.py +0 -82
- speclogician/presentation/renderers/state_instance_summary.py +0 -143
- speclogician/presentation/renderers/trace.py +0 -122
- speclogician/shell/app.py +0 -170
- speclogician/shell/shell_ch.py +0 -263
- speclogician/shell/shell_view.py +0 -153
- speclogician/state/change_result.py +0 -32
- speclogician/state/diff.py +0 -191
- speclogician/state/inst.py +0 -574
- speclogician/state/recommendation.py +0 -13
- speclogician/state/recommender.py +0 -577
- speclogician/state/state_stats.py +0 -133
- speclogician/tui/__init__.py +0 -0
- speclogician/tui/app.py +0 -257
- speclogician/tui/app.tcss +0 -160
- speclogician/tui/demo.py +0 -45
- speclogician/tui/images/speclogician-full.png +0 -0
- speclogician/tui/images/speclogician-minimal.png +0 -0
- speclogician/tui/main_screen.py +0 -454
- speclogician/tui/splash_screen.py +0 -51
- speclogician/tui/stats_screen.py +0 -125
- speclogician/utils/testing.py +0 -151
- speclogician-0.0.0b1.dist-info/METADATA +0 -116
- speclogician-0.0.0b1.dist-info/RECORD +0 -139
- /speclogician/{presentation → agent}/__init__.py +0 -0
- /speclogician/{presentation/builders → cmd}/__init__.py +0 -0
- /speclogician/{presentation/models → llms}/__init__.py +0 -0
- {speclogician-0.0.0b1.dist-info → speclogician-0.0.0.dev1.dist-info}/WHEEL +0 -0
speclogician/logic/api/client.py
DELETED
|
@@ -1,606 +0,0 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Imandra Inc.
|
|
3
|
-
#
|
|
4
|
-
# speclogician/logic/api/client.py
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import os
|
|
10
|
-
import tempfile
|
|
11
|
-
|
|
12
|
-
from dataclasses import dataclass
|
|
13
|
-
from typing import Sequence
|
|
14
|
-
|
|
15
|
-
from imandrax_api_models import InstanceRes, EvalRes
|
|
16
|
-
from imandrax_api_models.client import ImandraXAsyncClient, get_imandrax_async_client
|
|
17
|
-
from iml_query.processing.decomp import DecompReqArgs
|
|
18
|
-
|
|
19
|
-
from speclogician.utils import IMLValidity
|
|
20
|
-
from speclogician.modeling.scenario import Scenario
|
|
21
|
-
from speclogician.modeling.complement import ScenarioComplement
|
|
22
|
-
from speclogician.modeling.conflict import ScenarioConsumed
|
|
23
|
-
from speclogician.modeling.domain import DomainModel
|
|
24
|
-
from speclogician.data.artifact import (
|
|
25
|
-
TraceArtifact,
|
|
26
|
-
TraceIMLValidity,
|
|
27
|
-
TraceScenarioMatchResult
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
from ..strings import (
|
|
31
|
-
mk_predicates_only_instance_src,
|
|
32
|
-
mk_scenario_expr,
|
|
33
|
-
collect_pred_names,
|
|
34
|
-
mk_goal_def,
|
|
35
|
-
mk_spec_disjunction_expr,
|
|
36
|
-
mk_instance_src_for_expr
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
import structlog
|
|
40
|
-
structlog.configure(logger_factory=structlog.ReturnLoggerFactory()) # will drop all events
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
try:
|
|
44
|
-
from dotenv import load_dotenv, find_dotenv
|
|
45
|
-
# Will look for .env file in parent dirs
|
|
46
|
-
load_dotenv(find_dotenv(usecwd=True), override=False)
|
|
47
|
-
except ImportError:
|
|
48
|
-
pass
|
|
49
|
-
|
|
50
|
-
@dataclass(frozen=True)
|
|
51
|
-
class ScenarioConsistency:
|
|
52
|
-
is_consistent : bool
|
|
53
|
-
model_src: str | None
|
|
54
|
-
inst_src: str
|
|
55
|
-
|
|
56
|
-
@dataclass(frozen=True)
|
|
57
|
-
class ScenarioIntersection:
|
|
58
|
-
scenario1_name : str
|
|
59
|
-
scenario2_name : str
|
|
60
|
-
do_they_intersect : bool
|
|
61
|
-
model_src: str | None
|
|
62
|
-
|
|
63
|
-
@dataclass(frozen=True)
|
|
64
|
-
class TraceValueValidation:
|
|
65
|
-
ok: bool
|
|
66
|
-
errors: Sequence[str]
|
|
67
|
-
compiled_src: str
|
|
68
|
-
|
|
69
|
-
@dataclass(frozen=True)
|
|
70
|
-
class TraceScenarioMatch:
|
|
71
|
-
scenario_name: str
|
|
72
|
-
matches: bool
|
|
73
|
-
witness_src: str | None
|
|
74
|
-
reason: str | None = None
|
|
75
|
-
|
|
76
|
-
class SpecLogicianImandraX:
|
|
77
|
-
def __init__(self) -> None:
|
|
78
|
-
self._client_cm = None
|
|
79
|
-
self._client: ImandraXAsyncClient | None = None
|
|
80
|
-
|
|
81
|
-
async def __aenter__(self) -> "SpecLogicianImandraX":
|
|
82
|
-
self._client_cm = get_imandrax_async_client(os.getenv('IMANDRA_UNI_KEY'))
|
|
83
|
-
self._client = await self._client_cm.__aenter__()
|
|
84
|
-
return self
|
|
85
|
-
|
|
86
|
-
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
87
|
-
assert self._client_cm is not None
|
|
88
|
-
await self._client_cm.__aexit__(exc_type, exc, tb)
|
|
89
|
-
self._client = None
|
|
90
|
-
self._client_cm = None
|
|
91
|
-
|
|
92
|
-
@property
|
|
93
|
-
def c(self) -> ImandraXAsyncClient:
|
|
94
|
-
if self._client is None:
|
|
95
|
-
raise RuntimeError("ImandraX client not initialized")
|
|
96
|
-
return self._client
|
|
97
|
-
|
|
98
|
-
async def load_model(self, iml_src: str) -> EvalRes:
|
|
99
|
-
return await self.c.eval_model(src=iml_src)
|
|
100
|
-
|
|
101
|
-
async def check_scenario_consistent(
|
|
102
|
-
self,
|
|
103
|
-
domain_model: DomainModel,
|
|
104
|
-
scenario: Scenario,
|
|
105
|
-
check_state_preds : bool,
|
|
106
|
-
check_action_preds : bool
|
|
107
|
-
) -> ScenarioConsistency:
|
|
108
|
-
|
|
109
|
-
if not check_state_preds and not check_action_preds:
|
|
110
|
-
raise ValueError("At least `check_state_preds` or `check_action_preds` must be True!")
|
|
111
|
-
|
|
112
|
-
state_preds = scenario.given if check_state_preds else ()
|
|
113
|
-
action_preds = scenario.when if check_action_preds else ()
|
|
114
|
-
|
|
115
|
-
inst_name = f"__speclogician_{scenario.name}"
|
|
116
|
-
|
|
117
|
-
# Fast local validation: better UX than solver/type errors
|
|
118
|
-
domain_model.assert_has_predicates(given=state_preds, when=action_preds)
|
|
119
|
-
|
|
120
|
-
# Build a minimized IML model containing only what this scenario needs
|
|
121
|
-
d_model = domain_model.make_iml_model(
|
|
122
|
-
state_pred_names=list(state_preds),
|
|
123
|
-
action_pred_names=list(action_preds),
|
|
124
|
-
transition_names=[],
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
eval_res = await self.c.eval_model(src=d_model)
|
|
128
|
-
|
|
129
|
-
if bool(getattr(eval_res, "has_errors", False)):
|
|
130
|
-
errs = getattr(eval_res, "errors", None)
|
|
131
|
-
if errs:
|
|
132
|
-
raise ValueError(f"Domain model has errors (e.g. {errs[0]})")
|
|
133
|
-
raise ValueError("Domain model has errors")
|
|
134
|
-
|
|
135
|
-
# Predicates-only instance query in that model context
|
|
136
|
-
inst_src = mk_predicates_only_instance_src(
|
|
137
|
-
name=inst_name,
|
|
138
|
-
given=state_preds,
|
|
139
|
-
when=action_preds,
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
res: InstanceRes = await self.c.instance_src(src=inst_src)
|
|
143
|
-
|
|
144
|
-
return ScenarioConsistency(
|
|
145
|
-
is_consistent=res.sat is not None,
|
|
146
|
-
model_src=res.sat.model.src if res.sat is not None and res.sat.model is not None else None,
|
|
147
|
-
inst_src=inst_src
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
async def check_scenarios_intersect(
|
|
151
|
-
self,
|
|
152
|
-
domain_model: DomainModel,
|
|
153
|
-
sc_a: Scenario,
|
|
154
|
-
sc_b: Scenario,
|
|
155
|
-
) -> ScenarioIntersection:
|
|
156
|
-
"""
|
|
157
|
-
Check whether two scenarios intersect (predicate-only semantics).
|
|
158
|
-
|
|
159
|
-
Intersection exists iff there is a single (state, action) satisfying
|
|
160
|
-
both scenarios' given/when predicates.
|
|
161
|
-
"""
|
|
162
|
-
|
|
163
|
-
inst_name = f"__speclogician_intersect_{sc_a.name}_{sc_b.name}"
|
|
164
|
-
|
|
165
|
-
# 1) Fast local validation
|
|
166
|
-
domain_model.assert_has_predicates(given=sc_a.given, when=sc_a.when)
|
|
167
|
-
domain_model.assert_has_predicates(given=sc_b.given, when=sc_b.when)
|
|
168
|
-
|
|
169
|
-
# 2) Combine predicates
|
|
170
|
-
given = list(set(list(sc_a.given) + list(sc_b.given)))
|
|
171
|
-
when = list(set(list(sc_a.when) + list(sc_b.when)))
|
|
172
|
-
|
|
173
|
-
# 3) Build minimized IML model
|
|
174
|
-
d_model = domain_model.make_iml_model(
|
|
175
|
-
state_pred_names=given,
|
|
176
|
-
action_pred_names=when,
|
|
177
|
-
transition_names=[],
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
eval_res = await self.c.eval_model(src=d_model)
|
|
181
|
-
if bool(getattr(eval_res, "has_errors", False)):
|
|
182
|
-
errs = getattr(eval_res, "errors", None)
|
|
183
|
-
if errs:
|
|
184
|
-
raise ValueError(f"Domain model has errors (e.g. {errs[0]})")
|
|
185
|
-
raise ValueError("Domain model has errors")
|
|
186
|
-
|
|
187
|
-
# 4) Predicates-only instance
|
|
188
|
-
inst_src = mk_predicates_only_instance_src(
|
|
189
|
-
name=inst_name,
|
|
190
|
-
given=given,
|
|
191
|
-
when=when,
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
res: InstanceRes = await self.c.instance_src(src=inst_src)
|
|
195
|
-
|
|
196
|
-
return ScenarioIntersection (
|
|
197
|
-
scenario1_name = sc_a.name,
|
|
198
|
-
scenario2_name = sc_b.name,
|
|
199
|
-
do_they_intersect = res.sat is not None,
|
|
200
|
-
model_src = res.sat.model.src if res.sat is not None and res.sat.model is not None else None
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
async def decompose_goal_function(
|
|
204
|
-
self,
|
|
205
|
-
*,
|
|
206
|
-
domain_model: DomainModel,
|
|
207
|
-
goal_name: str,
|
|
208
|
-
goal_def_src: str,
|
|
209
|
-
state_pred_names: list[str],
|
|
210
|
-
action_pred_names: list[str],
|
|
211
|
-
assuming: str | None = None,
|
|
212
|
-
rule_specs: list[str] | None = None,
|
|
213
|
-
ctx_simp: bool | None = None,
|
|
214
|
-
lift_bool: str | None = None,
|
|
215
|
-
filter_out_false : bool = True
|
|
216
|
-
) -> ScenarioComplement:
|
|
217
|
-
|
|
218
|
-
base_src = domain_model.make_iml_model(
|
|
219
|
-
state_pred_names=state_pred_names,
|
|
220
|
-
action_pred_names=action_pred_names,
|
|
221
|
-
transition_names=[],
|
|
222
|
-
)
|
|
223
|
-
full_src = base_src + "\n\n" + goal_def_src
|
|
224
|
-
|
|
225
|
-
eval_res = await self.c.eval_model(src=full_src)
|
|
226
|
-
|
|
227
|
-
#print ("THE FULL MODEL is ")
|
|
228
|
-
#print (full_src)
|
|
229
|
-
#print ("THE FULL MODEL is ")
|
|
230
|
-
|
|
231
|
-
if bool(getattr(eval_res, "has_errors", False)):
|
|
232
|
-
errs = getattr(eval_res, "errors", None)
|
|
233
|
-
if errs:
|
|
234
|
-
raise ValueError(f"Domain model has errors (e.g. {errs[0]})")
|
|
235
|
-
raise ValueError("Domain model has errors")
|
|
236
|
-
|
|
237
|
-
req: DecompReqArgs = {
|
|
238
|
-
"name": goal_name,
|
|
239
|
-
"assuming": assuming,
|
|
240
|
-
"rule_specs": rule_specs,
|
|
241
|
-
"prune": True,
|
|
242
|
-
"ctx_simp": ctx_simp,
|
|
243
|
-
"lift_bool": lift_bool,
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
res = await self.c.decompose(**req)
|
|
247
|
-
|
|
248
|
-
if res.regions_str:
|
|
249
|
-
regions_str = list(filter (lambda x: x.model_eval_str != ['true', 'false'][int(filter_out_false)], res.regions_str))
|
|
250
|
-
else:
|
|
251
|
-
regions_str = []
|
|
252
|
-
|
|
253
|
-
return ScenarioComplement(regions=regions_str)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
async def decompose_intersection (
|
|
257
|
-
self,
|
|
258
|
-
*,
|
|
259
|
-
domain_model: DomainModel,
|
|
260
|
-
sc1: Scenario,
|
|
261
|
-
sc2: Scenario,
|
|
262
|
-
ctx_simp: bool | None = None,
|
|
263
|
-
lift_bool: str | None = None,
|
|
264
|
-
) -> ScenarioComplement:
|
|
265
|
-
domain_model.assert_has_predicates(given=sc1.given, when=sc1.when)
|
|
266
|
-
domain_model.assert_has_predicates(given=sc2.given, when=sc2.when)
|
|
267
|
-
|
|
268
|
-
state_names, action_names = collect_pred_names([sc1, sc2])
|
|
269
|
-
needs_action = bool(action_names)
|
|
270
|
-
|
|
271
|
-
goal_name = f"__decomp_intersect_{sc1.name}_{sc2.name}"
|
|
272
|
-
goal_expr = f"({mk_scenario_expr(sc1)}) && ({mk_scenario_expr(sc2)})"
|
|
273
|
-
goal_def = mk_goal_def(goal_name, goal_expr, needs_action=needs_action)
|
|
274
|
-
|
|
275
|
-
res = await self.decompose_goal_function(
|
|
276
|
-
domain_model=domain_model,
|
|
277
|
-
goal_name=goal_name,
|
|
278
|
-
goal_def_src=goal_def,
|
|
279
|
-
state_pred_names=state_names,
|
|
280
|
-
action_pred_names=action_names,
|
|
281
|
-
ctx_simp=ctx_simp,
|
|
282
|
-
lift_bool=lift_bool,
|
|
283
|
-
filter_out_false=True
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
return res
|
|
287
|
-
|
|
288
|
-
async def decompose_spec_complement (
|
|
289
|
-
self,
|
|
290
|
-
*,
|
|
291
|
-
domain_model: DomainModel,
|
|
292
|
-
scenarios: Sequence[Scenario],
|
|
293
|
-
ctx_simp: bool | None = None,
|
|
294
|
-
lift_bool: str | None = None,
|
|
295
|
-
) -> ScenarioComplement:
|
|
296
|
-
for sc in scenarios:
|
|
297
|
-
domain_model.assert_has_predicates(given=sc.given, when=sc.when)
|
|
298
|
-
|
|
299
|
-
state_names, action_names = collect_pred_names(scenarios)
|
|
300
|
-
needs_action = bool(action_names)
|
|
301
|
-
|
|
302
|
-
goal_name = "__decomp_spec_complement"
|
|
303
|
-
spec_expr = mk_spec_disjunction_expr(scenarios)
|
|
304
|
-
goal_expr = f"not ({spec_expr})"
|
|
305
|
-
goal_def = mk_goal_def(goal_name, goal_expr, needs_action=needs_action)
|
|
306
|
-
|
|
307
|
-
#print ("decomp <><>><>")
|
|
308
|
-
#print (domain_model)
|
|
309
|
-
#print ("<><><>")
|
|
310
|
-
#print ("goal name: " + goal_name)
|
|
311
|
-
#print ("decomp <><>><>")
|
|
312
|
-
|
|
313
|
-
return await self.decompose_goal_function(
|
|
314
|
-
domain_model=domain_model,
|
|
315
|
-
goal_name=goal_name,
|
|
316
|
-
goal_def_src=goal_def,
|
|
317
|
-
state_pred_names=state_names,
|
|
318
|
-
action_pred_names=action_names,
|
|
319
|
-
ctx_simp=True,
|
|
320
|
-
lift_bool=lift_bool,
|
|
321
|
-
filter_out_false=True
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
async def check_trace_iml_validity(self, tr: TraceArtifact, model: DomainModel) -> TraceIMLValidity:
|
|
326
|
-
errs: list[str] = []
|
|
327
|
-
|
|
328
|
-
base_src = model.make_iml_model([], [], [])
|
|
329
|
-
|
|
330
|
-
bindings: list[str] = []
|
|
331
|
-
if not tr.given.strip():
|
|
332
|
-
errs.append("Trace missing `given` state value")
|
|
333
|
-
else:
|
|
334
|
-
bindings.append(f"let s : state = {tr.given}")
|
|
335
|
-
|
|
336
|
-
if tr.when is not None and tr.when.strip():
|
|
337
|
-
bindings.append(f"let a : action = {tr.when}")
|
|
338
|
-
|
|
339
|
-
if tr.then is not None and tr.then.strip():
|
|
340
|
-
bindings.append(f"let s_then : state = {tr.then}")
|
|
341
|
-
|
|
342
|
-
if errs:
|
|
343
|
-
return TraceIMLValidity(iml_valid=IMLValidity.INVALID, err="\n".join(errs))
|
|
344
|
-
|
|
345
|
-
full_src = base_src + "\n\n" + "\n".join(bindings)
|
|
346
|
-
|
|
347
|
-
try:
|
|
348
|
-
eval_res = await self.c.eval_model(src=full_src)
|
|
349
|
-
except Exception as e:
|
|
350
|
-
# Could be network/credentials/etc — we truly don't know IML validity.
|
|
351
|
-
return TraceIMLValidity(iml_valid=IMLValidity.UNKNOWN, err=str(e))
|
|
352
|
-
|
|
353
|
-
if bool(getattr(eval_res, "has_errors", False)):
|
|
354
|
-
es = getattr(eval_res, "errors", None) or []
|
|
355
|
-
msg = "\n".join(str(e) for e in es) if es else "ImandraX eval_model: has_errors=True"
|
|
356
|
-
return TraceIMLValidity(iml_valid=IMLValidity.INVALID, err=msg)
|
|
357
|
-
|
|
358
|
-
return TraceIMLValidity(iml_valid=IMLValidity.VALID, err=None)
|
|
359
|
-
|
|
360
|
-
async def check_trace_match(self, tr: TraceArtifact, s: Scenario, model: DomainModel) -> TraceScenarioMatchResult:
|
|
361
|
-
errs: list[str] = []
|
|
362
|
-
|
|
363
|
-
# 0) Typecheck everything first (fast fail)
|
|
364
|
-
v = await self.check_trace_iml_validity(tr, model)
|
|
365
|
-
if v.iml_valid != IMLValidity.VALID:
|
|
366
|
-
return TraceScenarioMatchResult(
|
|
367
|
-
scenario_name=s.name,
|
|
368
|
-
iml_valid=False,
|
|
369
|
-
err=v.err,
|
|
370
|
-
given_matches=False,
|
|
371
|
-
when_matches=False,
|
|
372
|
-
transition_matches=False,
|
|
373
|
-
)
|
|
374
|
-
|
|
375
|
-
# 1) Build model with scenario deps
|
|
376
|
-
dm = model
|
|
377
|
-
dm.assert_has_predicates(given=s.given, when=s.when)
|
|
378
|
-
dm.assert_has_transitions(then=s.then)
|
|
379
|
-
|
|
380
|
-
base_src = dm.make_iml_model(
|
|
381
|
-
state_pred_names=list(s.given),
|
|
382
|
-
action_pred_names=list(s.when),
|
|
383
|
-
transition_names=list(s.then),
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
bindings: list[str] = [f"let s : state = {tr.given}"]
|
|
387
|
-
if tr.when is not None and tr.when.strip():
|
|
388
|
-
bindings.append(f"let a : action = {tr.when}")
|
|
389
|
-
if tr.then is not None and tr.then.strip():
|
|
390
|
-
bindings.append(f"let s_then : state = {tr.then}")
|
|
391
|
-
|
|
392
|
-
ctx = base_src + "\n\n" + "\n".join(bindings)
|
|
393
|
-
|
|
394
|
-
async def eval_bool(expr: str) -> tuple[bool, str | None]:
|
|
395
|
-
import os
|
|
396
|
-
import imandrax_api
|
|
397
|
-
imandrax_api_key = os.getenv("IMANDRA_UNI_KEY")
|
|
398
|
-
url = imandrax_api.url_prod
|
|
399
|
-
|
|
400
|
-
async with imandrax_api.AsyncClient(
|
|
401
|
-
url=url, auth_token=imandrax_api_key, timeout=300
|
|
402
|
-
) as ac:
|
|
403
|
-
src = ctx + "\n\n" + f"eval({expr})"
|
|
404
|
-
|
|
405
|
-
res = await ac.eval_src(src=src)
|
|
406
|
-
|
|
407
|
-
from imandrax_api.lib import read_artifact_zip
|
|
408
|
-
|
|
409
|
-
art_zip = await ac.get_artifact_zip(task=res.tasks[0], kind="eval_res")
|
|
410
|
-
with tempfile.NamedTemporaryFile(suffix=".zip", delete=True) as f:
|
|
411
|
-
f.write(art_zip.art_zip)
|
|
412
|
-
f.flush()
|
|
413
|
-
eval_res = read_artifact_zip(f.name)
|
|
414
|
-
|
|
415
|
-
if str(eval_res.res.v.v) == 'Eval_Value_view_V_true()':
|
|
416
|
-
return True, None
|
|
417
|
-
elif str(eval_res.res.v.v) == 'Eval_Value_view_V_false()':
|
|
418
|
-
return False, None
|
|
419
|
-
else:
|
|
420
|
-
return False, f"Result:{eval_res.res.v.v}"
|
|
421
|
-
|
|
422
|
-
#print("The result is" + str(eval_res.res.v.v))
|
|
423
|
-
#
|
|
424
|
-
#print ("The result is")
|
|
425
|
-
#print (res)
|
|
426
|
-
|
|
427
|
-
#import sys
|
|
428
|
-
#sys.exit(0)
|
|
429
|
-
|
|
430
|
-
#evs = getattr(res, "eval_results", None) or []
|
|
431
|
-
#if not evs or not getattr(evs[0], "success", False):
|
|
432
|
-
# return False, f"eval failed for: {expr}"
|
|
433
|
-
#v = (getattr(evs[0], "value_as_ocaml", "") or "").strip()
|
|
434
|
-
#if v == "true":
|
|
435
|
-
# return True, None
|
|
436
|
-
#if v == "false":
|
|
437
|
-
# return False, None
|
|
438
|
-
#return False, f"eval returned non-bool: {v!r} for {expr}"
|
|
439
|
-
|
|
440
|
-
# 2) given_matches
|
|
441
|
-
if list(s.given):
|
|
442
|
-
given_expr = " && ".join(f"({p} s)" for p in s.given)
|
|
443
|
-
given_ok, given_err = await eval_bool(given_expr)
|
|
444
|
-
if given_err:
|
|
445
|
-
errs.append(given_err)
|
|
446
|
-
else:
|
|
447
|
-
given_ok = True
|
|
448
|
-
|
|
449
|
-
# 3) when_matches
|
|
450
|
-
if list(s.when):
|
|
451
|
-
if not (tr.when is not None and tr.when.strip()):
|
|
452
|
-
when_ok = False
|
|
453
|
-
errs.append("Scenario has action predicates but trace has no `when` action value")
|
|
454
|
-
else:
|
|
455
|
-
when_expr = " && ".join(f"({p} s a)" for p in s.when)
|
|
456
|
-
when_ok, when_err = await eval_bool(when_expr)
|
|
457
|
-
if when_err:
|
|
458
|
-
errs.append(when_err)
|
|
459
|
-
else:
|
|
460
|
-
when_ok = True
|
|
461
|
-
|
|
462
|
-
# 4) transition_matches
|
|
463
|
-
# Semantics:
|
|
464
|
-
# - If scenario has no transitions, transition_matches is True (no then required).
|
|
465
|
-
# - If scenario has transitions, we need:
|
|
466
|
-
# - trace has when (single-action mode)
|
|
467
|
-
# - trace has then (state) to compare to
|
|
468
|
-
# - apply transitions sequentially: s |> t1 a |> t2 a |> ... and compare to s_then
|
|
469
|
-
if not list(s.then):
|
|
470
|
-
# Scenario does not specify any transitions: nothing to check.
|
|
471
|
-
trans_ok = True
|
|
472
|
-
else:
|
|
473
|
-
# Scenario specifies transitions: we need action.
|
|
474
|
-
if not (tr.when is not None and tr.when.strip()):
|
|
475
|
-
trans_ok = False
|
|
476
|
-
else:
|
|
477
|
-
trans_names = list(s.then)
|
|
478
|
-
|
|
479
|
-
# Build a sequential application expression.
|
|
480
|
-
# Example for [t1, t2, t3]:
|
|
481
|
-
# let s1 = t1 s a in let s2 = t2 s1 a in let s3 = t3 s2 a in ...
|
|
482
|
-
if len(trans_names) == 1:
|
|
483
|
-
final_state_expr = f"({trans_names[0]} s a)"
|
|
484
|
-
else:
|
|
485
|
-
lets: list[str] = []
|
|
486
|
-
prev = "s"
|
|
487
|
-
for i, t in enumerate(trans_names, 1):
|
|
488
|
-
si = f"s{i}"
|
|
489
|
-
lets.append(f"let {si} = ({t} {prev} a) in")
|
|
490
|
-
prev = si
|
|
491
|
-
final_state_expr = " ".join(lets) + f" {prev}"
|
|
492
|
-
|
|
493
|
-
if not (tr.then is not None and tr.then.strip()):
|
|
494
|
-
# then is optional: skip equality check, but still force typecheck/eval
|
|
495
|
-
trans_ok, trans_err = await eval_bool(f"let _ = {final_state_expr} in true")
|
|
496
|
-
else:
|
|
497
|
-
trans_expr = f"({final_state_expr} = s_then)"
|
|
498
|
-
trans_ok, trans_err = await eval_bool(trans_expr)
|
|
499
|
-
|
|
500
|
-
# IMPORTANT: do NOT append anything when trans_ok is False.
|
|
501
|
-
if trans_err:
|
|
502
|
-
errs.append(trans_err)
|
|
503
|
-
|
|
504
|
-
ok_err = "\n".join(errs) if errs else None
|
|
505
|
-
return TraceScenarioMatchResult(
|
|
506
|
-
scenario_name=s.name,
|
|
507
|
-
iml_valid=True,
|
|
508
|
-
err=ok_err,
|
|
509
|
-
when_matches=when_ok,
|
|
510
|
-
given_matches=given_ok,
|
|
511
|
-
transition_matches=trans_ok,
|
|
512
|
-
)
|
|
513
|
-
|
|
514
|
-
async def check_scenario_implies(
|
|
515
|
-
self,
|
|
516
|
-
domain_model: DomainModel,
|
|
517
|
-
*,
|
|
518
|
-
sc_a: Scenario, # A (more general)
|
|
519
|
-
sc_b: Scenario, # B (more specific)
|
|
520
|
-
) -> ScenarioConsumed | None:
|
|
521
|
-
"""
|
|
522
|
-
Return ScenarioConsumed(A,B) iff B ⇒ A (i.e. scenario B is a subset of scenario A).
|
|
523
|
-
Otherwise return None.
|
|
524
|
-
|
|
525
|
-
We check satisfiability of: (B && not A)
|
|
526
|
-
- UNSAT (no witness) => B ⇒ A => consumed(A,B)
|
|
527
|
-
- SAT (witness) => not implied => None
|
|
528
|
-
|
|
529
|
-
IMPORTANT: ImandraX usage here is:
|
|
530
|
-
1) eval_model(src=<full model that *defines* the named query>)
|
|
531
|
-
2) instance_src(src="<query_name>") # NOT source; just the name
|
|
532
|
-
"""
|
|
533
|
-
|
|
534
|
-
# 1) Fast local validation
|
|
535
|
-
domain_model.assert_has_predicates(given=sc_a.given, when=sc_a.when)
|
|
536
|
-
domain_model.assert_has_predicates(given=sc_b.given, when=sc_b.when)
|
|
537
|
-
|
|
538
|
-
# 2) Build minimal model with union of predicates used by either scenario
|
|
539
|
-
given = sorted(set(list(sc_a.given) + list(sc_b.given)))
|
|
540
|
-
when = sorted(set(list(sc_a.when) + list(sc_b.when)))
|
|
541
|
-
|
|
542
|
-
d_model = domain_model.make_iml_model(
|
|
543
|
-
state_pred_names=given,
|
|
544
|
-
action_pred_names=when,
|
|
545
|
-
transition_names=[],
|
|
546
|
-
)
|
|
547
|
-
|
|
548
|
-
# 3) Build counterexample expression: B && not(A)
|
|
549
|
-
a_expr = mk_scenario_expr(sc_a)
|
|
550
|
-
b_expr = mk_scenario_expr(sc_b)
|
|
551
|
-
ce_expr = f"({b_expr}) && (not ({a_expr}))"
|
|
552
|
-
needs_action = bool(when)
|
|
553
|
-
|
|
554
|
-
# 4) Define a NAMED boolean query inside the model context
|
|
555
|
-
# Ensure name is a valid identifier (avoid '-' etc.)
|
|
556
|
-
qname = f"__speclogician_implies_{sc_a.name}_{sc_b.name}".replace("-", "_")
|
|
557
|
-
|
|
558
|
-
# This helper must emit a *function definition*, not an `instance { ... }` block.
|
|
559
|
-
# Example shapes:
|
|
560
|
-
# let q (s: state) : bool = <expr>
|
|
561
|
-
# let q (s: state) (a: action) : bool = <expr>
|
|
562
|
-
|
|
563
|
-
def mk_bool_query_def(*, name: str, expr: str, needs_action: bool) -> str:
|
|
564
|
-
if needs_action:
|
|
565
|
-
return f"""
|
|
566
|
-
let {name} (s: state) (a: action) : bool =
|
|
567
|
-
({expr})
|
|
568
|
-
"""
|
|
569
|
-
return f"""
|
|
570
|
-
let {name} (s: state) : bool =
|
|
571
|
-
({expr})
|
|
572
|
-
"""
|
|
573
|
-
|
|
574
|
-
query_def = mk_bool_query_def(
|
|
575
|
-
name=qname,
|
|
576
|
-
expr=ce_expr,
|
|
577
|
-
needs_action=needs_action,
|
|
578
|
-
)
|
|
579
|
-
|
|
580
|
-
full_src = d_model + "\n\n" + query_def
|
|
581
|
-
|
|
582
|
-
# 5) eval_model the full program (model + query)
|
|
583
|
-
eval_res = await self.c.eval_model(src=full_src)
|
|
584
|
-
if bool(getattr(eval_res, "has_errors", False)):
|
|
585
|
-
errs = getattr(eval_res, "errors", None)
|
|
586
|
-
if errs:
|
|
587
|
-
raise ValueError(f"Domain model has errors (e.g. {errs[0]})")
|
|
588
|
-
raise ValueError("Domain model has errors")
|
|
589
|
-
|
|
590
|
-
# 6) instance_src just by NAME
|
|
591
|
-
res: InstanceRes = await self.c.instance_src(src=qname)
|
|
592
|
-
|
|
593
|
-
# SAT => counterexample exists => not consumed
|
|
594
|
-
if res.sat is not None:
|
|
595
|
-
return None
|
|
596
|
-
|
|
597
|
-
# UNSAT => implication holds => consumed
|
|
598
|
-
details = (
|
|
599
|
-
"Scenario is consumed: every state/action satisfying scenario2 also satisfies scenario1.\n"
|
|
600
|
-
"Checked UNSAT of: scenario2 && not(scenario1)."
|
|
601
|
-
)
|
|
602
|
-
return ScenarioConsumed(
|
|
603
|
-
scenario_name1=sc_a.name, # A (general)
|
|
604
|
-
scenario_name2=sc_b.name, # B (specific)
|
|
605
|
-
details=details,
|
|
606
|
-
)
|
speclogician/logic/api/decomp.py
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Imandra Inc.
|
|
3
|
-
#
|
|
4
|
-
# speclogician/logic/api/complement.py
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
import asyncio
|
|
8
|
-
from imandrax_api_models import DecomposeRes
|
|
9
|
-
|
|
10
|
-
from .client import SpecLogicianImandraX
|
|
11
|
-
from speclogician.modeling.spec import Spec
|
|
12
|
-
from speclogician.modeling.scenario import Scenario
|
|
13
|
-
from speclogician.modeling.complement import ScenarioComplement
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def assert_decomp_ok(
|
|
17
|
-
res: DecomposeRes,
|
|
18
|
-
*,
|
|
19
|
-
ctx: str = "decompose",
|
|
20
|
-
require_regions: bool = True,
|
|
21
|
-
) -> None:
|
|
22
|
-
if getattr(res, "errors", None):
|
|
23
|
-
raise AssertionError(f"{ctx}: decomposition errors: {res.errors}")
|
|
24
|
-
|
|
25
|
-
if res.regions_str is None:
|
|
26
|
-
raise AssertionError(f"{ctx}: decomposition produced no regions (regions_str is None)")
|
|
27
|
-
|
|
28
|
-
if require_regions and len(res.regions_str) == 0:
|
|
29
|
-
raise AssertionError(f"{ctx}: decomposition returned zero regions")
|
|
30
|
-
|
|
31
|
-
async def decomp_intersection_async(sc1: Scenario, sc2: Scenario, spec: Spec) -> ScenarioComplement:
|
|
32
|
-
"""
|
|
33
|
-
Decompose intersection of two scenarios and assert it succeeded.
|
|
34
|
-
"""
|
|
35
|
-
async with SpecLogicianImandraX() as ix:
|
|
36
|
-
res = await ix.decompose_intersection(
|
|
37
|
-
domain_model=spec.domain_model,
|
|
38
|
-
sc1=sc1,
|
|
39
|
-
sc2=sc2,
|
|
40
|
-
)
|
|
41
|
-
return res
|
|
42
|
-
|
|
43
|
-
def decomp_intersection(sc1: Scenario, sc2: Scenario, spec: Spec) -> ScenarioComplement:
|
|
44
|
-
"""
|
|
45
|
-
Sync wrapper around decomp_intersection_async.
|
|
46
|
-
"""
|
|
47
|
-
return asyncio.run(decomp_intersection_async(sc1, sc2, spec))
|
|
48
|
-
|
|
49
|
-
async def decomp_complement_async(spec: Spec) -> ScenarioComplement:
|
|
50
|
-
"""
|
|
51
|
-
Decompose complement of all scenarios in the spec and assert it succeeded.
|
|
52
|
-
"""
|
|
53
|
-
scenarios = list(spec.scenarios)
|
|
54
|
-
|
|
55
|
-
async with SpecLogicianImandraX() as ix:
|
|
56
|
-
res = await ix.decompose_spec_complement(
|
|
57
|
-
domain_model=spec.domain_model,
|
|
58
|
-
scenarios=scenarios,
|
|
59
|
-
)
|
|
60
|
-
return res
|
|
61
|
-
|
|
62
|
-
def decomp_complement(spec: Spec) -> ScenarioComplement:
|
|
63
|
-
"""
|
|
64
|
-
Sync wrapper around decomp_complement_async.
|
|
65
|
-
"""
|
|
66
|
-
return asyncio.run(decomp_complement_async(spec))
|
|
67
|
-
|