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,278 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Imandra Inc.
|
|
3
|
+
#
|
|
4
|
+
# speclogician/demos/cmd_demo.py
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.markdown import Markdown
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.rule import Rule
|
|
16
|
+
from rich.text import Text
|
|
17
|
+
|
|
18
|
+
from speclogician.demos.loader import (
|
|
19
|
+
discover_demos,
|
|
20
|
+
load_demo,
|
|
21
|
+
load_changes,
|
|
22
|
+
)
|
|
23
|
+
from speclogician.demos.runner import apply_demo_changes
|
|
24
|
+
from speclogician.state.state import State
|
|
25
|
+
from speclogician.state.change import StateChange
|
|
26
|
+
from speclogician.utils import console
|
|
27
|
+
|
|
28
|
+
app = typer.Typer(help="Browse and run SpecLogician demos (markdown + changes + launch).")
|
|
29
|
+
|
|
30
|
+
DEFAULT_DEMOS_DIR = Path("demos").resolve()
|
|
31
|
+
CACHE_FILENAME = "state.json"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# -----------------------------------------------------------------------------
|
|
35
|
+
# helpers
|
|
36
|
+
# -----------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
def _cache_path(demo_root: Path) -> Path:
|
|
39
|
+
return demo_root / CACHE_FILENAME
|
|
40
|
+
|
|
41
|
+
def _load_cached_state(cache_path: Path) -> State:
|
|
42
|
+
data = cache_path.read_text(encoding="utf-8", errors="replace")
|
|
43
|
+
# State is assumed to be a Pydantic model (v2): model_validate_json exists.
|
|
44
|
+
return State.model_validate_json(data) # type: ignore[attr-defined]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _save_cached_state(state: State, cache_path: Path) -> None:
|
|
48
|
+
cache_path.write_text(
|
|
49
|
+
state.model_dump_json(indent=2), # type: ignore[attr-defined]
|
|
50
|
+
encoding="utf-8",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _build_state_from_changes(
|
|
55
|
+
*,
|
|
56
|
+
demo_root: Path,
|
|
57
|
+
changes: list[StateChange],
|
|
58
|
+
console: Console
|
|
59
|
+
) -> State:
|
|
60
|
+
res = apply_demo_changes(
|
|
61
|
+
changes=changes,
|
|
62
|
+
console=console,
|
|
63
|
+
initial_state=None,
|
|
64
|
+
show_progress=True,
|
|
65
|
+
)
|
|
66
|
+
return res.state
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _load_description(demo_root: Path) -> str:
|
|
70
|
+
"""
|
|
71
|
+
"""
|
|
72
|
+
return (demo_root / "description.md").read_text()
|
|
73
|
+
|
|
74
|
+
def _ensure_cache_exists(
|
|
75
|
+
*,
|
|
76
|
+
demo_root: Path,
|
|
77
|
+
changes: list[StateChange],
|
|
78
|
+
console: Console
|
|
79
|
+
) -> Path:
|
|
80
|
+
"""
|
|
81
|
+
Cache policy:
|
|
82
|
+
- demos run <name> : ALWAYS rebuild and overwrite cache
|
|
83
|
+
- demos shell/tui : REQUIRE cache; if missing, we build it once (quietly) and save it
|
|
84
|
+
"""
|
|
85
|
+
cache_path = _cache_path(demo_root)
|
|
86
|
+
if cache_path.exists():
|
|
87
|
+
return cache_path
|
|
88
|
+
|
|
89
|
+
console.print(Text(f"Cache missing; building {cache_path.name}…", style="dim"))
|
|
90
|
+
res = apply_demo_changes(
|
|
91
|
+
changes=changes,
|
|
92
|
+
console=console,
|
|
93
|
+
initial_state=None,
|
|
94
|
+
show_progress=False,
|
|
95
|
+
)
|
|
96
|
+
_save_cached_state(res.state, cache_path)
|
|
97
|
+
return cache_path
|
|
98
|
+
|
|
99
|
+
# -----------------------------------------------------------------------------
|
|
100
|
+
# commands
|
|
101
|
+
# -----------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
@app.command("ls")
|
|
104
|
+
def ls_cmd():
|
|
105
|
+
demos = discover_demos(DEFAULT_DEMOS_DIR)
|
|
106
|
+
if not demos:
|
|
107
|
+
console.print(Text(f"No demos found in {DEFAULT_DEMOS_DIR}", style="yellow"))
|
|
108
|
+
raise typer.Exit(0)
|
|
109
|
+
|
|
110
|
+
console.print('[bold magenta italic]Available demos:')
|
|
111
|
+
for d in demos:
|
|
112
|
+
cache_path = _cache_path(d.root)
|
|
113
|
+
description = _load_description(d.root).strip()
|
|
114
|
+
cache_badge = "[green]cached[/green]" if cache_path.exists() else "[dim]no-cache[/dim]"
|
|
115
|
+
console.print(f"• [bold]{d.name}[/bold] {cache_badge} [italic]{description}[/italic]")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.command("artifacts")
|
|
119
|
+
def artifacts_cmd(
|
|
120
|
+
name: str = typer.Argument(..., help="Demo name (directory under demos/).")
|
|
121
|
+
):
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
demo = load_demo(DEFAULT_DEMOS_DIR, name)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
console.print(f'Failed to load in the demo: {str(e)}', style="bold red")
|
|
127
|
+
raise typer.Exit(2)
|
|
128
|
+
|
|
129
|
+
artifact_path = demo.artifacts_path
|
|
130
|
+
if not artifact_path.exists():
|
|
131
|
+
console.print(Text(f"No artifact.md at {artifact_path}", style="yellow"))
|
|
132
|
+
raise typer.Exit(1)
|
|
133
|
+
|
|
134
|
+
text = artifact_path.read_text(encoding="utf-8", errors="replace")
|
|
135
|
+
console.print(Panel(Text(f"{demo.name}", style="bold"), title="Demo", border_style="magenta"))
|
|
136
|
+
console.print(Markdown(text))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@app.command("changes")
|
|
140
|
+
def changes_cmd(
|
|
141
|
+
name: str = typer.Argument(..., help="Demo name."),
|
|
142
|
+
show_meta: bool = typer.Option(True, "--meta/--no-meta", help="Show per-change rationale Markdown."),
|
|
143
|
+
):
|
|
144
|
+
try:
|
|
145
|
+
demo = load_demo(DEFAULT_DEMOS_DIR, name)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
console.print(f'Failed to load in the demo: {str(e)}', style="bold red")
|
|
148
|
+
raise typer.Exit(2)
|
|
149
|
+
|
|
150
|
+
changes: list[StateChange] = load_changes(demo)
|
|
151
|
+
|
|
152
|
+
console.print(Panel(Text(demo.name, style="bold"), title="Demo changes", border_style="magenta"))
|
|
153
|
+
console.print(Text(f"{len(changes)} changes", style="dim"))
|
|
154
|
+
console.print()
|
|
155
|
+
|
|
156
|
+
for i, ch in enumerate(changes, start=1):
|
|
157
|
+
kind = getattr(ch, "kind", ch.__class__.__name__)
|
|
158
|
+
console.print(Rule(f"[bold]{i}. {kind}[/bold]", style="magenta"))
|
|
159
|
+
|
|
160
|
+
# Your StateChange implements __rich__ nicely.
|
|
161
|
+
console.print(ch)
|
|
162
|
+
|
|
163
|
+
if show_meta:
|
|
164
|
+
meta = getattr(ch, "meta", None)
|
|
165
|
+
if isinstance(meta, str) and meta.strip():
|
|
166
|
+
console.print()
|
|
167
|
+
console.print(Panel(Markdown(meta), title="rationale", border_style="cyan"))
|
|
168
|
+
|
|
169
|
+
console.print()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@app.command("run")
|
|
173
|
+
def run_cmd(
|
|
174
|
+
name: str = typer.Argument(..., help="Demo name."),
|
|
175
|
+
):
|
|
176
|
+
"""
|
|
177
|
+
Run the demo from scratch (apply all changes) and overwrite demo/state.json.
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
demo = load_demo(DEFAULT_DEMOS_DIR, name)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
console.print(f'Failed to load in the demo: {str(e)}', style="bold red")
|
|
183
|
+
raise typer.Exit(2)
|
|
184
|
+
|
|
185
|
+
changes: list[StateChange] = load_changes(demo)
|
|
186
|
+
|
|
187
|
+
console.print(Panel(Text(demo.name, style="bold"), title="Running demo", border_style="magenta"))
|
|
188
|
+
state = _build_state_from_changes(demo_root=demo.root, changes=changes, console=console)
|
|
189
|
+
|
|
190
|
+
cache_path = _cache_path(demo.root)
|
|
191
|
+
_save_cached_state(state, cache_path)
|
|
192
|
+
console.print(Text(f"Saved cache: {cache_path}", style="dim"))
|
|
193
|
+
|
|
194
|
+
console.print(Rule("[bold]Result[/bold]", style="magenta"))
|
|
195
|
+
|
|
196
|
+
insts = getattr(state, "instances", None) or []
|
|
197
|
+
if insts:
|
|
198
|
+
latest = insts[0]
|
|
199
|
+
console.print(Text("Latest instance:", style="bold"))
|
|
200
|
+
|
|
201
|
+
# ---- Presentation layer (summary panel) ----
|
|
202
|
+
try:
|
|
203
|
+
from speclogician.presentation.ctx import RenderCtx
|
|
204
|
+
from speclogician.presentation.builders.state_instance_summary import (
|
|
205
|
+
build_state_instance_summary_pm,
|
|
206
|
+
)
|
|
207
|
+
from speclogician.presentation.renderers.state_instance_summary import (
|
|
208
|
+
render_state_instance_summary,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
ctx = RenderCtx(
|
|
212
|
+
art_cont=getattr(latest, "art_container", None),
|
|
213
|
+
art_map=getattr(latest, "art_map", None),
|
|
214
|
+
)
|
|
215
|
+
pm = build_state_instance_summary_pm(latest, ctx)
|
|
216
|
+
console.print(render_state_instance_summary(pm, ctx=ctx))
|
|
217
|
+
except Exception as e:
|
|
218
|
+
# If presentation breaks, don’t break demos; fallback to raw rich/repr.
|
|
219
|
+
console.print(Text(f"[dim]Summary renderer failed: {e}[/dim]"))
|
|
220
|
+
console.print(latest.__rich__() if hasattr(latest, "__rich__") else repr(latest))
|
|
221
|
+
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
console.print(Text("State applied.", style="yellow"))
|
|
225
|
+
|
|
226
|
+
@app.command('shell')
|
|
227
|
+
def shell_cmd(
|
|
228
|
+
name: str = typer.Argument(..., help='Demo name.'),
|
|
229
|
+
):
|
|
230
|
+
"""
|
|
231
|
+
Start an interactive Python shell using the cached demo state (demo/state.json).
|
|
232
|
+
If missing, it will be built once and saved.
|
|
233
|
+
"""
|
|
234
|
+
try:
|
|
235
|
+
demo = load_demo(DEFAULT_DEMOS_DIR, name)
|
|
236
|
+
except Exception as e:
|
|
237
|
+
console.print(f'Failed to load in the demo: {str(e)}', style="bold red")
|
|
238
|
+
raise typer.Exit(2)
|
|
239
|
+
|
|
240
|
+
changes: list[StateChange] = load_changes(demo)
|
|
241
|
+
|
|
242
|
+
cache_path = _ensure_cache_exists(demo_root=demo.root, changes=changes, console=console)
|
|
243
|
+
state = _load_cached_state(cache_path)
|
|
244
|
+
|
|
245
|
+
import code
|
|
246
|
+
banner = f"SpecLogician demo '{name}' loaded from cache. `state` is available."
|
|
247
|
+
code.interact(banner=banner, local={"state": state})
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@app.command("tui")
|
|
251
|
+
def tui_cmd(
|
|
252
|
+
name: str = typer.Argument(..., help='Demo name.'),
|
|
253
|
+
):
|
|
254
|
+
"""
|
|
255
|
+
Launch the Textual TUI using the cached demo state (demo/state.json).
|
|
256
|
+
If missing, it will be built once and saved.
|
|
257
|
+
"""
|
|
258
|
+
try:
|
|
259
|
+
demo = load_demo(DEFAULT_DEMOS_DIR, name)
|
|
260
|
+
except Exception as e:
|
|
261
|
+
console.print(f'Failed to load in the demo: {str(e)}', style="bold red")
|
|
262
|
+
raise typer.Exit(2)
|
|
263
|
+
|
|
264
|
+
artifact_path = demo.artifacts_path
|
|
265
|
+
|
|
266
|
+
if not artifact_path.exists():
|
|
267
|
+
console.print(Text(f'No artifact.md at {artifact_path}', style="yellow"))
|
|
268
|
+
raise typer.Exit(1)
|
|
269
|
+
|
|
270
|
+
artifact_cmd = artifact_path.read_text(encoding='utf-8', errors='replace')
|
|
271
|
+
|
|
272
|
+
changes: list[StateChange] = load_changes(demo)
|
|
273
|
+
|
|
274
|
+
cache_path = _ensure_cache_exists(demo_root=demo.root, changes=changes, console=console)
|
|
275
|
+
state = _load_cached_state(cache_path)
|
|
276
|
+
|
|
277
|
+
from speclogician.tui.main_screen import SpecLogicianApp
|
|
278
|
+
SpecLogicianApp(state=state, demo_md=artifact_cmd, demo_title=f'Demo: {name}').run()
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Imandra Inc.
|
|
3
|
+
#
|
|
4
|
+
# speclogician/demos/loader.py
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib.util
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
|
|
13
|
+
from speclogician.demos.model import DemoSpec
|
|
14
|
+
from speclogician.state.state import State
|
|
15
|
+
from speclogician.state.change import StateChange
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# -----------------------------------------------------------------------------
|
|
19
|
+
# discovery / selection
|
|
20
|
+
# -----------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
def discover_demos(demos_dir: Path) -> list[DemoSpec]:
|
|
23
|
+
demos: list[DemoSpec] = []
|
|
24
|
+
if not demos_dir.exists():
|
|
25
|
+
return demos
|
|
26
|
+
|
|
27
|
+
for p in sorted(demos_dir.iterdir()):
|
|
28
|
+
if not p.is_dir():
|
|
29
|
+
continue
|
|
30
|
+
if (p / "changes.py").exists():
|
|
31
|
+
demos.append(DemoSpec(name=p.name, root=p))
|
|
32
|
+
return demos
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_demo(demos_dir: Path, name: str) -> DemoSpec:
|
|
36
|
+
root = demos_dir / name
|
|
37
|
+
if not root.exists() or not root.is_dir():
|
|
38
|
+
raise FileNotFoundError(f'demo not found: {name}')
|
|
39
|
+
if not (root / "changes.py").exists():
|
|
40
|
+
raise FileNotFoundError('demo missing changes.py')
|
|
41
|
+
return DemoSpec(name=name, root=root)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# -----------------------------------------------------------------------------
|
|
45
|
+
# changes.py loader
|
|
46
|
+
# -----------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
def _load_module_from_path(py_path: Path, module_name: str) -> Any:
|
|
49
|
+
spec = importlib.util.spec_from_file_location(module_name, str(py_path))
|
|
50
|
+
if spec is None or spec.loader is None:
|
|
51
|
+
raise RuntimeError(f"Could not import: {py_path}")
|
|
52
|
+
mod = importlib.util.module_from_spec(spec)
|
|
53
|
+
spec.loader.exec_module(mod) # type: ignore[attr-defined]
|
|
54
|
+
return mod
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load_changes(demo: DemoSpec) -> list[StateChange]:
|
|
58
|
+
"""
|
|
59
|
+
Returns the list in `CHANGES` from the demo's changes.py.
|
|
60
|
+
Kept as `Any` to avoid importing all StateChange subclasses here.
|
|
61
|
+
"""
|
|
62
|
+
mod = _load_module_from_path(
|
|
63
|
+
demo.changes_path,
|
|
64
|
+
module_name=f"speclogician_demo_{demo.name}",
|
|
65
|
+
)
|
|
66
|
+
if not hasattr(mod, "CHANGES"):
|
|
67
|
+
raise RuntimeError(f"{demo.changes_path} must define CHANGES = [...]")
|
|
68
|
+
changes = getattr(mod, "CHANGES")
|
|
69
|
+
if not isinstance(changes, list):
|
|
70
|
+
raise RuntimeError(f"{demo.changes_path}: CHANGES must be a list")
|
|
71
|
+
return changes
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def load_demo_name(demo: DemoSpec) -> str:
|
|
75
|
+
mod = _load_module_from_path(
|
|
76
|
+
demo.changes_path,
|
|
77
|
+
module_name=f"speclogician_demo_name_{demo.name}",
|
|
78
|
+
)
|
|
79
|
+
return getattr(mod, "DEMO_NAME", demo.name)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# -----------------------------------------------------------------------------
|
|
83
|
+
# cache (state.json in demo folder)
|
|
84
|
+
# -----------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
def load_cached_state(demo: DemoSpec) -> State:
|
|
87
|
+
"""
|
|
88
|
+
Used by `speclogician demos tui <name>` and `... shell <name>`.
|
|
89
|
+
|
|
90
|
+
Strict: requires demo.state.json to exist.
|
|
91
|
+
"""
|
|
92
|
+
p = demo.cache_path
|
|
93
|
+
if not p.exists():
|
|
94
|
+
raise FileNotFoundError(
|
|
95
|
+
f"Demo cache missing: {p}\n"
|
|
96
|
+
f"Expected a JSON-serialized State model at {p.name}."
|
|
97
|
+
)
|
|
98
|
+
return State.model_validate_json(p.read_text(encoding="utf-8"))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def save_cached_state(demo: DemoSpec, state: State) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Writes demo.state.json (State model JSON) into the demo directory.
|
|
104
|
+
"""
|
|
105
|
+
demo.root.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
demo.cache_path.write_text(
|
|
107
|
+
state.model_dump_json(indent=2),
|
|
108
|
+
encoding="utf-8",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# -----------------------------------------------------------------------------
|
|
113
|
+
# execution (always run changes from empty state)
|
|
114
|
+
# -----------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
ApplyChangeFn = Callable[[State, Any], State]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def run_changes_fresh(demo: DemoSpec, *, apply_change: ApplyChangeFn) -> State:
|
|
120
|
+
"""
|
|
121
|
+
Used by `speclogician demos run <name>`.
|
|
122
|
+
|
|
123
|
+
Always starts from an empty State and applies every change from changes.py.
|
|
124
|
+
|
|
125
|
+
`apply_change` is injected to keep this module independent of the engine
|
|
126
|
+
implementation. Typical usage:
|
|
127
|
+
|
|
128
|
+
def apply_change(state: State, ch: StateChange) -> State:
|
|
129
|
+
return state.process_change(ch)
|
|
130
|
+
|
|
131
|
+
"""
|
|
132
|
+
state = State()
|
|
133
|
+
for ch in load_changes(demo):
|
|
134
|
+
state = apply_change(state, ch)
|
|
135
|
+
return state
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Imandra Inc.
|
|
3
|
+
#
|
|
4
|
+
# speclogician/demos/model.py
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class DemoSpec:
|
|
14
|
+
name: str
|
|
15
|
+
root: Path
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def artifacts_path(self) -> Path:
|
|
19
|
+
return self.root / "artifacts.md"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def description_path(self) -> Path:
|
|
23
|
+
return self.root / "description.md"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def changes_path(self) -> Path:
|
|
27
|
+
return self.root / "changes.py"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Imandra Inc.
|
|
3
|
+
#
|
|
4
|
+
# speclogician/demos/runner.py
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
from contextlib import nullcontext
|
|
12
|
+
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.status import Status
|
|
15
|
+
|
|
16
|
+
from speclogician.state.state import State
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class DemoRunResult:
|
|
21
|
+
state: State
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def apply_demo_changes(
|
|
25
|
+
*,
|
|
26
|
+
changes: list[Any],
|
|
27
|
+
console: Console,
|
|
28
|
+
initial_state: Optional[State] = None,
|
|
29
|
+
show_progress: bool = True,
|
|
30
|
+
) -> DemoRunResult:
|
|
31
|
+
"""
|
|
32
|
+
Apply an ordered list of StateChange objects to a State.
|
|
33
|
+
"""
|
|
34
|
+
state = initial_state or State()
|
|
35
|
+
|
|
36
|
+
if show_progress:
|
|
37
|
+
status_ctx = Status(
|
|
38
|
+
"[bold cyan]Applying demo changes…[/bold cyan]",
|
|
39
|
+
console=console,
|
|
40
|
+
spinner="dots",
|
|
41
|
+
)
|
|
42
|
+
else:
|
|
43
|
+
status_ctx = nullcontext() # type: ignore[name-defined]
|
|
44
|
+
|
|
45
|
+
with status_ctx:
|
|
46
|
+
for i, ch in enumerate(changes, start=1):
|
|
47
|
+
if show_progress:
|
|
48
|
+
status_ctx.update(f"[bold cyan]Applying change {i}/{len(changes)}:[/bold cyan] {ch.short_str()}")
|
|
49
|
+
state.process_change(ch, json_only=False)
|
|
50
|
+
|
|
51
|
+
return DemoRunResult(state=state)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Imandra Inc.
|
|
3
|
+
#
|
|
4
|
+
# speclogician/logic/api/__init__.py
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
from .client import SpecLogicianImandraX
|
|
8
|
+
from .decomp import (
|
|
9
|
+
decomp_complement, decomp_intersection
|
|
10
|
+
)
|
|
11
|
+
from .scenario import (
|
|
12
|
+
check_all_preds, check_action_preds, check_state_preds, check_intersection
|
|
13
|
+
)
|
|
14
|
+
from .traces import check_trace_iml_validity, check_trace_match
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"SpecLogicianImandraX",
|
|
19
|
+
"decomp",
|
|
20
|
+
"scenario",
|
|
21
|
+
"check_trace_iml_validity",
|
|
22
|
+
"check_trace_match",
|
|
23
|
+
"check_all_preds",
|
|
24
|
+
"check_action_preds",
|
|
25
|
+
"check_state_preds",
|
|
26
|
+
"check_intersection",
|
|
27
|
+
"decomp_complement",
|
|
28
|
+
"decomp_intersection"
|
|
29
|
+
]
|