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,454 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/tui/main_screen.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from rich.console import Group
12
+ from rich.panel import Panel
13
+ from rich.syntax import Syntax
14
+ from rich.text import Text
15
+
16
+ from textual.app import ComposeResult
17
+ from textual.containers import Container, Horizontal
18
+ from textual.screen import Screen
19
+ from textual.widgets import (
20
+ Footer,
21
+ Header,
22
+ ListItem,
23
+ ListView,
24
+ RichLog,
25
+ Static,
26
+ TabbedContent,
27
+ TabPane,
28
+ )
29
+
30
+ from ..data.refs import DocRef, SrcCodeRef
31
+ from ..data.traces import LogTrace, TestTrace
32
+ from ..modeling.predicates import ActionPredicate, StatePredicate, Transition
33
+ from ..modeling.scenario import Scenario
34
+ from ..state.inst import StateInstance
35
+ from ..presentation.api import (
36
+ present_data_artifact,
37
+ present_predicate,
38
+ present_scenario,
39
+ present_state_instance,
40
+ present_trace,
41
+ present_transition,
42
+ present_scenario_complement, # NEW
43
+ )
44
+ from ..presentation.ctx import RenderCtx
45
+
46
+ ctx = RenderCtx()
47
+
48
+
49
+ class InstanceItem(ListItem):
50
+ """Single-line list entry for a StateInstance."""
51
+
52
+ def __init__(self, inst: StateInstance) -> None:
53
+ self.inst = inst
54
+ super().__init__(Static(self._render_line()))
55
+
56
+ def _render_line(self) -> str:
57
+ """
58
+ Format:
59
+ 2025-02-03 14:22:11 | base=OK sc(miss=1 unsat=0) rec(e=1 w=2 n=3)
60
+ """
61
+
62
+ # --- timestamp ---
63
+ ts = self.inst.created_at.strftime("%Y-%m-%d %H:%M:%S")
64
+
65
+ parts: list[str] = []
66
+
67
+ # --- base status ---
68
+ base_status = None
69
+ try:
70
+ base_status = self.inst.spec.domain_model.base_status
71
+ except Exception:
72
+ pass
73
+
74
+ if base_status is not None:
75
+ parts.append(f"base={base_status.name}")
76
+ else:
77
+ parts.append("base=—")
78
+
79
+ # --- scenario health (from state_diff if present) ---
80
+ sd = getattr(self.inst, "state_diff", None)
81
+ if sd is not None:
82
+ miss = getattr(getattr(sd, "num_sc_missing", None), "after", None)
83
+ unsat = getattr(getattr(sd, "num_sc_inconsistent", None), "after", None)
84
+
85
+ if isinstance(miss, int) or isinstance(unsat, int):
86
+ parts.append(f"sc(miss={miss or 0} unsat={unsat or 0})")
87
+
88
+ # --- recommendations summary ---
89
+ recs = getattr(self.inst, "recommendations", None)
90
+ if recs is not None:
91
+ e = getattr(recs, "num_error", 0) or 0
92
+ w = getattr(recs, "num_warning", 0) or 0
93
+ n = getattr(recs, "num_next", 0) or 0
94
+ parts.append(f"rec(e={e} w={w} n={n})")
95
+
96
+ return f"{ts} | " + " ".join(parts)
97
+
98
+
99
+ class MainScreen(Screen[Any]):
100
+ """Main TUI screen."""
101
+
102
+ def compose(self) -> ComposeResult:
103
+ yield Header()
104
+
105
+ with Container(id="body"):
106
+ # ----- split: instances list + details -----
107
+ with Horizontal(id="instances_split"):
108
+
109
+ # left: instances list only
110
+ with Container(id="instances_panel"):
111
+ self.instances_list = ListView(id="instances_list")
112
+ yield self.instances_list
113
+
114
+ # ----- right: tabbed details -----
115
+ with TabbedContent(initial="summary"):
116
+ with TabPane("Summary", id="summary"):
117
+ self.instance_summary = RichLog(
118
+ id="instance_summary",
119
+ wrap=False,
120
+ highlight=True,
121
+ auto_scroll=False,
122
+ )
123
+ yield self.instance_summary
124
+
125
+ with TabPane("Full IML Model", id="model"):
126
+ with Container(id="full_iml_panel"):
127
+ with Horizontal(id="full_iml_toolbar"):
128
+ yield Static("Press 'c' to copy", id="copy_hint")
129
+
130
+ self.full_iml_model = RichLog(
131
+ id="full_iml_model",
132
+ wrap=False,
133
+ highlight=True,
134
+ auto_scroll=False,
135
+ )
136
+ yield self.full_iml_model
137
+
138
+ with TabPane("Predicates", id="predicates"):
139
+ self.predicates = RichLog(
140
+ id="predicates_view",
141
+ wrap=True,
142
+ highlight=True,
143
+ auto_scroll=False,
144
+ )
145
+ yield self.predicates
146
+
147
+ with TabPane("Transitions", id="transitions"):
148
+ self.transitions = RichLog(
149
+ id="transitions_view",
150
+ wrap=True,
151
+ highlight=True,
152
+ auto_scroll=False,
153
+ )
154
+ yield self.transitions
155
+
156
+ with TabPane("Scenarios", id="scenarios"):
157
+ self.scenarios = RichLog(
158
+ id="scenarios_view",
159
+ wrap=True,
160
+ highlight=True,
161
+ auto_scroll=False,
162
+ )
163
+ yield self.scenarios
164
+
165
+ # NEW: Scenario complement tab
166
+ with TabPane("Complement", id="complement"):
167
+ self.complement = RichLog(
168
+ id="complement_view",
169
+ wrap=True,
170
+ highlight=True,
171
+ auto_scroll=False,
172
+ )
173
+ yield self.complement
174
+
175
+ with TabPane("Test Traces", id="test_traces"):
176
+ self.test_traces = RichLog(
177
+ id="test_traces_view",
178
+ wrap=True,
179
+ highlight=True,
180
+ auto_scroll=False,
181
+ )
182
+ yield self.test_traces
183
+
184
+ with TabPane("Log Traces", id="log_traces"):
185
+ self.log_traces = RichLog(
186
+ id="log_traces_view",
187
+ wrap=True,
188
+ highlight=True,
189
+ auto_scroll=False,
190
+ )
191
+ yield self.log_traces
192
+
193
+ with TabPane("Src Artifacts", id="src_artifacts"):
194
+ self.src_refs = RichLog(
195
+ id="src_artifacts_view",
196
+ wrap=True,
197
+ highlight=True,
198
+ auto_scroll=False,
199
+ )
200
+ yield self.src_refs
201
+
202
+ with TabPane("Doc Artifacts", id="doc_artifacts"):
203
+ self.doc_refs = RichLog(
204
+ id="doc_artifacts_view",
205
+ wrap=True,
206
+ highlight=True,
207
+ auto_scroll=False,
208
+ )
209
+ yield self.doc_refs
210
+
211
+ # --- status line above Footer ---
212
+ self.status_line = Static("", id="status_line")
213
+ yield self.status_line
214
+
215
+ yield Footer()
216
+
217
+ # -------------------------------------------------------------------------
218
+ # UI plumbing
219
+ # -------------------------------------------------------------------------
220
+
221
+ def set_status(self, msg: str = "") -> None:
222
+ """Set footer status line text."""
223
+ self.status_line.update(msg)
224
+
225
+ def clear_status(self) -> None:
226
+ self.set_status("")
227
+
228
+ def refresh_from_state(self) -> None:
229
+ """
230
+ Rebuild instances list from self.app.state and reselect an instance.
231
+ """
232
+ prev_created_at: str | None = None
233
+ try:
234
+ if self.instances_list.index is not None and self.instances_list.index >= 0:
235
+ item = self.instances_list.children[self.instances_list.index]
236
+ if isinstance(item, InstanceItem):
237
+ prev_created_at = item.inst.created_at.isoformat()
238
+ except Exception:
239
+ prev_created_at = None
240
+
241
+ self.instances_list.clear()
242
+ for inst in self.app.state.instances:
243
+ self.instances_list.append(InstanceItem(inst))
244
+
245
+ if not self.app.state.instances:
246
+ self.clear_status()
247
+ return
248
+
249
+ idx = 0
250
+ if prev_created_at is not None:
251
+ for i, inst in enumerate(self.app.state.instances):
252
+ if inst.created_at.isoformat() == prev_created_at:
253
+ idx = i
254
+ break
255
+
256
+ self.instances_list.index = idx
257
+ self._select_instance(self.app.state.instances[idx])
258
+
259
+ # reflect current update-available status
260
+ if getattr(self.app, "_update_available", False):
261
+ sig = getattr(self.app, "_pending_state_sig", None)
262
+ if sig is not None:
263
+ mtime_ns, _ = sig
264
+ self.set_status(
265
+ f"[yellow]Update available[/yellow] — press [bold]u[/bold] to reload ({self.app._fmt_mtime(mtime_ns)})"
266
+ )
267
+ else:
268
+ self.clear_status()
269
+
270
+ # -------------------------------------------------------------------------
271
+ # rendering helpers
272
+ # -------------------------------------------------------------------------
273
+
274
+ def _show_instance_summary(self, inst: StateInstance) -> None:
275
+ self.instance_summary.clear()
276
+ self.instance_summary.write(present_state_instance(inst, ctx=ctx, json_only=False))
277
+ self.instance_summary.scroll_to(y=0, animate=False, immediate=True, force=True)
278
+
279
+ def _show_full_iml_model(self, iml_code: str) -> None:
280
+ self._full_iml_text = iml_code or ""
281
+ self.full_iml_model.clear()
282
+ self.full_iml_model.write(Syntax(self._full_iml_text, "ocaml", line_numbers=True, word_wrap=False))
283
+
284
+ def _show_predicates(
285
+ self,
286
+ state_preds: list[StatePredicate],
287
+ action_preds: list[ActionPredicate],
288
+ ) -> None:
289
+ self.predicates.clear()
290
+
291
+ state_r = [present_predicate(p, ctx=ctx, json_only=False) for p in state_preds]
292
+ action_r = [present_predicate(p, ctx=ctx, json_only=False) for p in action_preds]
293
+
294
+ self.predicates.write(
295
+ Panel(
296
+ Group(*state_r) if state_r else Text("— none —"),
297
+ title=f"State predicates ({len(state_r)})",
298
+ )
299
+ )
300
+
301
+ self.predicates.write(
302
+ Panel(
303
+ Group(*action_r) if action_r else Text("— none —"),
304
+ title=f"Action predicates ({len(action_r)})",
305
+ )
306
+ )
307
+ self.predicates.scroll_home(animate=False)
308
+
309
+ def _show_transitions(self, transitions: list[Transition]) -> None:
310
+ self.transitions.clear()
311
+ trs = [present_transition(t, ctx=ctx, json_only=False) for t in transitions]
312
+ self.transitions.write(
313
+ Panel(
314
+ Group(*trs) if trs else Text("— none —"),
315
+ title=f"Transitions ({len(trs)})",
316
+ )
317
+ )
318
+ self.transitions.scroll_home(animate=False)
319
+
320
+ def _show_scenarios(self, scenarios: list[Scenario]) -> None:
321
+ self.scenarios.clear()
322
+ scs = [present_scenario(s, ctx=ctx, json_only=False) for s in scenarios]
323
+ self.scenarios.write(
324
+ Panel(
325
+ Group(*scs) if scs else Text("— none —"),
326
+ title=f"Scenarios ({len(scs)})",
327
+ )
328
+ )
329
+ self.scenarios.scroll_home(animate=False)
330
+
331
+ def _show_scenario_complement(self, inst: StateInstance) -> None:
332
+ """
333
+ Render spec.scenario_comp via the new presentation API.
334
+ """
335
+ self.complement.clear()
336
+
337
+ comp = getattr(getattr(inst, "spec", None), "scenario_comp", None)
338
+ if comp is None:
339
+ self.complement.write(Panel(Text("— no scenario complement —", style="dim"), title="Complement"))
340
+ self.complement.scroll_home(animate=False)
341
+ return
342
+
343
+ try:
344
+ rend = present_scenario_complement(comp, ctx=ctx, json_only=False)
345
+ except Exception as e:
346
+ self.complement.write(
347
+ Panel(
348
+ Text(f"Failed to render complement: {type(e).__name__}: {e}", style="red"),
349
+ title="Complement",
350
+ )
351
+ )
352
+ self.complement.scroll_home(animate=False)
353
+ return
354
+
355
+ # `rend` is a rich renderable
356
+ self.complement.write(rend)
357
+ self.complement.scroll_home(animate=False)
358
+
359
+ def _show_test_traces(self, test_traces: list[TestTrace]) -> None:
360
+ self.test_traces.clear()
361
+ tts = [present_trace(tt, ctx=ctx, json_only=False) for tt in test_traces]
362
+ self.test_traces.write(
363
+ Panel(
364
+ Group(*tts) if tts else Text("— none —"),
365
+ title=f"Test Traces ({len(tts)})",
366
+ )
367
+ )
368
+ self.test_traces.scroll_home(animate=False)
369
+
370
+ def _show_log_traces(self, log_traces: list[LogTrace]) -> None:
371
+ self.log_traces.clear()
372
+ lts = [present_trace(lt, ctx=ctx, json_only=False) for lt in log_traces]
373
+ self.log_traces.write(
374
+ Panel(
375
+ Group(*lts) if lts else Text("— none —"),
376
+ title=f"Log Traces ({len(lts)})",
377
+ )
378
+ )
379
+ self.log_traces.scroll_home(animate=False, immediate=True)
380
+
381
+ def _show_src_artifacts(self, src_refs: list[SrcCodeRef]) -> None:
382
+ self.src_refs.clear()
383
+ srefs = [present_data_artifact(sr, ctx=ctx, json_only=False) for sr in src_refs]
384
+ self.src_refs.write(
385
+ Panel(
386
+ Group(*srefs) if srefs else Text("— none —"),
387
+ title=f"Source Code References ({len(srefs)})",
388
+ )
389
+ )
390
+ self.src_refs.scroll_home(animate=False, immediate=True)
391
+
392
+ def _show_doc_artifacts(self, doc_refs: list[DocRef]) -> None:
393
+ self.doc_refs.clear()
394
+ drefs = [present_data_artifact(dr, ctx=ctx, json_only=False) for dr in doc_refs]
395
+ self.doc_refs.write(
396
+ Panel(
397
+ Group(*drefs) if drefs else Text("— none —"),
398
+ title=f"Documentation References ({len(drefs)})",
399
+ )
400
+ )
401
+ self.doc_refs.scroll_home(animate=False, immediate=True)
402
+
403
+ def _select_instance(self, inst: StateInstance) -> None:
404
+ self._show_instance_summary(inst)
405
+ self._show_full_iml_model(inst.spec.full_model())
406
+ self._show_predicates(inst.spec.domain_model.state_preds, inst.spec.domain_model.action_preds)
407
+ self._show_transitions(inst.spec.domain_model.transitions)
408
+ self._show_scenarios(inst.spec.scenarios)
409
+ self._show_scenario_complement(inst) # NEW
410
+ self._show_test_traces(inst.art_container.test_traces)
411
+ self._show_log_traces(inst.art_container.log_traces)
412
+ self._show_src_artifacts(inst.art_container.src_code)
413
+ self._show_doc_artifacts(inst.art_container.doc_ref)
414
+
415
+ # -------------------------------------------------------------------------
416
+ # events
417
+ # -------------------------------------------------------------------------
418
+
419
+ def on_mount(self) -> None:
420
+ for inst in self.app.state.instances:
421
+ self.instances_list.append(InstanceItem(inst))
422
+
423
+ if self.app.state and self.app.state.instances:
424
+ self.instances_list.index = 0
425
+ self._select_instance(self.app.state.instances[0])
426
+
427
+ self.instances_list.border_title = "State Instances"
428
+
429
+ # reflect update status on first mount
430
+ if getattr(self.app, "_update_available", False):
431
+ sig = getattr(self.app, "_pending_state_sig", None)
432
+ if sig is not None:
433
+ mtime_ns, _ = sig
434
+ self.set_status(
435
+ f"[yellow]Update available[/yellow] — press [bold]u[/bold] to reload ({self.app._fmt_mtime(mtime_ns)})"
436
+ )
437
+ else:
438
+ self.clear_status()
439
+
440
+ def action_show_tab(self, tab: str) -> None:
441
+ tc = self.query_one(TabbedContent)
442
+ tc.active = tab
443
+
444
+ def on_list_view_highlighted(self, event: ListView.Highlighted) -> None:
445
+ if event.list_view is not self.instances_list:
446
+ return
447
+ if isinstance(event.item, InstanceItem):
448
+ self._select_instance(event.item.inst)
449
+
450
+ def on_list_view_selected(self, event: ListView.Selected) -> None:
451
+ if event.list_view is not self.instances_list:
452
+ return
453
+ if isinstance(event.item, InstanceItem):
454
+ self._select_instance(event.item.inst)
@@ -0,0 +1,51 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/tui/splash.py
5
+ #
6
+
7
+ from typing import Any
8
+ from pathlib import Path
9
+ from textual.screen import Screen
10
+ from textual.containers import Container
11
+ from textual.app import ComposeResult
12
+ from textual.widgets import Static
13
+ from textual_image.widget import Image
14
+ from rich.align import Align
15
+ from rich.text import Text
16
+
17
+ class SplashScreen(Screen[Any]):
18
+ CSS = """
19
+ SplashScreen {
20
+ align: center middle;
21
+ }
22
+
23
+ #logo {
24
+ margin-bottom: 1;
25
+ }
26
+
27
+ #status {
28
+ width: auto;
29
+ content-align: center middle;
30
+ }
31
+
32
+ """
33
+ LOGO_PATH = Path(__file__).parent / "images" / "speclogician-minimal.png"
34
+ def compose(self) -> ComposeResult:
35
+ with Container(id="logo") as c:
36
+ c.border_title = "width: auto; height: auto;"
37
+ yield Image(self.LOGO_PATH, classes="width-auto height-auto")
38
+
39
+ yield Static(
40
+ Align.center(Text("Loading…", style="dim")),
41
+ id="status",
42
+ )
43
+
44
+ def on_mount(self) -> None:
45
+ def go_next() -> None:
46
+ if getattr(self.app, "demo_md", None):
47
+ self.app.action_demo()
48
+ else:
49
+ self.app.switch_screen("main")
50
+
51
+ self.set_timer(2.0, go_next)
@@ -0,0 +1,125 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/tui/stats.py
5
+ #
6
+
7
+ from typing import Any
8
+ from textual.screen import Screen
9
+ from textual.app import ComposeResult
10
+ from textual.widgets import Header, Footer
11
+ from textual.containers import Container
12
+ from textual_plotext import PlotextPlot
13
+
14
+ from ..state.state_stats import StatsCalculator
15
+
16
+
17
+ class StatsScreen(Screen[Any]):
18
+ CSS = """
19
+ #body { height: 1fr; }
20
+
21
+ /* 2x2 grid */
22
+ #grid {
23
+ height: 1fr;
24
+ layout: grid;
25
+ grid-size: 2 2;
26
+ grid-gutter: 1 1;
27
+ }
28
+
29
+ PlotextPlot {
30
+ height: 1fr;
31
+ width: 1fr;
32
+ }
33
+ """
34
+
35
+ def compose(self) -> ComposeResult:
36
+ yield Header()
37
+ with Container(id="body"):
38
+ with Container(id="grid"):
39
+ self.plot_model = PlotextPlot()
40
+ self.plot_scenarios = PlotextPlot()
41
+ self.plot_artifacts = PlotextPlot()
42
+ self.plot_ratios = PlotextPlot()
43
+
44
+ yield self.plot_model
45
+ yield self.plot_scenarios
46
+ yield self.plot_artifacts
47
+ yield self.plot_ratios
48
+ yield Footer()
49
+
50
+ def on_mount(self) -> None:
51
+ # If you want live updating while running:
52
+ # self.set_interval(0.5, self.refresh_plot)
53
+ self.refresh_plot()
54
+
55
+ @staticmethod
56
+ def _safe_ratio_series(num: list[int], den: list[int]) -> list[float]:
57
+ out: list[float] = []
58
+ for n, d in zip(num, den):
59
+ out.append(n / (d if d else 1))
60
+ return out
61
+
62
+ def refresh_plot(self) -> None:
63
+
64
+ v = StatsCalculator.stats(self.app.state.instances)
65
+
66
+ # oldest -> newest already
67
+ xs = list(range(len(v["created_at"])))
68
+
69
+ # -------------------------
70
+ # 1) Model growth & usage
71
+ # -------------------------
72
+ plt = self.plot_model.plt
73
+ plt.clear_data()
74
+ plt.title("Model growth & usage")
75
+ plt.xlabel("Instance (oldest → newest)")
76
+ plt.ylabel("count")
77
+ plt.plot(xs, v["num_preds_total"])
78
+ plt.plot(xs, v["num_preds_matched"])
79
+ plt.plot(xs, v["num_trans_total"])
80
+ plt.scatter(xs, v["num_preds_matched"])
81
+ self.plot_model.refresh()
82
+
83
+ # -------------------------
84
+ # 2) Scenario health
85
+ # -------------------------
86
+ plt = self.plot_scenarios.plt
87
+ plt.clear_data()
88
+ plt.title("Scenario health")
89
+ plt.xlabel("Instance (oldest → newest)")
90
+ plt.ylabel("count")
91
+ plt.plot(xs, v["num_sc_total"])
92
+ plt.plot(xs, v["num_sc_matched"])
93
+ plt.plot(xs, v["num_sc_missing"])
94
+ plt.plot(xs, v["num_sc_inconsistent"])
95
+ plt.plot(xs, v["num_sc_conflicted"])
96
+ self.plot_scenarios.refresh()
97
+
98
+ # -------------------------
99
+ # 3) Artifact pipeline quality
100
+ # -------------------------
101
+ plt = self.plot_artifacts.plt
102
+ plt.clear_data()
103
+ plt.title("Artifacts: total → logic-good → matched")
104
+ plt.xlabel("Instance (oldest → newest)")
105
+ plt.ylabel("count")
106
+ plt.plot(xs, v["num_traces_total"])
107
+ plt.plot(xs, v["num_traces_logic_good"])
108
+ plt.plot(xs, v["num_traces_matched"])
109
+ plt.scatter(xs, v["num_traces_matched"])
110
+ self.plot_artifacts.refresh()
111
+
112
+ # -------------------------
113
+ # 4) Coverage ratios
114
+ # -------------------------
115
+ plt = self.plot_ratios.plt
116
+ plt.clear_data()
117
+ plt.title("Coverage ratios")
118
+ plt.xlabel("Instance (oldest → newest)")
119
+ plt.ylabel("ratio (0..1)")
120
+ preds_ratio = self._safe_ratio_series(v["num_preds_matched"], v["num_preds_total"])
121
+ traces_ratio = self._safe_ratio_series(v["num_traces_matched"], v["num_traces_total"])
122
+ plt.plot(xs, preds_ratio)
123
+ plt.plot(xs, traces_ratio)
124
+ plt.scatter(xs, preds_ratio)
125
+ self.plot_ratios.refresh()