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.
- speclogician/__init__.py +0 -0
- speclogician/commands/__init__.py +15 -0
- speclogician/commands/cmd_ch.py +616 -0
- speclogician/commands/cmd_find.py +256 -0
- speclogician/commands/cmd_view.py +202 -0
- speclogician/commands/runner.py +149 -0
- speclogician/commands/utils.py +101 -0
- speclogician/data/__init__.py +0 -0
- speclogician/data/artifact.py +63 -0
- speclogician/data/container.py +402 -0
- speclogician/data/mapping.py +88 -0
- speclogician/data/refs.py +24 -0
- speclogician/data/traces.py +26 -0
- speclogician/demos/.DS_Store +0 -0
- speclogician/demos/cmd_demo.py +278 -0
- speclogician/demos/loader.py +135 -0
- speclogician/demos/model.py +27 -0
- speclogician/demos/runner.py +51 -0
- speclogician/logic/__init__.py +11 -0
- speclogician/logic/api/__init__.py +29 -0
- speclogician/logic/api/client.py +606 -0
- speclogician/logic/api/decomp.py +67 -0
- speclogician/logic/api/scenario.py +102 -0
- speclogician/logic/api/traces.py +59 -0
- speclogician/logic/lib/__init__.py +19 -0
- speclogician/logic/lib/complement.py +107 -0
- speclogician/logic/lib/domain_model.py +59 -0
- speclogician/logic/lib/predicates.py +151 -0
- speclogician/logic/lib/scenarios.py +369 -0
- speclogician/logic/lib/traces.py +114 -0
- speclogician/logic/lib/transitions.py +104 -0
- speclogician/logic/main.py +246 -0
- speclogician/logic/strings.py +194 -0
- speclogician/logic/utils.py +135 -0
- speclogician/main.py +139 -0
- speclogician/modeling/__init__.py +31 -0
- speclogician/modeling/complement.py +104 -0
- speclogician/modeling/component.py +71 -0
- speclogician/modeling/conflict.py +26 -0
- speclogician/modeling/domain.py +349 -0
- speclogician/modeling/predicates.py +59 -0
- speclogician/modeling/scenario.py +162 -0
- speclogician/modeling/spec.py +306 -0
- speclogician/modeling/spec_stats.py +39 -0
- speclogician/presentation/__init__.py +0 -0
- speclogician/presentation/api.py +244 -0
- speclogician/presentation/builders/__init__.py +0 -0
- speclogician/presentation/builders/_links.py +44 -0
- speclogician/presentation/builders/container.py +53 -0
- speclogician/presentation/builders/data_artifact.py +42 -0
- speclogician/presentation/builders/domain.py +54 -0
- speclogician/presentation/builders/instances_list.py +38 -0
- speclogician/presentation/builders/predicate.py +51 -0
- speclogician/presentation/builders/recommendations.py +41 -0
- speclogician/presentation/builders/scenario.py +41 -0
- speclogician/presentation/builders/scenario_complement.py +82 -0
- speclogician/presentation/builders/smart_find.py +39 -0
- speclogician/presentation/builders/spec.py +39 -0
- speclogician/presentation/builders/state_diff.py +150 -0
- speclogician/presentation/builders/state_instance.py +42 -0
- speclogician/presentation/builders/state_instance_summary.py +84 -0
- speclogician/presentation/builders/trace.py +58 -0
- speclogician/presentation/ctx.py +38 -0
- speclogician/presentation/models/__init__.py +0 -0
- speclogician/presentation/models/container.py +44 -0
- speclogician/presentation/models/data_artifact.py +33 -0
- speclogician/presentation/models/domain.py +50 -0
- speclogician/presentation/models/instances_list.py +23 -0
- speclogician/presentation/models/predicate.py +60 -0
- speclogician/presentation/models/recommendations.py +34 -0
- speclogician/presentation/models/scenario.py +31 -0
- speclogician/presentation/models/scenario_complement.py +40 -0
- speclogician/presentation/models/smart_find.py +34 -0
- speclogician/presentation/models/spec.py +32 -0
- speclogician/presentation/models/state_diff.py +34 -0
- speclogician/presentation/models/state_instance.py +31 -0
- speclogician/presentation/models/state_instance_summary.py +102 -0
- speclogician/presentation/models/trace.py +42 -0
- speclogician/presentation/preview/__init__.py +13 -0
- speclogician/presentation/preview/cli.py +50 -0
- speclogician/presentation/preview/fixtures/__init__.py +205 -0
- speclogician/presentation/preview/fixtures/artifact_container.py +150 -0
- speclogician/presentation/preview/fixtures/data_artifact.py +144 -0
- speclogician/presentation/preview/fixtures/domain_model.py +162 -0
- speclogician/presentation/preview/fixtures/instances_list.py +162 -0
- speclogician/presentation/preview/fixtures/predicate.py +184 -0
- speclogician/presentation/preview/fixtures/scenario.py +84 -0
- speclogician/presentation/preview/fixtures/scenario_complement.py +81 -0
- speclogician/presentation/preview/fixtures/smart_find.py +140 -0
- speclogician/presentation/preview/fixtures/spec.py +95 -0
- speclogician/presentation/preview/fixtures/state_diff.py +158 -0
- speclogician/presentation/preview/fixtures/state_instance.py +128 -0
- speclogician/presentation/preview/fixtures/state_instance_summary.py +80 -0
- speclogician/presentation/preview/fixtures/trace.py +206 -0
- speclogician/presentation/preview/registry.py +42 -0
- speclogician/presentation/renderers/__init__.py +24 -0
- speclogician/presentation/renderers/container.py +136 -0
- speclogician/presentation/renderers/data_artifact.py +144 -0
- speclogician/presentation/renderers/domain.py +123 -0
- speclogician/presentation/renderers/instances_list.py +120 -0
- speclogician/presentation/renderers/predicate.py +180 -0
- speclogician/presentation/renderers/recommendations.py +90 -0
- speclogician/presentation/renderers/scenario.py +94 -0
- speclogician/presentation/renderers/scenario_complement.py +59 -0
- speclogician/presentation/renderers/smart_find.py +307 -0
- speclogician/presentation/renderers/spec.py +105 -0
- speclogician/presentation/renderers/state_diff.py +102 -0
- speclogician/presentation/renderers/state_instance.py +82 -0
- speclogician/presentation/renderers/state_instance_summary.py +143 -0
- speclogician/presentation/renderers/trace.py +122 -0
- speclogician/py.typed +0 -0
- speclogician/shell/app.py +170 -0
- speclogician/shell/shell_ch.py +263 -0
- speclogician/shell/shell_view.py +153 -0
- speclogician/state/__init__.py +0 -0
- speclogician/state/change.py +428 -0
- speclogician/state/change_result.py +32 -0
- speclogician/state/diff.py +191 -0
- speclogician/state/inst.py +574 -0
- speclogician/state/recommendation.py +13 -0
- speclogician/state/recommender.py +577 -0
- speclogician/state/state.py +465 -0
- speclogician/state/state_stats.py +133 -0
- speclogician/tui/__init__.py +0 -0
- speclogician/tui/app.py +257 -0
- speclogician/tui/app.tcss +160 -0
- speclogician/tui/demo.py +45 -0
- speclogician/tui/images/speclogician-full.png +0 -0
- speclogician/tui/images/speclogician-minimal.png +0 -0
- speclogician/tui/main_screen.py +454 -0
- speclogician/tui/splash_screen.py +51 -0
- speclogician/tui/stats_screen.py +125 -0
- speclogician/utils/__init__.py +78 -0
- speclogician/utils/load.py +166 -0
- speclogician/utils/prompt.md +325 -0
- speclogician/utils/testing.py +151 -0
- speclogician-0.0.0b1.dist-info/METADATA +116 -0
- speclogician-0.0.0b1.dist-info/RECORD +139 -0
- 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))
|