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,256 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/commands/find_cmd.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ import typer
12
+ from rich.table import Table
13
+
14
+ from .utils import get_state
15
+ from ..utils import console
16
+
17
+ app = typer.Typer(help="Find various components of the state")
18
+
19
+
20
+ # -----------------------------------------------------------------------------
21
+ # helpers
22
+ # -----------------------------------------------------------------------------
23
+
24
+ def _matches_component(q: str, *, name: str, src_code: str) -> bool:
25
+ qq = (q or "").lower()
26
+ return (qq in (name or "").lower()) or (qq in (src_code or "").lower())
27
+
28
+
29
+ def _emit_json(payload: dict[str, Any]) -> None:
30
+ # Keep stdout clean for machine mode (matches your convention elsewhere)
31
+ import json as _json
32
+ typer.echo(_json.dumps(payload, indent=2, sort_keys=True))
33
+
34
+
35
+ def _preview(src: str, n: int = 160) -> str:
36
+ return (src or "")[:n].replace("\n", " ")
37
+
38
+
39
+ # -----------------------------------------------------------------------------
40
+ # predicates
41
+ # -----------------------------------------------------------------------------
42
+
43
+ @app.command(name="predicates", help="Search state/action predicates by name or source text")
44
+ def search_predicates(
45
+ ctx: typer.Context,
46
+ q: str = typer.Argument(..., help="Search string (name or source snippet)"),
47
+ kind: str = typer.Option(
48
+ "all",
49
+ "--kind",
50
+ help="Search scope: all|state|action",
51
+ ),
52
+ json_only: bool = typer.Option(
53
+ False,
54
+ "--json",
55
+ help="Emit JSON only (no rich output)",
56
+ ),
57
+ ) -> None:
58
+ state = get_state(ctx=ctx)
59
+ dm = state.curr_state().spec.domain_model
60
+
61
+ k = (kind or "all").lower().strip()
62
+ if k not in ("all", "state", "action"):
63
+ raise typer.BadParameter("kind must be one of: all|state|action")
64
+
65
+ matches: list[dict[str, Any]] = []
66
+
67
+ if k in ("all", "state"):
68
+ for p in getattr(dm, "state_preds", []):
69
+ name = getattr(p, "name", "")
70
+ src_code = getattr(p, "src_code", "")
71
+ if _matches_component(q, name=name, src_code=src_code):
72
+ matches.append(
73
+ {
74
+ "type": "StatePredicate",
75
+ "kind": getattr(p, "kind", "state_predicate"),
76
+ "name": name,
77
+ "src_preview": _preview(src_code),
78
+ }
79
+ )
80
+
81
+ if k in ("all", "action"):
82
+ for p in getattr(dm, "action_preds", []):
83
+ name = getattr(p, "name", "")
84
+ src_code = getattr(p, "src_code", "")
85
+ if _matches_component(q, name=name, src_code=src_code):
86
+ matches.append(
87
+ {
88
+ "type": "ActionPredicate",
89
+ "kind": getattr(p, "kind", "action_predicate"),
90
+ "name": name,
91
+ "src_preview": _preview(src_code),
92
+ }
93
+ )
94
+
95
+ # Deterministic output (nice for tests)
96
+ matches.sort(key=lambda m: (m.get("type", ""), m.get("name", "")))
97
+
98
+ if json_only:
99
+ _emit_json(
100
+ {
101
+ "query": q,
102
+ "kind": k,
103
+ "count": len(matches),
104
+ "results": matches,
105
+ }
106
+ )
107
+ return
108
+
109
+ t = Table(title=f"Predicate search for {q!r}", show_header=True, header_style="bold")
110
+ t.add_column("Type", no_wrap=True)
111
+ t.add_column("Name", no_wrap=True)
112
+ t.add_column("Preview")
113
+ for m in matches:
114
+ prev = m["src_preview"]
115
+ t.add_row(m["type"], m["name"], prev + ("…" if len(prev) == 160 else ""))
116
+ console.print(t)
117
+
118
+
119
+ # -----------------------------------------------------------------------------
120
+ # transitions
121
+ # -----------------------------------------------------------------------------
122
+
123
+ @app.command(name="transitions", help="Search transition functions by name or source text")
124
+ def search_transitions(
125
+ ctx: typer.Context,
126
+ q: str = typer.Argument(..., help="Search string (name or source snippet)"),
127
+ json_only: bool = typer.Option(
128
+ False,
129
+ "--json",
130
+ help="Emit JSON only (no rich output)",
131
+ ),
132
+ ) -> None:
133
+ state = get_state(ctx=ctx)
134
+ dm = state.curr_state().spec.domain_model
135
+
136
+ matches: list[dict[str, Any]] = []
137
+
138
+ for tr in getattr(dm, "transitions", []):
139
+ name = getattr(tr, "name", "")
140
+ src_code = getattr(tr, "src_code", "")
141
+ if _matches_component(q, name=name, src_code=src_code):
142
+ matches.append(
143
+ {
144
+ "type": "Transition",
145
+ "kind": getattr(tr, "kind", "transition"),
146
+ "name": name,
147
+ "src_preview": _preview(src_code),
148
+ }
149
+ )
150
+
151
+ matches.sort(key=lambda m: m.get("name", ""))
152
+
153
+ if json_only:
154
+ _emit_json(
155
+ {
156
+ "query": q,
157
+ "count": len(matches),
158
+ "results": matches,
159
+ }
160
+ )
161
+ return
162
+
163
+ t = Table(title=f"Transition search for {q!r}", show_header=True, header_style="bold")
164
+ t.add_column("Name", no_wrap=True)
165
+ t.add_column("Preview")
166
+ for m in matches:
167
+ prev = m["src_preview"]
168
+ t.add_row(m["name"], prev + ("…" if len(prev) == 160 else ""))
169
+ console.print(t)
170
+
171
+
172
+ # -----------------------------------------------------------------------------
173
+ # artifacts (smart_find + PM renderer)
174
+ # -----------------------------------------------------------------------------
175
+
176
+ @app.command("artifacts")
177
+ def search_artifacts(
178
+ ctx: typer.Context,
179
+ q: str = typer.Argument(..., help="Search string"),
180
+ kind: str = typer.Option(
181
+ "all",
182
+ "--kind",
183
+ help="Search scope: all|tests|logs|docs|src",
184
+ ),
185
+ json_only: bool = typer.Option(
186
+ False,
187
+ "--json",
188
+ help="Emit JSON only (no rich output)",
189
+ ),
190
+ ids: bool = typer.Option(
191
+ False,
192
+ "--ids",
193
+ help="Show artifact IDs in rich output",
194
+ ),
195
+ compact: bool = typer.Option(
196
+ False,
197
+ "--compact",
198
+ help="Use compact tables/panels in rich output",
199
+ ),
200
+ show_empty: bool = typer.Option(
201
+ False,
202
+ "--show-empty",
203
+ help="Render empty sections in rich output",
204
+ ),
205
+ max_per_type: int = typer.Option(
206
+ 50,
207
+ "--max-per-type",
208
+ min=1,
209
+ help="Maximum results to show per artifact type",
210
+ ),
211
+ ) -> None:
212
+ """Smart text search across artifacts (tests/logs/docs/src)."""
213
+ state = get_state(ctx=ctx)
214
+ ac = state.curr_state().art_container
215
+
216
+ k = (kind or "all").lower().strip()
217
+ include: set[str] | None
218
+ if k in ("all", "*"):
219
+ include = None
220
+ elif k in ("tests", "test"):
221
+ include = {"test"}
222
+ elif k in ("logs", "log"):
223
+ include = {"log"}
224
+ elif k in ("docs", "doc"):
225
+ include = {"doc"}
226
+ elif k in ("src", "source"):
227
+ include = {"src"}
228
+ else:
229
+ raise typer.BadParameter("kind must be one of: all|tests|logs|docs|src")
230
+
231
+ # --- search ---
232
+ res = ac.smart_find(q, include=include, max_per_type=max_per_type)
233
+
234
+ # --- build PM + render ---
235
+ from speclogician.presentation.ctx import RenderCtx
236
+ from speclogician.presentation.builders.smart_find import build_smart_find_pm
237
+ from speclogician.presentation.renderers.smart_find import render_smart_find
238
+
239
+ rctx = RenderCtx(
240
+ show_art_ids=ids,
241
+ show_ids=ids,
242
+ compact=compact,
243
+ show_empty_sections=show_empty, # now a real field on RenderCtx
244
+ )
245
+
246
+ pm = build_smart_find_pm(res, rctx)
247
+
248
+ if json_only:
249
+ payload = pm.model_dump()
250
+ # test-friendly extra metadata
251
+ payload["query"] = q
252
+ payload["kind"] = k
253
+ _emit_json(payload)
254
+ return
255
+
256
+ console.print(render_smart_find(pm, ctx=rctx))
@@ -0,0 +1,202 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/cli/view_cmd.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from typing import Optional
11
+ import typer
12
+
13
+ from speclogician.presentation.ctx import RenderCtx
14
+ from speclogician.presentation.api import (
15
+ present_state_instance,
16
+ present_spec,
17
+ present_domain_model,
18
+ present_artifact_container,
19
+ present_state_diff,
20
+ present_instances_list,
21
+ )
22
+
23
+ from .utils import get_state
24
+
25
+ app = typer.Typer(help="View components of the current state (rich or JSON).")
26
+
27
+
28
+ def _mk_ctx(
29
+ *,
30
+ compact: bool,
31
+ show_stats_only: bool,
32
+ show_code: bool,
33
+ show_ids: bool,
34
+ show_art_ids: bool,
35
+ ) -> RenderCtx:
36
+ return RenderCtx(
37
+ compact=compact,
38
+ show_stats_only=show_stats_only,
39
+ show_code=show_code,
40
+ show_ids=show_ids,
41
+ show_art_ids=show_art_ids,
42
+ )
43
+
44
+
45
+ def _emit(obj: object, *, json_only: bool) -> None:
46
+ if json_only:
47
+ typer.echo(json.dumps(obj, indent=2, default=str))
48
+ else:
49
+ # rich renderables can be printed by your console; if you want,
50
+ # import speclogician.utils.console and console.print(obj)
51
+ from speclogician.utils import console
52
+ console.print(obj)
53
+
54
+
55
+ # ---------------------------
56
+ # view state (full)
57
+ # ---------------------------
58
+
59
+ @app.command("state")
60
+ def view_state(
61
+ ctx: typer.Context,
62
+ idx: Optional[int] = typer.Option(None, "--idx", help="State instance index (default: latest/0)"),
63
+ json_only: bool = typer.Option(False, "--json", help="Emit JSON only"),
64
+ compact: bool = typer.Option(False, "--compact"),
65
+ show_stats_only: bool = typer.Option(False, "--stats-only"),
66
+ show_code: bool = typer.Option(True, "--code/--no-code"),
67
+ show_ids: bool = typer.Option(False, "--ids"),
68
+ show_art_ids: bool = typer.Option(False, "--art-ids"),
69
+ ) -> None:
70
+ st = get_state(ctx)
71
+ i = 0 if idx is None else idx
72
+ si = st.instances[i]
73
+
74
+ rctx = _mk_ctx(
75
+ compact=compact,
76
+ show_stats_only=show_stats_only,
77
+ show_code=show_code,
78
+ show_ids=show_ids,
79
+ show_art_ids=show_art_ids,
80
+ )
81
+
82
+ out = present_state_instance(si, ctx=rctx, json_only=json_only)
83
+ _emit(out, json_only=json_only)
84
+
85
+
86
+ # ---------------------------
87
+ # view spec
88
+ # ---------------------------
89
+
90
+ @app.command("spec")
91
+ def view_spec_cmd(
92
+ ctx: typer.Context,
93
+ idx: Optional[int] = typer.Option(None, "--idx"),
94
+ json_only: bool = typer.Option(False, "--json"),
95
+ compact: bool = typer.Option(False, "--compact"),
96
+ show_stats_only: bool = typer.Option(False, "--stats-only"),
97
+ show_code: bool = typer.Option(True, "--code/--no-code"),
98
+ show_ids: bool = typer.Option(False, "--ids"),
99
+ show_art_ids: bool = typer.Option(False, "--art-ids"),
100
+ ) -> None:
101
+ st = get_state(ctx)
102
+ i = 0 if idx is None else idx
103
+ si = st.instances[i]
104
+
105
+ rctx = _mk_ctx(compact=compact, show_stats_only=show_stats_only, show_code=show_code, show_ids=show_ids, show_art_ids=show_art_ids)
106
+ out = present_spec(si.spec, ctx=rctx, json_only=json_only)
107
+ _emit(out, json_only=json_only)
108
+
109
+
110
+ # ---------------------------
111
+ # view domain model
112
+ # ---------------------------
113
+
114
+ @app.command("domain")
115
+ def view_domain_cmd(
116
+ ctx: typer.Context,
117
+ idx: Optional[int] = typer.Option(None, "--idx"),
118
+ json_only: bool = typer.Option(False, "--json"),
119
+ compact: bool = typer.Option(False, "--compact"),
120
+ show_stats_only: bool = typer.Option(False, "--stats-only"),
121
+ show_code: bool = typer.Option(True, "--code/--no-code"),
122
+ show_ids: bool = typer.Option(False, "--ids"),
123
+ show_art_ids: bool = typer.Option(False, "--art-ids"),
124
+ ) -> None:
125
+ st = get_state(ctx)
126
+ i = 0 if idx is None else idx
127
+ si = st.instances[i]
128
+
129
+ rctx = _mk_ctx(compact=compact, show_stats_only=show_stats_only, show_code=show_code, show_ids=show_ids, show_art_ids=show_art_ids)
130
+ out = present_domain_model(si.spec.domain_model, ctx=rctx, json_only=json_only)
131
+ _emit(out, json_only=json_only)
132
+
133
+
134
+ # ---------------------------
135
+ # view artifacts container
136
+ # ---------------------------
137
+
138
+ @app.command("artifacts")
139
+ def view_artifacts_cmd(
140
+ ctx: typer.Context,
141
+ idx: Optional[int] = typer.Option(None, "--idx"),
142
+ json_only: bool = typer.Option(False, "--json"),
143
+ compact: bool = typer.Option(False, "--compact"),
144
+ show_stats_only: bool = typer.Option(False, "--stats-only"),
145
+ show_code: bool = typer.Option(True, "--code/--no-code"),
146
+ show_ids: bool = typer.Option(False, "--ids"),
147
+ show_art_ids: bool = typer.Option(False, "--art-ids"),
148
+ ) -> None:
149
+ st = get_state(ctx)
150
+ i = 0 if idx is None else idx
151
+ si = st.instances[i]
152
+
153
+ rctx = _mk_ctx(compact=compact, show_stats_only=show_stats_only, show_code=show_code, show_ids=show_ids, show_art_ids=show_art_ids)
154
+ out = present_artifact_container(si.art_container, ctx=rctx, json_only=json_only)
155
+ _emit(out, json_only=json_only)
156
+
157
+
158
+ # ---------------------------
159
+ # view diff (latest instance must have it computed/stored, or compute outside)
160
+ # ---------------------------
161
+
162
+ @app.command("diff")
163
+ def view_diff_cmd(
164
+ ctx: typer.Context,
165
+ idx: Optional[int] = typer.Option(None, "--idx"),
166
+ json_only: bool = typer.Option(False, "--json"),
167
+ compact: bool = typer.Option(False, "--compact"),
168
+ show_stats_only: bool = typer.Option(False, "--stats-only"),
169
+ show_code: bool = typer.Option(True, "--code/--no-code"),
170
+ show_ids: bool = typer.Option(False, "--ids"),
171
+ show_art_ids: bool = typer.Option(False, "--art-ids"),
172
+ ) -> None:
173
+ st = get_state(ctx)
174
+ i = 0 if idx is None else idx
175
+ si = st.instances[i]
176
+
177
+ if si.state_diff is None:
178
+ raise typer.BadParameter("No state_diff available on this instance.")
179
+
180
+ rctx = _mk_ctx(compact=compact, show_stats_only=show_stats_only, show_code=show_code, show_ids=show_ids, show_art_ids=show_art_ids)
181
+ out = present_state_diff(si.state_diff, ctx=rctx, json_only=json_only)
182
+ _emit(out, json_only=json_only)
183
+
184
+
185
+ # ---------------------------
186
+ # view list of instances
187
+ # ---------------------------
188
+
189
+ @app.command("instances")
190
+ def view_instances_cmd(
191
+ ctx: typer.Context,
192
+ json_only: bool = typer.Option(False, "--json"),
193
+ compact: bool = typer.Option(False, "--compact"),
194
+ show_stats_only: bool = typer.Option(False, "--stats-only"),
195
+ show_code: bool = typer.Option(True, "--code/--no-code"),
196
+ show_ids: bool = typer.Option(False, "--ids"),
197
+ show_art_ids: bool = typer.Option(False, "--art-ids"),
198
+ ) -> None:
199
+ st = get_state(ctx)
200
+ rctx = _mk_ctx(compact=compact, show_stats_only=show_stats_only, show_code=show_code, show_ids=show_ids, show_art_ids=show_art_ids)
201
+ out = present_instances_list(st.instances, ctx=rctx, json_only=json_only)
202
+ _emit(out, json_only=json_only)
@@ -0,0 +1,149 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/commands/runner.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import typer
11
+ from enum import Enum
12
+ from datetime import datetime
13
+ from typing import NoReturn, Any
14
+ from rich.console import Console
15
+
16
+ from speclogician.state.change import StateChange
17
+ from speclogician.state.state import State # adjust import
18
+ from speclogician.state.change_result import (
19
+ ProcessChangeResult,
20
+ ProcessChangeError,
21
+ ProcessChangeSuccess,
22
+ )
23
+
24
+ from speclogician.presentation.ctx import RenderCtx
25
+ from speclogician.presentation.builders.state_instance import build_state_instance_pm
26
+ from speclogician.presentation.builders.state_diff import build_state_diff_pm
27
+
28
+ from speclogician.presentation.renderers.state_instance import render_state_instance
29
+ from speclogician.presentation.renderers.state_diff import render_state_diff # assuming you have it
30
+
31
+ console = Console()
32
+
33
+
34
+ def _dump_json(obj: Any) -> str:
35
+ def _default(o: Any):
36
+ if hasattr(o, "model_dump"):
37
+ return o.model_dump(mode="json")
38
+ if isinstance(o, Enum):
39
+ return o.value
40
+ if isinstance(o, datetime):
41
+ return o.isoformat()
42
+ raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
43
+
44
+ return json.dumps(
45
+ obj,
46
+ default=_default,
47
+ indent=2,
48
+ sort_keys=True,
49
+ )
50
+
51
+ def run_change_and_render(
52
+ st: State,
53
+ change: StateChange,
54
+ *,
55
+ json_only: bool,
56
+ ) -> NoReturn:
57
+ """
58
+ Unified runner for CLI commands.
59
+
60
+ - json_only=True: emits JSON PM payload to stdout (stable, parseable).
61
+ - json_only=False: pretty-prints PMs via Rich renderers.
62
+ """
63
+
64
+ # Always take the object path so we can drive presentation from PMs.
65
+ result_obj: ProcessChangeResult = st.process_change(change, json_only=json_only)
66
+ st.save()
67
+
68
+ # -------------------------
69
+ # Error branch
70
+ # -------------------------
71
+ if not result_obj["ok"]:
72
+ err_res: ProcessChangeError = result_obj
73
+
74
+ # Build ctx from the old state if possible
75
+ old_state = err_res["old_state"]
76
+ ctx = RenderCtx(
77
+ art_cont=old_state.art_container,
78
+ art_map=old_state.art_map,
79
+ )
80
+
81
+ old_pm = build_state_instance_pm(old_state, ctx=ctx)
82
+
83
+ if json:
84
+ payload = {
85
+ "ok": False,
86
+ "stage": err_res["stage"],
87
+ "change": err_res["change"].model_dump(mode="json"),
88
+ "error": {
89
+ "type": type(err_res["error"]).__name__,
90
+ "message": str(err_res["error"]),
91
+ },
92
+ "old_state": old_pm.model_dump(mode="json"),
93
+ }
94
+ typer.echo(_dump_json(payload))
95
+ raise typer.Exit(code=1)
96
+
97
+ # human output
98
+ console.print(f"[bold red]Error[/bold red] stage={err_res['stage']}")
99
+ console.print(f"[red]{type(err_res['error']).__name__}: {err_res['error']}[/red]\n")
100
+ console.print("[bold]Old state[/bold]")
101
+ console.print(render_state_instance(old_pm, ctx=ctx))
102
+ raise typer.Exit(code=1)
103
+
104
+ # -------------------------
105
+ # Success branch
106
+ # -------------------------
107
+ ok_res: ProcessChangeSuccess = result_obj
108
+
109
+ old_state = ok_res["old_state"]
110
+ new_state = ok_res["new_state"]
111
+ sd = ok_res["state_diff"]
112
+
113
+ # Prefer new-state context for presentation (has freshest art_map/art_cont)
114
+ ctx = RenderCtx(
115
+ art_cont=new_state.art_container,
116
+ art_map=new_state.art_map,
117
+ )
118
+
119
+ old_pm = build_state_instance_pm(old_state, ctx=ctx)
120
+ new_pm = build_state_instance_pm(new_state, ctx=ctx)
121
+ diff_pm = build_state_diff_pm(sd, ctx=ctx)
122
+
123
+ if json:
124
+ payload = {
125
+ "ok": True,
126
+ "stage": "ok",
127
+ "change": ok_res["change"].model_dump(mode="json"),
128
+ "old_state": old_pm.model_dump(mode="json"),
129
+ "new_state": new_pm.model_dump(mode="json"),
130
+ "state_diff": diff_pm.model_dump(mode="json"),
131
+ "new_num_instances": ok_res["new_num_instances"],
132
+ }
133
+ typer.echo(_dump_json(payload))
134
+ raise typer.Exit(code=0)
135
+
136
+ # human output
137
+ console.print("[bold]Old state[/bold]")
138
+ console.print(render_state_instance(old_pm, ctx=ctx))
139
+ console.print()
140
+
141
+ console.print("[bold]New state[/bold]")
142
+ console.print(render_state_instance(new_pm, ctx=ctx))
143
+ console.print()
144
+
145
+ console.print("[bold]Diff[/bold]")
146
+ console.print(render_state_diff(diff_pm, ctx=ctx))
147
+ console.print()
148
+
149
+ raise typer.Exit(code=0)
@@ -0,0 +1,101 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/commands/utils.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+ from collections.abc import Mapping, Sequence
9
+
10
+ import json
11
+ from pathlib import Path
12
+ from typing import TypeVar
13
+
14
+ import typer
15
+ from ..utils.load import load_state
16
+ from ..state.change import StateChange
17
+ from ..state.state import State
18
+
19
+ from ..modeling.scenario import ScenarioDelta
20
+
21
+ T = TypeVar("T", bound=StateChange)
22
+
23
+ def get_state(ctx: typer.Context) -> State:
24
+ state = ctx.obj.state
25
+ if state is None:
26
+ state = load_state()
27
+ return state
28
+
29
+ def normalize_string_escapes(v: str) -> str:
30
+ """
31
+ Convert escaped sequences like '\\n', '\\t' into actual characters.
32
+ """
33
+ try:
34
+ return v.encode("utf-8").decode("unicode_escape")
35
+ except Exception:
36
+ return v
37
+
38
+ def normalize_data(obj):
39
+ if isinstance(obj, str):
40
+ return normalize_string_escapes(obj)
41
+
42
+ if isinstance(obj, Mapping):
43
+ return {k: normalize_data(v) for k, v in obj.items()}
44
+
45
+ if isinstance(obj, Sequence) and not isinstance(obj, (str, bytes)):
46
+ return [normalize_data(v) for v in obj]
47
+
48
+ return obj
49
+
50
+
51
+ def parse_scenario_delta(value: str | None) -> ScenarioDelta | None:
52
+ """
53
+ Parse a ScenarioDelta from CLI input.
54
+
55
+ Accepted forms:
56
+ 1) None / "" -> None
57
+ 2) Comma-separated tokens:
58
+ "+p1,+p2,-p3" => add=["p1","p2"], remove=["p3"]
59
+ "p1,p2" => add=["p1","p2"], remove=[]
60
+ "-p3" => add=[], remove=["p3"]
61
+ 3) JSON:
62
+ '{"add":["p1"],"remove":["p2"]}'
63
+ or a path to a JSON file containing that object.
64
+
65
+ Validation (dedup, identifier check, overlap) is enforced by ScenarioDelta itself.
66
+ """
67
+ if value is None:
68
+ return None
69
+
70
+ s = value.strip()
71
+ if not s:
72
+ return None
73
+
74
+ # File path to JSON
75
+ p = Path(s)
76
+ if p.exists() and p.is_file():
77
+ data = json.loads(p.read_text())
78
+ delta = ScenarioDelta.model_validate(data)
79
+ return None if delta.is_empty() else delta
80
+
81
+ # Inline JSON
82
+ if s.startswith("{"):
83
+ data = json.loads(s)
84
+ delta = ScenarioDelta.model_validate(data)
85
+ return None if delta.is_empty() else delta
86
+
87
+ # Token format: "+a,+b,-c" or "a,b"
88
+ add: list[str] = []
89
+ remove: list[str] = []
90
+
91
+ for tok in (t.strip() for t in s.split(",") if t.strip()):
92
+ if tok.startswith("+"):
93
+ add.append(tok[1:].strip())
94
+ elif tok.startswith("-"):
95
+ remove.append(tok[1:].strip())
96
+ else:
97
+ # default: treat as add
98
+ add.append(tok)
99
+
100
+ delta = ScenarioDelta(add=add, remove=remove)
101
+ return None if delta.is_empty() else delta
File without changes