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.
Files changed (153) hide show
  1. speclogician/agent/funcs.py +29 -0
  2. speclogician/cmd/agent_cmd.py +89 -0
  3. speclogician/cmd/data_cmd.py +24 -0
  4. speclogician/cmd/model_cmd.py +42 -0
  5. speclogician/cmd/overlay_cmd.py +30 -0
  6. speclogician/cmd/scenario_cmd.py +61 -0
  7. speclogician/cmd/state_cmd.py +52 -0
  8. speclogician/data/artifact.py +8 -50
  9. speclogician/data/container.py +18 -384
  10. speclogician/data/mapping.py +18 -17
  11. speclogician/data/refs.py +12 -11
  12. speclogician/data/reports.py +11 -0
  13. speclogician/data/traces.py +15 -6
  14. speclogician/llms/llmtools.py +102 -0
  15. speclogician/llms/overlay.py +264 -0
  16. speclogician/main.py +36 -102
  17. speclogician/modeling/__init__.py +0 -31
  18. speclogician/modeling/component.py +4 -60
  19. speclogician/modeling/conflict.py +5 -19
  20. speclogician/modeling/domain.py +93 -280
  21. speclogician/modeling/model.py +206 -0
  22. speclogician/modeling/predicates.py +20 -22
  23. speclogician/modeling/report.py +33 -0
  24. speclogician/modeling/scenario.py +119 -87
  25. speclogician/sl_cmd.py +76 -0
  26. speclogician/state/change.py +98 -378
  27. speclogician/state/state.py +183 -399
  28. speclogician/tui/box.tcss +10 -0
  29. speclogician/tui/tui.py +131 -0
  30. speclogician/utils/__init__.py +1 -70
  31. speclogician/utils/imx.py +195 -0
  32. speclogician/utils/load.py +25 -147
  33. speclogician/utils/prompt.md +1 -325
  34. speclogician-0.0.0.dev1.dist-info/METADATA +21 -0
  35. speclogician-0.0.0.dev1.dist-info/RECORD +43 -0
  36. speclogician/commands/__init__.py +0 -15
  37. speclogician/commands/cmd_ch.py +0 -616
  38. speclogician/commands/cmd_find.py +0 -256
  39. speclogician/commands/cmd_view.py +0 -202
  40. speclogician/commands/runner.py +0 -149
  41. speclogician/commands/utils.py +0 -101
  42. speclogician/demos/.DS_Store +0 -0
  43. speclogician/demos/cmd_demo.py +0 -278
  44. speclogician/demos/loader.py +0 -135
  45. speclogician/demos/model.py +0 -27
  46. speclogician/demos/runner.py +0 -51
  47. speclogician/logic/__init__.py +0 -11
  48. speclogician/logic/api/__init__.py +0 -29
  49. speclogician/logic/api/client.py +0 -606
  50. speclogician/logic/api/decomp.py +0 -67
  51. speclogician/logic/api/scenario.py +0 -102
  52. speclogician/logic/api/traces.py +0 -59
  53. speclogician/logic/lib/__init__.py +0 -19
  54. speclogician/logic/lib/complement.py +0 -107
  55. speclogician/logic/lib/domain_model.py +0 -59
  56. speclogician/logic/lib/predicates.py +0 -151
  57. speclogician/logic/lib/scenarios.py +0 -369
  58. speclogician/logic/lib/traces.py +0 -114
  59. speclogician/logic/lib/transitions.py +0 -104
  60. speclogician/logic/main.py +0 -246
  61. speclogician/logic/strings.py +0 -194
  62. speclogician/logic/utils.py +0 -135
  63. speclogician/modeling/complement.py +0 -104
  64. speclogician/modeling/spec.py +0 -306
  65. speclogician/modeling/spec_stats.py +0 -39
  66. speclogician/presentation/api.py +0 -244
  67. speclogician/presentation/builders/_links.py +0 -44
  68. speclogician/presentation/builders/container.py +0 -53
  69. speclogician/presentation/builders/data_artifact.py +0 -42
  70. speclogician/presentation/builders/domain.py +0 -54
  71. speclogician/presentation/builders/instances_list.py +0 -38
  72. speclogician/presentation/builders/predicate.py +0 -51
  73. speclogician/presentation/builders/recommendations.py +0 -41
  74. speclogician/presentation/builders/scenario.py +0 -41
  75. speclogician/presentation/builders/scenario_complement.py +0 -82
  76. speclogician/presentation/builders/smart_find.py +0 -39
  77. speclogician/presentation/builders/spec.py +0 -39
  78. speclogician/presentation/builders/state_diff.py +0 -150
  79. speclogician/presentation/builders/state_instance.py +0 -42
  80. speclogician/presentation/builders/state_instance_summary.py +0 -84
  81. speclogician/presentation/builders/trace.py +0 -58
  82. speclogician/presentation/ctx.py +0 -38
  83. speclogician/presentation/models/container.py +0 -44
  84. speclogician/presentation/models/data_artifact.py +0 -33
  85. speclogician/presentation/models/domain.py +0 -50
  86. speclogician/presentation/models/instances_list.py +0 -23
  87. speclogician/presentation/models/predicate.py +0 -60
  88. speclogician/presentation/models/recommendations.py +0 -34
  89. speclogician/presentation/models/scenario.py +0 -31
  90. speclogician/presentation/models/scenario_complement.py +0 -40
  91. speclogician/presentation/models/smart_find.py +0 -34
  92. speclogician/presentation/models/spec.py +0 -32
  93. speclogician/presentation/models/state_diff.py +0 -34
  94. speclogician/presentation/models/state_instance.py +0 -31
  95. speclogician/presentation/models/state_instance_summary.py +0 -102
  96. speclogician/presentation/models/trace.py +0 -42
  97. speclogician/presentation/preview/__init__.py +0 -13
  98. speclogician/presentation/preview/cli.py +0 -50
  99. speclogician/presentation/preview/fixtures/__init__.py +0 -205
  100. speclogician/presentation/preview/fixtures/artifact_container.py +0 -150
  101. speclogician/presentation/preview/fixtures/data_artifact.py +0 -144
  102. speclogician/presentation/preview/fixtures/domain_model.py +0 -162
  103. speclogician/presentation/preview/fixtures/instances_list.py +0 -162
  104. speclogician/presentation/preview/fixtures/predicate.py +0 -184
  105. speclogician/presentation/preview/fixtures/scenario.py +0 -84
  106. speclogician/presentation/preview/fixtures/scenario_complement.py +0 -81
  107. speclogician/presentation/preview/fixtures/smart_find.py +0 -140
  108. speclogician/presentation/preview/fixtures/spec.py +0 -95
  109. speclogician/presentation/preview/fixtures/state_diff.py +0 -158
  110. speclogician/presentation/preview/fixtures/state_instance.py +0 -128
  111. speclogician/presentation/preview/fixtures/state_instance_summary.py +0 -80
  112. speclogician/presentation/preview/fixtures/trace.py +0 -206
  113. speclogician/presentation/preview/registry.py +0 -42
  114. speclogician/presentation/renderers/__init__.py +0 -24
  115. speclogician/presentation/renderers/container.py +0 -136
  116. speclogician/presentation/renderers/data_artifact.py +0 -144
  117. speclogician/presentation/renderers/domain.py +0 -123
  118. speclogician/presentation/renderers/instances_list.py +0 -120
  119. speclogician/presentation/renderers/predicate.py +0 -180
  120. speclogician/presentation/renderers/recommendations.py +0 -90
  121. speclogician/presentation/renderers/scenario.py +0 -94
  122. speclogician/presentation/renderers/scenario_complement.py +0 -59
  123. speclogician/presentation/renderers/smart_find.py +0 -307
  124. speclogician/presentation/renderers/spec.py +0 -105
  125. speclogician/presentation/renderers/state_diff.py +0 -102
  126. speclogician/presentation/renderers/state_instance.py +0 -82
  127. speclogician/presentation/renderers/state_instance_summary.py +0 -143
  128. speclogician/presentation/renderers/trace.py +0 -122
  129. speclogician/shell/app.py +0 -170
  130. speclogician/shell/shell_ch.py +0 -263
  131. speclogician/shell/shell_view.py +0 -153
  132. speclogician/state/change_result.py +0 -32
  133. speclogician/state/diff.py +0 -191
  134. speclogician/state/inst.py +0 -574
  135. speclogician/state/recommendation.py +0 -13
  136. speclogician/state/recommender.py +0 -577
  137. speclogician/state/state_stats.py +0 -133
  138. speclogician/tui/__init__.py +0 -0
  139. speclogician/tui/app.py +0 -257
  140. speclogician/tui/app.tcss +0 -160
  141. speclogician/tui/demo.py +0 -45
  142. speclogician/tui/images/speclogician-full.png +0 -0
  143. speclogician/tui/images/speclogician-minimal.png +0 -0
  144. speclogician/tui/main_screen.py +0 -454
  145. speclogician/tui/splash_screen.py +0 -51
  146. speclogician/tui/stats_screen.py +0 -125
  147. speclogician/utils/testing.py +0 -151
  148. speclogician-0.0.0b1.dist-info/METADATA +0 -116
  149. speclogician-0.0.0b1.dist-info/RECORD +0 -139
  150. /speclogician/{presentation → agent}/__init__.py +0 -0
  151. /speclogician/{presentation/builders → cmd}/__init__.py +0 -0
  152. /speclogician/{presentation/models → llms}/__init__.py +0 -0
  153. {speclogician-0.0.0b1.dist-info → speclogician-0.0.0.dev1.dist-info}/WHEEL +0 -0
@@ -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
- )
@@ -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
-