speclogician 0.0.0b1__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 (139) hide show
  1. speclogician/__init__.py +0 -0
  2. speclogician/commands/__init__.py +15 -0
  3. speclogician/commands/cmd_ch.py +616 -0
  4. speclogician/commands/cmd_find.py +256 -0
  5. speclogician/commands/cmd_view.py +202 -0
  6. speclogician/commands/runner.py +149 -0
  7. speclogician/commands/utils.py +101 -0
  8. speclogician/data/__init__.py +0 -0
  9. speclogician/data/artifact.py +63 -0
  10. speclogician/data/container.py +402 -0
  11. speclogician/data/mapping.py +88 -0
  12. speclogician/data/refs.py +24 -0
  13. speclogician/data/traces.py +26 -0
  14. speclogician/demos/.DS_Store +0 -0
  15. speclogician/demos/cmd_demo.py +278 -0
  16. speclogician/demos/loader.py +135 -0
  17. speclogician/demos/model.py +27 -0
  18. speclogician/demos/runner.py +51 -0
  19. speclogician/logic/__init__.py +11 -0
  20. speclogician/logic/api/__init__.py +29 -0
  21. speclogician/logic/api/client.py +606 -0
  22. speclogician/logic/api/decomp.py +67 -0
  23. speclogician/logic/api/scenario.py +102 -0
  24. speclogician/logic/api/traces.py +59 -0
  25. speclogician/logic/lib/__init__.py +19 -0
  26. speclogician/logic/lib/complement.py +107 -0
  27. speclogician/logic/lib/domain_model.py +59 -0
  28. speclogician/logic/lib/predicates.py +151 -0
  29. speclogician/logic/lib/scenarios.py +369 -0
  30. speclogician/logic/lib/traces.py +114 -0
  31. speclogician/logic/lib/transitions.py +104 -0
  32. speclogician/logic/main.py +246 -0
  33. speclogician/logic/strings.py +194 -0
  34. speclogician/logic/utils.py +135 -0
  35. speclogician/main.py +139 -0
  36. speclogician/modeling/__init__.py +31 -0
  37. speclogician/modeling/complement.py +104 -0
  38. speclogician/modeling/component.py +71 -0
  39. speclogician/modeling/conflict.py +26 -0
  40. speclogician/modeling/domain.py +349 -0
  41. speclogician/modeling/predicates.py +59 -0
  42. speclogician/modeling/scenario.py +162 -0
  43. speclogician/modeling/spec.py +306 -0
  44. speclogician/modeling/spec_stats.py +39 -0
  45. speclogician/presentation/__init__.py +0 -0
  46. speclogician/presentation/api.py +244 -0
  47. speclogician/presentation/builders/__init__.py +0 -0
  48. speclogician/presentation/builders/_links.py +44 -0
  49. speclogician/presentation/builders/container.py +53 -0
  50. speclogician/presentation/builders/data_artifact.py +42 -0
  51. speclogician/presentation/builders/domain.py +54 -0
  52. speclogician/presentation/builders/instances_list.py +38 -0
  53. speclogician/presentation/builders/predicate.py +51 -0
  54. speclogician/presentation/builders/recommendations.py +41 -0
  55. speclogician/presentation/builders/scenario.py +41 -0
  56. speclogician/presentation/builders/scenario_complement.py +82 -0
  57. speclogician/presentation/builders/smart_find.py +39 -0
  58. speclogician/presentation/builders/spec.py +39 -0
  59. speclogician/presentation/builders/state_diff.py +150 -0
  60. speclogician/presentation/builders/state_instance.py +42 -0
  61. speclogician/presentation/builders/state_instance_summary.py +84 -0
  62. speclogician/presentation/builders/trace.py +58 -0
  63. speclogician/presentation/ctx.py +38 -0
  64. speclogician/presentation/models/__init__.py +0 -0
  65. speclogician/presentation/models/container.py +44 -0
  66. speclogician/presentation/models/data_artifact.py +33 -0
  67. speclogician/presentation/models/domain.py +50 -0
  68. speclogician/presentation/models/instances_list.py +23 -0
  69. speclogician/presentation/models/predicate.py +60 -0
  70. speclogician/presentation/models/recommendations.py +34 -0
  71. speclogician/presentation/models/scenario.py +31 -0
  72. speclogician/presentation/models/scenario_complement.py +40 -0
  73. speclogician/presentation/models/smart_find.py +34 -0
  74. speclogician/presentation/models/spec.py +32 -0
  75. speclogician/presentation/models/state_diff.py +34 -0
  76. speclogician/presentation/models/state_instance.py +31 -0
  77. speclogician/presentation/models/state_instance_summary.py +102 -0
  78. speclogician/presentation/models/trace.py +42 -0
  79. speclogician/presentation/preview/__init__.py +13 -0
  80. speclogician/presentation/preview/cli.py +50 -0
  81. speclogician/presentation/preview/fixtures/__init__.py +205 -0
  82. speclogician/presentation/preview/fixtures/artifact_container.py +150 -0
  83. speclogician/presentation/preview/fixtures/data_artifact.py +144 -0
  84. speclogician/presentation/preview/fixtures/domain_model.py +162 -0
  85. speclogician/presentation/preview/fixtures/instances_list.py +162 -0
  86. speclogician/presentation/preview/fixtures/predicate.py +184 -0
  87. speclogician/presentation/preview/fixtures/scenario.py +84 -0
  88. speclogician/presentation/preview/fixtures/scenario_complement.py +81 -0
  89. speclogician/presentation/preview/fixtures/smart_find.py +140 -0
  90. speclogician/presentation/preview/fixtures/spec.py +95 -0
  91. speclogician/presentation/preview/fixtures/state_diff.py +158 -0
  92. speclogician/presentation/preview/fixtures/state_instance.py +128 -0
  93. speclogician/presentation/preview/fixtures/state_instance_summary.py +80 -0
  94. speclogician/presentation/preview/fixtures/trace.py +206 -0
  95. speclogician/presentation/preview/registry.py +42 -0
  96. speclogician/presentation/renderers/__init__.py +24 -0
  97. speclogician/presentation/renderers/container.py +136 -0
  98. speclogician/presentation/renderers/data_artifact.py +144 -0
  99. speclogician/presentation/renderers/domain.py +123 -0
  100. speclogician/presentation/renderers/instances_list.py +120 -0
  101. speclogician/presentation/renderers/predicate.py +180 -0
  102. speclogician/presentation/renderers/recommendations.py +90 -0
  103. speclogician/presentation/renderers/scenario.py +94 -0
  104. speclogician/presentation/renderers/scenario_complement.py +59 -0
  105. speclogician/presentation/renderers/smart_find.py +307 -0
  106. speclogician/presentation/renderers/spec.py +105 -0
  107. speclogician/presentation/renderers/state_diff.py +102 -0
  108. speclogician/presentation/renderers/state_instance.py +82 -0
  109. speclogician/presentation/renderers/state_instance_summary.py +143 -0
  110. speclogician/presentation/renderers/trace.py +122 -0
  111. speclogician/py.typed +0 -0
  112. speclogician/shell/app.py +170 -0
  113. speclogician/shell/shell_ch.py +263 -0
  114. speclogician/shell/shell_view.py +153 -0
  115. speclogician/state/__init__.py +0 -0
  116. speclogician/state/change.py +428 -0
  117. speclogician/state/change_result.py +32 -0
  118. speclogician/state/diff.py +191 -0
  119. speclogician/state/inst.py +574 -0
  120. speclogician/state/recommendation.py +13 -0
  121. speclogician/state/recommender.py +577 -0
  122. speclogician/state/state.py +465 -0
  123. speclogician/state/state_stats.py +133 -0
  124. speclogician/tui/__init__.py +0 -0
  125. speclogician/tui/app.py +257 -0
  126. speclogician/tui/app.tcss +160 -0
  127. speclogician/tui/demo.py +45 -0
  128. speclogician/tui/images/speclogician-full.png +0 -0
  129. speclogician/tui/images/speclogician-minimal.png +0 -0
  130. speclogician/tui/main_screen.py +454 -0
  131. speclogician/tui/splash_screen.py +51 -0
  132. speclogician/tui/stats_screen.py +125 -0
  133. speclogician/utils/__init__.py +78 -0
  134. speclogician/utils/load.py +166 -0
  135. speclogician/utils/prompt.md +325 -0
  136. speclogician/utils/testing.py +151 -0
  137. speclogician-0.0.0b1.dist-info/METADATA +116 -0
  138. speclogician-0.0.0b1.dist-info/RECORD +139 -0
  139. speclogician-0.0.0b1.dist-info/WHEEL +4 -0
