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,78 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # utils/__init__.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import json
11
+ import typer
12
+ from typing import Any, TypeAlias
13
+ from enum import Enum, StrEnum
14
+ from rich.console import Console
15
+ from rich.theme import Theme
16
+
17
+ IMANDRA_KEY_ENV = "IMANDRA_UNI_KEY"
18
+
19
+
20
+ console = Console(
21
+ force_terminal=True,
22
+ color_system="truecolor",
23
+ theme=Theme({"info": "cyan", "ok": "green", "warn": "yellow", "err": "red"})
24
+ )
25
+
26
+ class IMLValidity (StrEnum):
27
+ VALID = 'valid'
28
+ INVALID = 'invalid'
29
+ UNKNOWN = 'unknown'
30
+
31
+
32
+ JSONScalar: TypeAlias = str | int | float | bool | None
33
+ JSONValue: TypeAlias = JSONScalar | list["JSONValue"] | dict[str, "JSONValue"]
34
+ JSONObject: TypeAlias = dict[str, JSONValue]
35
+
36
+
37
+ def emit_json(obj: JSONObject) -> None:
38
+ """
39
+ Print JSON (never exits).
40
+ Use this only at the CLI boundary or for debugging.
41
+ """
42
+ typer.echo(json.dumps(obj, indent=2, default=_json_default))
43
+
44
+
45
+ def _json_default(o: Any) -> Any:
46
+ # Enums (BaseStatus, PredicateType, etc)
47
+ if isinstance(o, Enum):
48
+ return o.value
49
+ # datetime, Path, etc (best-effort string)
50
+ try:
51
+ return str(o)
52
+ except Exception:
53
+ return repr(o)
54
+
55
+ def require_imandra_key() -> None:
56
+ """
57
+ Ensure IMANDRA_UNI_KEY is present in the environment.
58
+ Exit the CLI with a clear message if not.
59
+ """
60
+ key = os.getenv(IMANDRA_KEY_ENV)
61
+
62
+ if not key:
63
+ console.print(
64
+ "[red]✖ ImandraX API key missing[/red]\n\n"
65
+ "SpecLogician requires an Imandra Universe API key to run.\n\n"
66
+ "Please set the environment variable:\n\n"
67
+ f" export {IMANDRA_KEY_ENV}=<your-api-key>\n\n"
68
+ "You can obtain a key from Imandra Universe."
69
+ )
70
+ raise typer.Exit(code=2)
71
+
72
+ __all__ = [
73
+ "require_imandra_key",
74
+ "emit_json",
75
+ "_json_default",
76
+ "IMLValidity",
77
+ "console"
78
+ ]
@@ -0,0 +1,166 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/utils/load.py
5
+ #
6
+
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+
11
+ import typer
12
+ from typing import NoReturn
13
+ from rich.prompt import Prompt
14
+
15
+ from ..state.state import State
16
+ from .__init__ import console
17
+
18
+
19
+ def _emit_json(obj: dict, exit_code: int = 0) -> NoReturn:
20
+ """Print JSON and exit (useful for CLI / tests)."""
21
+ typer.echo(json.dumps(obj, indent=2))
22
+ raise typer.Exit(code=exit_code)
23
+
24
+
25
+ def load_state(path: Path | None = None, json: bool = False) -> State:
26
+ """
27
+ Load a State either from an explicit JSON path or from the local directory.
28
+
29
+ - If `path` is provided:
30
+ * if it exists: load JSON from it
31
+ * if it doesn't exist: ask to create it (unless json)
32
+ - If `path` is None:
33
+ * load from current directory via State.from_dir()
34
+ * if missing: ask to create it (unless json)
35
+
36
+ If json=True:
37
+ - emit JSON on success/error and Exit (no prompts).
38
+ """
39
+ cwd = Path(os.getcwd())
40
+
41
+ def create_new_state(target: Path | None) -> State:
42
+ """
43
+ Create a new state and persist it.
44
+ NOTE: Your State.save() appears to accept a directory path; we use:
45
+ - cwd when target is None
46
+ - target.parent when target is a file path
47
+ """
48
+ state = State()
49
+
50
+ if target is None:
51
+ state.save(str(cwd))
52
+ return state
53
+
54
+ target = target.expanduser()
55
+ target.parent.mkdir(parents=True, exist_ok=True)
56
+ state.save(str(target.parent))
57
+ return state
58
+
59
+ # -------------------------
60
+ # Case 1: explicit file path
61
+ # -------------------------
62
+ if path is not None:
63
+ path = path.expanduser()
64
+
65
+ if path.exists():
66
+ try:
67
+ if path.is_dir():
68
+ new_state = State.from_dir(dirpath=path)
69
+ else:
70
+ new_state = State.from_json(path.read_text())
71
+ except Exception as e:
72
+ if json:
73
+ _emit_json(
74
+ {
75
+ "ok": False,
76
+ "error": "failed_to_load_state_json",
77
+ "path": str(path),
78
+ "message": str(e),
79
+ },
80
+ exit_code=2,
81
+ )
82
+ console.print(f"[red]Failed to load state JSON:[/] {path}\n{e}")
83
+ raise
84
+
85
+ if json:
86
+ _emit_json(
87
+ {"ok": True, "source": "state_json", "path": str(path)},
88
+ exit_code=0,
89
+ )
90
+ return new_state
91
+
92
+ # File doesn't exist
93
+ if json:
94
+ _emit_json(
95
+ {
96
+ "ok": False,
97
+ "error": "state_json_not_found",
98
+ "path": str(path),
99
+ "message": "State JSON file does not exist.",
100
+ },
101
+ exit_code=2,
102
+ )
103
+
104
+ answer = Prompt.ask(
105
+ prompt=f"⚠️ State file not found at {path}. Create a new one?",
106
+ choices=["Yes", "No"],
107
+ console=console,
108
+ case_sensitive=False,
109
+ default="Yes",
110
+ )
111
+ if answer.lower() == "no":
112
+ console.print("Goodbye!")
113
+ raise typer.Exit(0)
114
+
115
+ console.print("Creating a new state!")
116
+ return create_new_state(path)
117
+
118
+ # -------------------------
119
+ # Case 2: default local dir
120
+ # -------------------------
121
+ try:
122
+ new_state = State.from_dir(dirpath=str(cwd))
123
+ except Exception as e:
124
+ if json:
125
+ _emit_json(
126
+ {
127
+ "ok": False,
128
+ "error": "failed_to_load_state_from_dir",
129
+ "dir": str(cwd),
130
+ "message": str(e),
131
+ },
132
+ exit_code=2,
133
+ )
134
+ console.print(f"[red]Failed to load state from directory:[/] {cwd}\n{e}")
135
+ raise
136
+
137
+ if new_state is not None:
138
+ if json:
139
+ _emit_json({"ok": True, "source": "dir", "dir": str(cwd)}, exit_code=0)
140
+ return new_state
141
+
142
+ # Not found in cwd
143
+ if json:
144
+ _emit_json(
145
+ {
146
+ "ok": False,
147
+ "error": "state_not_found_in_dir",
148
+ "dir": str(cwd),
149
+ "message": "No state found in current directory.",
150
+ },
151
+ exit_code=2,
152
+ )
153
+
154
+ answer = Prompt.ask(
155
+ prompt="⚠️ Couldn't find a state in current directory. Create a new one?",
156
+ choices=["Yes", "No"],
157
+ console=console,
158
+ case_sensitive=False,
159
+ default="Yes",
160
+ )
161
+ if answer.lower() == "no":
162
+ console.print("Goodbye!")
163
+ raise typer.Exit(0)
164
+
165
+ console.print("Creating a new state!")
166
+ return create_new_state(None)
@@ -0,0 +1,325 @@
1
+ # SpecLogician Agent Playbook
2
+ ## Formal Reasoning Copilot for Code, Finance, and Contracts
3
+
4
+ ---
5
+
6
+ ## 1. Purpose and Audience
7
+
8
+ This playbook is written for **LLM-based coding and reasoning agents**
9
+ (Claude Code, Copilot, Cursor, internal agents) operating alongside:
10
+
11
+ - **SpecLogician** (stateful specification + analysis loop)
12
+ - **CodeLogician** (code → formal model translation)
13
+ - **ImandraX** (symbolic reasoning, decomposition, complement analysis)
14
+
15
+ The goal is to enable **trustworthy, auditable, and logically complete workflows**
16
+ for regulated and high‑assurance domains:
17
+ - Software systems
18
+ - Financial models
19
+ - Risk engines
20
+ - M&A and regulatory contracts
21
+ - Protocols and workflows
22
+
23
+ You are not a text editor.
24
+ You are a **formal reasoning copilot**.
25
+
26
+ ---
27
+
28
+ ## 2. Core Mental Model
29
+
30
+ SpecLogician maintains an **immutable state history**.
31
+
32
+ Each change:
33
+ - Produces a new `StateInstance`
34
+ - Triggers logical analysis
35
+ - Produces a structured `state_diff`
36
+
37
+ A state consists of:
38
+
39
+ ```
40
+ State
41
+ ├─ Spec
42
+ │ ├─ Domain Model
43
+ │ │ ├─ Base (types + core invariants, IML)
44
+ │ │ ├─ State Predicates
45
+ │ │ ├─ Action Predicates
46
+ │ │ └─ Transitions
47
+ │ ├─ Scenarios (Given / When / Then)
48
+ │ └─ Scenario Complement (coverage)
49
+ ├─ Artifact Container
50
+ │ ├─ Test Traces
51
+ │ ├─ Log Traces
52
+ │ ├─ Documents
53
+ │ └─ Source References
54
+ ├─ Artifact Mapping
55
+ ├─ Derived Stats
56
+ └─ StateDiff (vs previous state)
57
+ ```
58
+
59
+ Nothing mutates in place.
60
+ Progress is measured by **diffs and coverage**, not by edits.
61
+
62
+ ---
63
+
64
+ ## 3. The Agent Loop (Golden Path)
65
+
66
+ Every successful interaction follows this loop:
67
+
68
+ 1. **Inspect**
69
+ ```bash
70
+ speclogician view state
71
+ speclogician view diff
72
+ ```
73
+
74
+ 2. **Diagnose**
75
+ - Missing predicates?
76
+ - Inconsistent scenarios?
77
+ - Large complement?
78
+ - Traces not matching scenarios?
79
+
80
+ 3. **Search**
81
+ ```bash
82
+ speclogician find predicates <query>
83
+ speclogician find transitions <query>
84
+ speclogician find artifacts <query>
85
+ ```
86
+
87
+ 4. **Apply exactly ONE change**
88
+ ```bash
89
+ speclogician ch ...
90
+ ```
91
+
92
+ 5. **Re‑inspect**
93
+ ```bash
94
+ speclogician view diff
95
+ ```
96
+
97
+ 6. **Repeat**
98
+
99
+ > One change. One diff. One improvement.
100
+
101
+ ---
102
+
103
+ ## 4. Change Commands (`speclogician ch`)
104
+
105
+ ### 4.1 Domain Base
106
+
107
+ Defines the semantic universe.
108
+
109
+ ```bash
110
+ speclogician ch base-edit --src "type state = { x:int }
111
+ type action = int"
112
+ ```
113
+
114
+ Rules:
115
+ - Must be valid IML
116
+ - Breaks everything downstream if wrong
117
+ - Complement is gated on base validity
118
+
119
+ ---
120
+
121
+ ### 4.2 Predicates
122
+
123
+ Predicates define **classifications**, not behavior.
124
+
125
+ ```bash
126
+ speclogician ch pred-add --pred-type STATE --pred-name is_large_trade --pred-src "s.amount > 1_000_000"
127
+ ```
128
+
129
+ Types:
130
+ - STATE: properties of state
131
+ - ACTION: properties of actions
132
+
133
+ ---
134
+
135
+ ### 4.3 Transitions
136
+
137
+ Transitions define **state evolution**.
138
+
139
+ ```bash
140
+ speclogician ch trans-add --trans-name execute --trans-src "let execute (s:state) (a:action) = { amount = s.amount + a }"
141
+ ```
142
+
143
+ ---
144
+
145
+ ### 4.4 Scenarios
146
+
147
+ Scenarios define **intended behaviors**.
148
+
149
+ ```bash
150
+ speclogician ch scenario-add trade_exec --given is_authorized --when place_order --then execute
151
+ ```
152
+
153
+ Scenarios:
154
+ - Are partial specifications
155
+ - Do not need to be complete
156
+ - Drive complement reasoning
157
+
158
+ ---
159
+
160
+ ### 4.5 Traces
161
+
162
+ Traces are **concrete evidence**.
163
+
164
+ ```bash
165
+ speclogician ch trace test-add --art-id T1 --name test_large_trade --given "{ amount = 2_000_000 }" --when "{ order = BUY }" --then "{ approved = true }"
166
+ ```
167
+
168
+ Traces validate scenarios but **do not replace them**.
169
+
170
+ ---
171
+
172
+ ## 5. View Commands (`speclogician view`)
173
+
174
+ Most important commands:
175
+
176
+ ```bash
177
+ speclogician view diff
178
+ speclogician view spec
179
+ speclogician view domain
180
+ speclogician view artifacts
181
+ ```
182
+
183
+ Rules:
184
+ - Always inspect `view diff`
185
+ - Treat diffs as first‑class artifacts
186
+ - Never assume success without checking
187
+
188
+ ---
189
+
190
+ ## 6. Search Commands (`speclogician find`)
191
+
192
+ Use search before modification.
193
+
194
+ ```bash
195
+ speclogician find predicates limit --kind state
196
+ speclogician find transitions approve
197
+ speclogician find artifacts margin --kind docs
198
+ ```
199
+
200
+ Search prevents duplicate logic and drift.
201
+
202
+ ---
203
+
204
+ ## 7. Complement & Coverage
205
+
206
+ The **scenario complement** represents:
207
+ > All logically possible behaviors not covered by scenarios.
208
+
209
+ Properties:
210
+ - Computed via ImandraX
211
+ - Semantic (not test‑based)
212
+ - Shrinking complement = progress
213
+
214
+ Signals:
215
+ - No complement → base or scenarios invalid
216
+ - Large complement → missing intent
217
+ - Empty complement → specification is complete (rare, valuable)
218
+
219
+ ---
220
+
221
+ ## 8. Worked Example 1: Minimal State Machine
222
+
223
+ ### Goal
224
+ Model a simple counter with increment.
225
+
226
+ **Base**
227
+ ```bash
228
+ speclogician ch base-edit --src "type state = { x:int }
229
+ type action = int"
230
+ ```
231
+
232
+ **Predicate**
233
+ ```bash
234
+ speclogician ch pred-add --pred-type STATE --pred-name nonneg --pred-src "s.x >= 0"
235
+ ```
236
+
237
+ **Transition**
238
+ ```bash
239
+ speclogician ch trans-add --trans-name inc --trans-src "let inc s a = { x = s.x + a }"
240
+ ```
241
+
242
+ **Scenario**
243
+ ```bash
244
+ speclogician ch scenario-add inc_ok --given nonneg --then inc
245
+ ```
246
+
247
+ Inspect:
248
+ ```bash
249
+ speclogician view diff
250
+ ```
251
+
252
+ Complement highlights:
253
+ - What about negative actions?
254
+ - What about overflow?
255
+
256
+ Add scenarios accordingly.
257
+
258
+ ---
259
+
260
+ ## 9. Worked Example 2: Financial Rule
261
+
262
+ ### Rule
263
+ Large trades require approval.
264
+
265
+ **Predicate**
266
+ ```bash
267
+ speclogician ch pred-add --pred-type STATE --pred-name large_trade --pred-src "s.amount > 1_000_000"
268
+ ```
269
+
270
+ **Scenario**
271
+ ```bash
272
+ speclogician ch scenario-add approval_required --given large_trade --then approve
273
+ ```
274
+
275
+ Complement reveals:
276
+ - Small trades?
277
+ - Large trades without approval?
278
+ - Conflicting predicates?
279
+
280
+ This is logic discovery, not testing.
281
+
282
+ ---
283
+
284
+ ## 10. Domains Beyond Code
285
+
286
+ This workflow applies unchanged to:
287
+
288
+ - M&A contracts (conditions, obligations, breaches)
289
+ - Financial models (risk regions, limits)
290
+ - Regulatory logic (must/must-not rules)
291
+ - Protocols (valid/invalid sequences)
292
+
293
+ Replace:
294
+ - state → contract state
295
+ - action → event
296
+ - transition → clause effect
297
+
298
+ ---
299
+
300
+ ## 11. What NOT To Do
301
+
302
+ ❌ Edit files directly
303
+ ❌ Apply multiple changes at once
304
+ ❌ Ignore `state_diff`
305
+ ❌ Confuse tests with coverage
306
+ ❌ Assume LLM intuition equals correctness
307
+
308
+ ---
309
+
310
+ ## 12. Final Agent Instructions
311
+
312
+ You are a **reasoning engine supervisor**.
313
+
314
+ Every step must:
315
+ - Be justified by `view` or `find`
316
+ - Produce a measurable `state_diff`
317
+ - Reduce inconsistency or complement
318
+ - Increase confidence and auditability
319
+
320
+ If unsure:
321
+ - Inspect more
322
+ - Ask for clarification
323
+ - Apply the smallest possible change
324
+
325
+ Formal reasoning is a process, not a jump.
@@ -0,0 +1,151 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/utils/testing.py
5
+ #
6
+
7
+ from .__init__ import JSONObject, JSONValue
8
+
9
+ import json
10
+ from typing import cast
11
+ from click.testing import Result
12
+
13
+ # -----------------------
14
+ # JSON parsing + narrowing helpers
15
+ # -----------------------
16
+
17
+ def parse_json_result(res : Result) -> JSONObject:
18
+ """
19
+ Parse JSON from CLI output.
20
+
21
+ If stdout is empty (Typer/Pydantic failed before we could emit JSON),
22
+ return a synthetic JSON error object so tests can assert consistently.
23
+ """
24
+ out = (res.stdout or "").strip()
25
+
26
+ if not out:
27
+ # Pre-command failure (argument parsing / Pydantic validation / Typer)
28
+ # Normalize into the same "result envelope" your CLI emits.
29
+ exc_s = ""
30
+ if res.exception is not None:
31
+ exc_s = str(res.exception)
32
+ elif res.output:
33
+ exc_s = str(res.output)
34
+
35
+ return cast(
36
+ JSONObject,
37
+ {
38
+ "ok": False,
39
+ "stage": "cli_parse",
40
+ "error": exc_s,
41
+ },
42
+ )
43
+
44
+ try:
45
+ data = json.loads(out)
46
+ except json.JSONDecodeError as e:
47
+ raise AssertionError(
48
+ "Failed to decode JSON from stdout.\n"
49
+ f"exit_code={res.exit_code}\n"
50
+ f"stdout={res.stdout!r}\n"
51
+ f"stderr={res.stderr!r}\n"
52
+ f"output={res.output!r}\n"
53
+ f"exception={res.exception!r}\n"
54
+ f"json_error={e}\n"
55
+ ) from e
56
+
57
+ # Ensure it's a dict-like JSON object
58
+ if not isinstance(data, dict):
59
+ raise AssertionError(f"Expected JSON object, got: {type(data).__name__}: {data!r}")
60
+
61
+ return cast(JSONObject, data)
62
+
63
+ def get_change(data: JSONObject) -> JSONObject:
64
+ """
65
+ New CLI JSON wraps the change in {"ok": ..., "change": {...}, ...}.
66
+ Also accept legacy where the change was top-level.
67
+ """
68
+ chg = data.get("change")
69
+ if isinstance(chg, dict):
70
+ return cast(JSONObject, chg)
71
+ return data
72
+
73
+
74
+ def _as_object(v: JSONValue, *, ctx: str = "value") -> JSONObject:
75
+ if not isinstance(v, dict):
76
+ raise AssertionError(f"Expected object for {ctx}, got {type(v).__name__}: {v!r}")
77
+ return cast(JSONObject, v)
78
+
79
+
80
+ def _as_str(v: JSONValue, *, ctx: str = "value") -> str:
81
+ if not isinstance(v, str):
82
+ raise AssertionError(f"Expected string for {ctx}, got {type(v).__name__}: {v!r}")
83
+ return v
84
+
85
+
86
+ def _as_bool(v: JSONValue, *, ctx: str = "value") -> bool:
87
+ if not isinstance(v, bool):
88
+ raise AssertionError(f"Expected bool for {ctx}, got {type(v).__name__}: {v!r}")
89
+ return v
90
+
91
+
92
+ def _as_str_list(v: JSONValue, *, ctx: str = "value") -> list[str]:
93
+ if not isinstance(v, list) or not all(isinstance(x, str) for x in v):
94
+ raise AssertionError(f"Expected list[str] for {ctx}, got: {v!r}")
95
+ return cast(list[str], v)
96
+
97
+
98
+ # -----------------------
99
+ # Change payload accessors
100
+ # -----------------------
101
+
102
+ def chg_kind(chg: JSONObject) -> str | None:
103
+ v = chg.get("kind")
104
+ if v is None:
105
+ return None
106
+ return _as_str(v, ctx="change.kind")
107
+
108
+
109
+ def chg_scenario_name(chg: JSONObject) -> str:
110
+ return _as_str(chg.get("scenario_name"), ctx="change.scenario_name")
111
+
112
+
113
+ def delta_add(chg: JSONObject, field: str) -> list[str]:
114
+ """
115
+ field is one of: "given", "when", "then"
116
+ """
117
+ obj = _as_object(chg.get(field), ctx=f"change.{field}")
118
+ return _as_str_list(obj.get("add"), ctx=f"change.{field}.add")
119
+
120
+
121
+ def delta_remove(chg: JSONObject, field: str) -> list[str]:
122
+ obj = _as_object(chg.get(field), ctx=f"change.{field}")
123
+ return _as_str_list(obj.get("remove"), ctx=f"change.{field}.remove")
124
+
125
+
126
+ def result_ok(data: JSONObject) -> bool:
127
+ return _as_bool(data.get("ok"), ctx="result.ok")
128
+
129
+
130
+ def result_stage(data: JSONObject) -> str | None:
131
+ v = data.get("stage")
132
+ if v is None:
133
+ return None
134
+ return _as_str(v, ctx="result.stage")
135
+
136
+
137
+ def result_error_str(data: JSONObject) -> str:
138
+ """
139
+ Your JSON-safe error is a JSONObject (per your typing), but for tests we usually
140
+ want a stable string. This extracts a string in a robust way.
141
+ Adjust if your error schema is richer (e.g. {"message": "...", "type": "..."}).
142
+ """
143
+ err = data.get("error")
144
+ if err is None:
145
+ return ""
146
+ if isinstance(err, str):
147
+ return err
148
+ if isinstance(err, dict) and isinstance(err.get("message"), str):
149
+ return cast(str, err["message"])
150
+ # fallback: stable stringify
151
+ return json.dumps(cast(JSONValue, err))