@@ -0,0 +1,246 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/logic/main.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ from ..modeling.spec import Spec
10
+ from ..data.container import ArtifactContainer
11
+ from ..data.mapping import ArtifactMap
12
+
13
+ from ..state.change import (
14
+ StateChange,
15
+ DomainModelBaseEdit,
16
+ PredicateAdd,
17
+ PredicateEdit,
18
+ PredicateRemove,
19
+ TransitionAdd,
20
+ TransitionEdit,
21
+ TransitionRemove,
22
+ ScenarioAdd,
23
+ ScenarioEdit,
24
+ ScenarioRemove,
25
+ AddTestTrace,
26
+ EditTestTrace,
27
+ AddLogTrace,
28
+ EditLogTrace
29
+ )
30
+
31
+ from .lib.domain_model import check_domain_model
32
+ from .lib.predicates import check_predicates
33
+ from .lib.transitions import check_transitions
34
+ from .lib.scenarios import check_scenarios
35
+ from .lib.traces import check_traces
36
+ from .lib.complement import maybe_update_spec_complement
37
+
38
+ from ..utils import console
39
+
40
+
41
+ def _hdr(title: str, subtitle: str | None = None, json_only: bool = False) -> None:
42
+ if json_only:
43
+ return
44
+ from rich.panel import Panel
45
+ from rich.text import Text
46
+
47
+ console.print(
48
+ Panel(
49
+ Text(subtitle or ""),
50
+ title=f"[bold]Logician[/bold] · {title}",
51
+ border_style="cyan",
52
+ )
53
+ )
54
+
55
+
56
+ def _note(msg: str, json_only: bool = False) -> None:
57
+ if not json_only:
58
+ console.print(msg)
59
+
60
+ def analyze_change(
61
+ spec: Spec,
62
+ art_cont: ArtifactContainer,
63
+ art_map: ArtifactMap,
64
+ change: StateChange,
65
+ json_only: bool = False,
66
+ ) -> None:
67
+ """
68
+ Top-level entry for logical analysis post-change event.
69
+
70
+ Only performs logic checks when relevant.
71
+ All other change types are treated as no-ops here.
72
+ """
73
+
74
+ # ----------------------------
75
+ # Domain base model changed
76
+ # ----------------------------
77
+ if isinstance(change, DomainModelBaseEdit):
78
+ _hdr("DomainModelBaseEdit", "Rechecking domain model and all dependent logic", json_only=json_only)
79
+ check_domain_model(spec, json_only=json_only)
80
+ check_predicates(spec, json_only=json_only)
81
+ check_transitions(spec, json_only=json_only)
82
+ check_scenarios(spec, json_only=json_only)
83
+ check_traces(spec, art_cont=art_cont, art_map=art_map, traces=[], json_only=json_only)
84
+
85
+ try:
86
+ maybe_update_spec_complement(spec)
87
+ except Exception as e:
88
+ _note(f"[yellow]⚠[/yellow] Complement update failed (non-fatal): {type(e).__name__}: {e}", json_only=json_only)
89
+
90
+ _note("[green]✓[/green] Domain analysis complete", json_only=json_only)
91
+ return
92
+
93
+ # ----------------------------
94
+ # Predicates changed
95
+ # ----------------------------
96
+ if isinstance(change, (PredicateAdd, PredicateEdit)):
97
+ _hdr("Predicate change", f"Predicate: [bold]{change.pred_name}[/bold]", json_only=json_only)
98
+ check_predicates(spec, names=[change.pred_name], json_only=json_only)
99
+ check_scenarios(spec, predicates=[change.pred_name], json_only=json_only)
100
+ check_traces(spec, art_cont=art_cont, art_map=art_map, traces=[], json_only=json_only)
101
+
102
+ try:
103
+ maybe_update_spec_complement(spec)
104
+ except Exception as e:
105
+ _note(f"[yellow]⚠[/yellow] Complement update failed (non-fatal): {type(e).__name__}: {e}", json_only=json_only)
106
+
107
+ _note("[green]✓[/green] Predicate analysis complete", json_only=json_only)
108
+ return
109
+
110
+ if isinstance(change, PredicateRemove):
111
+ _hdr("Predicate removed", f"[bold]{change.pred_name}[/bold]", json_only=json_only)
112
+ check_scenarios(spec, predicates=[change.pred_name], json_only=json_only)
113
+ check_traces(spec, art_cont=art_cont, art_map=art_map, traces=[], json_only=json_only)
114
+
115
+ try:
116
+ maybe_update_spec_complement(spec)
117
+ except Exception as e:
118
+ _note(f"[yellow]⚠[/yellow] Complement update failed (non-fatal): {type(e).__name__}: {e}", json_only=json_only)
119
+
120
+ _note("[green]✓[/green] Scenario refresh complete", json_only=json_only)
121
+ return
122
+
123
+ # ----------------------------
124
+ # Transitions changed
125
+ # ----------------------------
126
+ if isinstance(change, (TransitionAdd, TransitionEdit)):
127
+ _hdr("Transition change", f"Transition: [bold]{change.trans_name}[/bold]", json_only=json_only)
128
+ check_transitions(spec, names=[change.trans_name], json_only=json_only)
129
+ check_scenarios(spec, transitions=[change.trans_name], json_only=json_only)
130
+ check_traces(spec, art_cont=art_cont, art_map=art_map, traces=[], json_only=json_only)
131
+
132
+ try:
133
+ maybe_update_spec_complement(spec)
134
+ except Exception as e:
135
+ _note(f"[yellow]⚠[/yellow] Complement update failed (non-fatal): {type(e).__name__}: {e}", json_only=json_only)
136
+
137
+ _note("[green]✓[/green] Transition analysis complete", json_only=json_only)
138
+ return
139
+
140
+ if isinstance(change, TransitionRemove):
141
+ _hdr("Transition removed", f"[bold]{change.trans_name}[/bold]", json_only=json_only)
142
+ check_scenarios(spec, transitions=[change.trans_name], json_only=json_only)
143
+ check_traces(spec, art_cont=art_cont, art_map=art_map, traces=[], json_only=json_only)
144
+
145
+ try:
146
+ maybe_update_spec_complement(spec)
147
+ except Exception as e:
148
+ _note(f"[yellow]⚠[/yellow] Complement update failed (non-fatal): {type(e).__name__}: {e}", json_only=json_only)
149
+
150
+ _note("[green]✓[/green] Scenario refresh complete", json_only=json_only)
151
+ return
152
+
153
+ # ----------------------------
154
+ # Scenarios changed
155
+ # ----------------------------
156
+ if isinstance(change, (ScenarioAdd, ScenarioEdit)):
157
+ _hdr("Scenario change", f"Scenario: [bold]{change.scenario_name}[/bold]", json_only=json_only)
158
+ check_scenarios(spec, names=[change.scenario_name], json_only=json_only)
159
+ check_traces(spec, art_cont=art_cont, art_map=art_map, traces=[], json_only=json_only)
160
+
161
+ try:
162
+ maybe_update_spec_complement(spec)
163
+ except Exception as e:
164
+ _note(f"[yellow]⚠[/yellow] Complement update failed (non-fatal): {type(e).__name__}: {e}", json_only=json_only)
165
+
166
+ _note("[green]✓[/green] Scenario analysis complete", json_only=json_only)
167
+ return
168
+
169
+ if isinstance(change, ScenarioRemove):
170
+ _hdr("Scenario removed", "Refreshing scenarios, predicates, and traces", json_only=json_only)
171
+ check_scenarios(spec, json_only=json_only)
172
+ check_predicates(spec, json_only=json_only)
173
+ check_traces(spec=spec, art_cont=art_cont, art_map=art_map, traces=[], json_only=json_only)
174
+
175
+ try:
176
+ maybe_update_spec_complement(spec)
177
+ except Exception as e:
178
+ _note(f"[yellow]⚠[/yellow] Complement update failed (non-fatal): {type(e).__name__}: {e}", json_only=json_only)
179
+
180
+ _note("[green]✓[/green] Full refresh complete", json_only=json_only)
181
+ return
182
+
183
+ # ----------------------------
184
+ # Trace artifacts changed
185
+ # ----------------------------
186
+ if isinstance(change, AddTestTrace):
187
+ if not art_cont.test_traces:
188
+ return
189
+
190
+ tt = art_cont.test_traces[-1]
191
+ # best-effort sanity check; no-op if mismatch
192
+ if change.art_id is not None and tt.art_id != change.art_id:
193
+ pass
194
+
195
+ assert tt.art_id is not None
196
+
197
+ check_traces(
198
+ spec=spec,
199
+ art_cont=art_cont,
200
+ art_map=art_map,
201
+ traces=[tt.art_id],
202
+ json_only=json_only,
203
+ )
204
+
205
+ _note("[green]✓[/green] Trace analysis complete", json_only=json_only)
206
+ return
207
+
208
+ if isinstance(change, AddLogTrace):
209
+ if not art_cont.log_traces:
210
+ return
211
+
212
+ lt = art_cont.log_traces[-1]
213
+ if change.art_id is not None and lt.art_id != change.art_id:
214
+ pass
215
+
216
+ assert lt.art_id is not None
217
+
218
+ check_traces(
219
+ spec=spec,
220
+ art_cont=art_cont,
221
+ art_map=art_map,
222
+ traces=[lt.art_id],
223
+ json_only=json_only,
224
+ )
225
+
226
+ _note("[green]✓[/green] Trace analysis complete", json_only=json_only)
227
+ return
228
+
229
+ if isinstance(change, (EditTestTrace, EditLogTrace)):
230
+ check_traces(
231
+ spec=spec,
232
+ art_cont=art_cont,
233
+ art_map=art_map,
234
+ traces=[change.art_id],
235
+ json_only=json_only,
236
+ )
237
+
238
+ _note("[green]✓[/green] Trace analysis complete", json_only=json_only)
239
+ return
240
+
241
+ # ----------------------------
242
+ # Explicit no-op branch
243
+ # ----------------------------
244
+ # All remaining change types (data artifacts, mapping edits, etc.)
245
+ # are intentionally ignored here and handled elsewhere.
246
+ return
@@ -0,0 +1,194 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/logic/strings.py
5
+ #
6
+
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from typing import Sequence
12
+
13
+ from ..modeling.scenario import Scenario
14
+
15
+ def parens(x: str) -> str:
16
+ x = x.strip()
17
+ return x if (x.startswith("(") and x.endswith(")")) else f"({x})"
18
+
19
+
20
+ def and_all(conjuncts: Sequence[str]) -> str:
21
+ if not conjuncts:
22
+ return "true"
23
+ return " && ".join(parens(c) for c in conjuncts)
24
+
25
+
26
+ def mk_predicates_only_instance_src(
27
+ name: str,
28
+ *,
29
+ given: Sequence[str], # state predicate names: p : state -> bool
30
+ when: Sequence[str], # action predicate names: q : state -> action -> bool (state first!)
31
+ state_type: str = "state",
32
+ action_type: str = "action",
33
+ state_var: str = "s",
34
+ action_var: str = "a",
35
+ ) -> str:
36
+ """
37
+ Consistency query: only predicates (given/when), no transitions.
38
+
39
+ Produces:
40
+ (p1 s) && (q1 s a) && ...
41
+ """
42
+ lines : list[str] = ["fun (s : state) (a : action) -> "]
43
+
44
+ exprs: list[str] = []
45
+ exprs += [f"{p} {state_var}" for p in given]
46
+ exprs += [f"{q} {state_var} {action_var}" for q in when]
47
+
48
+ lines.append(f" {and_all(exprs)}")
49
+ result = "\n".join(lines)
50
+
51
+ return result
52
+
53
+
54
+ def mk_same_action_trace_instance_src(
55
+ name: str,
56
+ *,
57
+ given: Sequence[str], # state predicate names
58
+ when: Sequence[str], # action predicate names
59
+ transitions: Sequence[str], # transition fn names: t : state -> action -> state
60
+ state_type: str = "state",
61
+ action_type: str = "action",
62
+ state0_var: str = "s0",
63
+ action_var: str = "a",
64
+ when_each_step: bool = True, # apply action predicates at each pre-state
65
+ ) -> str:
66
+ """
67
+ Trace query: transitions applied sequentially using the SAME action `a`.
68
+
69
+ Produces:
70
+
71
+ instance <name> :
72
+ let s0 : state = s0 in
73
+ let a : action = a in
74
+ let s1 = t0 s0 a in
75
+ let s2 = t1 s1 a in
76
+ (given(s0)) && (when(s0,a)) && (when(s1,a)) ...
77
+
78
+ Note: for N transitions, `when` is applied to s0..s{N-1} if when_each_step=True.
79
+ """
80
+ lines: list[str] = [f"instance {name} :"]
81
+
82
+ # typed existentials
83
+ lines.append(f" let {state0_var} : {state_type} = {state0_var} in")
84
+ lines.append(f" let {action_var} : {action_type} = {action_var} in")
85
+
86
+ # let-chain states
87
+ # s0 already exists; create s1..sN
88
+ for i, tname in enumerate(transitions):
89
+ si = state0_var if i == 0 else f"s{i}"
90
+ si1 = f"s{i+1}"
91
+ lines.append(f" let {si1} = {tname} {si} {action_var} in")
92
+
93
+ exprs: list[str] = []
94
+ exprs += [f"{p} {state0_var}" for p in given]
95
+
96
+ if when_each_step:
97
+ for i in range(len(transitions)):
98
+ si = state0_var if i == 0 else f"s{i}"
99
+ exprs += [f"{q} {si} {action_var}" for q in when]
100
+ else:
101
+ exprs += [f"{q} {state0_var} {action_var}" for q in when]
102
+
103
+ lines.append(f" {and_all(exprs)}")
104
+ return "\n".join(lines)
105
+
106
+
107
+ def mk_scenario_expr(sc: Scenario, *, s_var: str = "s", a_var: str = "a") -> str:
108
+ parts: list[str] = []
109
+ parts += [f"({p} {s_var})" for p in sc.given]
110
+ parts += [f"({q} {s_var} {a_var})" for q in sc.when]
111
+ return "true" if not parts else " && ".join(parts)
112
+
113
+ def mk_spec_disjunction_expr(scenarios: Sequence[Scenario]) -> str:
114
+ disj = " || ".join(f"({mk_scenario_expr(sc)})" for sc in scenarios)
115
+ return "false" if not disj else disj
116
+
117
+ def mk_goal_def(goal_name: str, goal_expr: str, *, needs_action: bool) -> str:
118
+ if needs_action:
119
+ return (
120
+ f"let {goal_name} (s : state) (a : action) : bool =\n"
121
+ f" {goal_expr}\n"
122
+ )
123
+ else:
124
+ return (
125
+ f"let {goal_name} (s : state) : bool =\n"
126
+ f" {goal_expr}\n"
127
+ )
128
+
129
+ def collect_pred_names(scenarios: Sequence[Scenario]) -> tuple[list[str], list[str]]:
130
+ state_names: list[str] = []
131
+ action_names: list[str] = []
132
+ for sc in scenarios:
133
+ state_names += list(sc.given)
134
+ action_names += list(sc.when)
135
+ return state_names, action_names
136
+
137
+ LET_RE = re.compile(r"^\s*let\s+([a-zA-Z_][a-zA-Z0-9_']*)\s*(?::\s*[^=]+)?=\s*(.+)$", re.DOTALL)
138
+
139
+ def normalize_optional(s: str | None) -> str | None:
140
+ if s is None:
141
+ return None
142
+ s2 = s.strip()
143
+ return s2 if s2 else None
144
+
145
+ def mk_typed_let(binding_src: str, *, expected_type: str, default_name: str) -> tuple[str, str]:
146
+ """
147
+ Returns: (let_src, bound_name)
148
+ Accepts either:
149
+ - `let x = ...` (or `let x : t = ...`) -> keeps x
150
+ - `<expr>` -> wraps as `let <default_name> : expected_type = (<expr>)`
151
+ """
152
+ m = LET_RE.match(binding_src.strip())
153
+ if m:
154
+ name = m.group(1)
155
+ # Keep user's let as-is; we just trust their annotation (or lack of).
156
+ # (Type errors will be caught by eval_model.)
157
+ return binding_src.strip(), name
158
+
159
+ name = default_name
160
+ let_src = f"let {name} : {expected_type} = (\n{binding_src.strip()}\n)"
161
+ return let_src, name
162
+
163
+ def mk_instance_src_for_expr(*, name: str, expr: str, needs_action: bool) -> str:
164
+ # Assumes `state` and (optionally) `action` exist in the model context.
165
+ # `expr` must be boolean and can mention `s` and optionally `a`.
166
+ if needs_action:
167
+ return f"""
168
+ instance {name} =
169
+ {{ s : state; a : action }}
170
+ where {expr}
171
+ """
172
+ return f"""
173
+ instance {name} =
174
+ {{ s : state }}
175
+ where {expr}
176
+ """
177
+
178
+ def errs_to_string(eval_res: object) -> str | None:
179
+ """
180
+ Your eval_model result looks like it may have .has_errors and .errors.
181
+ Convert to a multi-line string.
182
+ """
183
+ if not bool(getattr(eval_res, "has_errors", False)):
184
+ return None
185
+
186
+ errs = getattr(eval_res, "errors", None)
187
+ if not errs:
188
+ return "Domain/trace model has errors"
189
+
190
+ # errs items could be strings or structured; stringify safely
191
+ lines: list[str] = []
192
+ for e in errs:
193
+ lines.append(str(e))
194
+ return "\n".join(lines)
@@ -0,0 +1,135 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/logic/utils.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Literal
10
+
11
+ InstanceStatus = Literal["sat", "unsat", "unknown", "err"]
12
+
13
+ _ONEOF_CANDIDATES = ("result", "res", "outcome", "status", "response")
14
+
15
+ def instance_status(res: Any) -> InstanceStatus:
16
+
17
+ # 1) Prefer oneof introspection if available
18
+ for oneof_name in _ONEOF_CANDIDATES:
19
+ try:
20
+ which = res.WhichOneof(oneof_name)
21
+ except Exception:
22
+ continue
23
+ if which in ("sat", "unsat", "unknown", "err"):
24
+ return which # type: ignore[return-value]
25
+
26
+ # 2) Fallback to HasField checks
27
+ for k in ("sat", "unsat", "unknown", "err"):
28
+ try:
29
+ if res.HasField(k):
30
+ return k # type: ignore[return-value]
31
+ except Exception:
32
+ pass
33
+
34
+ # 3) Conservative fallback
35
+ return "unknown"
36
+
37
+
38
+ def instance_is_sat(res: Any) -> bool:
39
+ return res.sat is not None
40
+
41
+ def instance_model(res: Any) -> Any | None:
42
+ """
43
+ Returns Model if SAT, else None.
44
+ """
45
+ return res.sat.model.src if res.sat is not None and res.sat.model is not None else None
46
+
47
+ def instance_model_src(res: Any) -> str | None:
48
+ """
49
+ Returns Model.src if SAT, else None.
50
+ """
51
+ m = instance_model(res)
52
+ if m is None:
53
+ return None
54
+ return getattr(m, "src", None)
55
+
56
+
57
+ def instance_model_artifact(res: Any) -> Any | None:
58
+ """
59
+ Returns Model.artifact if SAT, else None.
60
+ """
61
+ m = instance_model(res)
62
+ if m is None:
63
+ return None
64
+ return getattr(m, "artifact", None)
65
+
66
+
67
+ def instance_unsat_proof(res: Any) -> str | None:
68
+ """
69
+ Returns Unsat.proof_pp if UNSAT, else None.
70
+ """
71
+ if instance_status(res) != "unsat":
72
+ return None
73
+ try:
74
+ return res.unsat.proof_pp
75
+ except Exception:
76
+ return None
77
+
78
+
79
+ def instance_unknown_reason(res: Any) -> str | None:
80
+ """
81
+ Returns unknown StringMsg content if available.
82
+ `_utils_pb2.StringMsg` typically stores a `msg` or `value` field; handle both.
83
+ """
84
+ if instance_status(res) != "unknown":
85
+ return None
86
+ u = getattr(res, "unknown", None)
87
+ if u is None:
88
+ return None
89
+ return getattr(u, "msg", None) or getattr(u, "value", None) or str(u)
90
+
91
+
92
+ def instance_has_errors(res: Any) -> bool:
93
+ try:
94
+ return len(res.errors) > 0
95
+ except Exception:
96
+ return False
97
+
98
+
99
+ def instance_to_jsonable(res: Any) -> dict[str, Any]:
100
+ """
101
+ Minimal, stable JSONable view for SpecLogician logs/CLI:
102
+ - status
103
+ - model.src (if sat)
104
+ - unsat.proof_pp (if unsat)
105
+ - unknown reason (if unknown)
106
+ - errors count (+ optionally stringified errors)
107
+ - task (stringified)
108
+ """
109
+ st = instance_status(res)
110
+ out: dict[str, Any] = {
111
+ "status": st,
112
+ "has_errors": instance_has_errors(res),
113
+ "errors_count": len(res.errors) if hasattr(res, "errors") else 0,
114
+ }
115
+
116
+ if st == "sat":
117
+ m = res.sat.model
118
+ out["model"] = {
119
+ "m_type": str(getattr(m, "m_type", "")),
120
+ "src": getattr(m, "src", ""),
121
+ # artifact can be huge; include a string repr unless you want deeper extraction
122
+ "artifact": str(getattr(m, "artifact", "")),
123
+ }
124
+ elif st == "unsat":
125
+ out["proof_pp"] = getattr(res.unsat, "proof_pp", "")
126
+ elif st == "unknown":
127
+ out["reason"] = instance_unknown_reason(res)
128
+ elif st == "err":
129
+ out["reason"] = "err"
130
+
131
+ # Task is protobuf; stringify safely
132
+ if hasattr(res, "task"):
133
+ out["task"] = str(res.task)
134
+
135
+ return out