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,257 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/tui/app.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import time
11
+ from datetime import datetime, timezone
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ import pyperclip
16
+ from textual.app import App
17
+ from textual.screen import Screen
18
+
19
+ from ..state.state import State
20
+ from ..utils.load import load_state
21
+ from .demo import DemoScreen
22
+ from .splash_screen import SplashScreen
23
+ from .stats_screen import StatsScreen
24
+ from .main_screen import MainScreen
25
+
26
+
27
+ class SpecLogicianApp(App[Any]):
28
+ """SpecLogician TUI app with optional file-watch + manual update."""
29
+
30
+ TITLE = "Imandra SpecLogician"
31
+ SUB_TITLE = "AI-powered spec synthesis, verification & analysis"
32
+
33
+ CSS_PATH = "app.tcss"
34
+
35
+ SCREENS = {
36
+ "splash": SplashScreen,
37
+ "demo": DemoScreen,
38
+ "stats": StatsScreen,
39
+ "main": MainScreen,
40
+ }
41
+
42
+ BINDINGS = [
43
+ ("s", "stats", "Statistics"),
44
+ ("m", "main", "Main"),
45
+ ("d", "demo", "Demo Artifacts"),
46
+ ("c", "copy_full_iml", "Copy IML"),
47
+ ("u", "update_state", "Update State"),
48
+ ("q", "quit", "Quit"),
49
+ ]
50
+
51
+ def __init__(
52
+ self,
53
+ state: State | None = None,
54
+ demo_md: str | None = None,
55
+ demo_title: str = "Demo artifacts",
56
+ *,
57
+ state_path: Path | None = None,
58
+ watch_state_file: bool = False,
59
+ poll_interval_s: float = 0.5,
60
+ ) -> None:
61
+ super().__init__()
62
+
63
+ self.demo_md = demo_md
64
+ self.demo_title = demo_title
65
+
66
+ self.state_path = state_path
67
+ self.watch_state_file = watch_state_file
68
+ self.poll_interval_s = poll_interval_s
69
+
70
+ # manual-update mode fields
71
+ self._pending_state_sig: tuple[int, int] | None = None
72
+ self._update_available: bool = False
73
+
74
+ # state
75
+ self.state = load_state() if state is None else state
76
+
77
+ # file watch bookkeeping
78
+ self._state_sig: tuple[int, int] | None = None
79
+ self._last_reload_ts: float = 0.0
80
+
81
+ # one-shot popup
82
+ self._notified_update_available: bool = False
83
+
84
+ # -------------------------------------------------------------------------
85
+ # mount + nav
86
+ # -------------------------------------------------------------------------
87
+
88
+ def on_mount(self) -> None:
89
+ # If demo isn't available,
90
+ # action_demo() will show a popup instead.
91
+
92
+ # start watch timer if enabled
93
+ if self.watch_state_file and self.state_path:
94
+ self._state_sig = self._get_state_file_signature()
95
+ self._set_last_updated_indicator(self._state_sig)
96
+ self.set_interval(self.poll_interval_s, self._poll_state_file)
97
+ else:
98
+ self._set_last_updated_indicator(None)
99
+
100
+ self.push_screen("splash")
101
+
102
+ def action_stats(self) -> None:
103
+ self.switch_screen("stats")
104
+
105
+ def action_main(self) -> None:
106
+ self.switch_screen("main")
107
+
108
+ def action_demo(self) -> None:
109
+ # If not in demo mode, show a popup (mirrors "No update available")
110
+ if not self.demo_md:
111
+ self.notify("No demo artifacts available (run in demo mode to enable).", timeout=2.0)
112
+ return
113
+ self.push_screen(DemoScreen(self.demo_md, title=self.demo_title))
114
+
115
+ def action_noop(self) -> None:
116
+ pass
117
+
118
+ # -------------------------------------------------------------------------
119
+ # copy helper
120
+ # -------------------------------------------------------------------------
121
+
122
+ def action_copy_full_iml(self) -> None:
123
+ screen: Screen[Any] = self.screen
124
+ if not isinstance(screen, MainScreen):
125
+ self.notify("Copy is only available on the Main screen.", timeout=1.0)
126
+ return
127
+
128
+ iml_text = getattr(screen, "_full_iml_text", "")
129
+ if not iml_text:
130
+ self.notify("No IML model to copy.", timeout=1.0)
131
+ return
132
+
133
+ pyperclip.copy(iml_text)
134
+ self.notify("Full IML model copied to clipboard.", timeout=1.2)
135
+
136
+ # -------------------------------------------------------------------------
137
+ # state apply / refresh
138
+ # -------------------------------------------------------------------------
139
+
140
+ def _apply_new_state(self, new_state: State) -> None:
141
+ self.state = new_state
142
+
143
+ screen: Screen[Any] = self.screen
144
+ if isinstance(screen, MainScreen):
145
+ screen.refresh_from_state()
146
+
147
+ # -------------------------------------------------------------------------
148
+ # file watching (polling) + subtitle indicator
149
+ # -------------------------------------------------------------------------
150
+
151
+ def _fmt_mtime(self, mtime_ns: int) -> str:
152
+ dt = datetime.fromtimestamp(mtime_ns / 1e9, tz=timezone.utc)
153
+ return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
154
+
155
+ def _set_last_updated_indicator(self, sig: tuple[int, int] | None) -> None:
156
+ base = "AI-powered spec synthesis, verification & analysis"
157
+ if not self.watch_state_file or not self.state_path or sig is None:
158
+ self.sub_title = base
159
+ return
160
+ mtime_ns, _ = sig
161
+ self.sub_title = f"{base} • state updated {self._fmt_mtime(mtime_ns)}"
162
+
163
+ def _set_update_indicator_pending(self, sig: tuple[int, int]) -> None:
164
+ base = "AI-powered spec synthesis, verification & analysis"
165
+ mtime_ns, _ = sig
166
+ self.sub_title = f"{base} • update available ({self._fmt_mtime(mtime_ns)})"
167
+
168
+ def _get_state_file_signature(self) -> tuple[int, int] | None:
169
+ if not self.state_path:
170
+ return None
171
+ try:
172
+ st = os.stat(self.state_path)
173
+ return (st.st_mtime_ns, st.st_size)
174
+ except FileNotFoundError:
175
+ return None
176
+
177
+ def _poll_state_file(self) -> None:
178
+ if not self.watch_state_file or not self.state_path:
179
+ return
180
+
181
+ sig = self._get_state_file_signature()
182
+ if sig is None:
183
+ return
184
+
185
+ if self._state_sig is None:
186
+ self._state_sig = sig
187
+ self._set_last_updated_indicator(sig)
188
+ return
189
+
190
+ if sig == self._state_sig:
191
+ return
192
+
193
+ # file changed: mark pending update (manual)
194
+ self._pending_state_sig = sig
195
+ self._update_available = True
196
+ self._set_update_indicator_pending(sig)
197
+
198
+ # update footer status if MainScreen is visible
199
+ screen: Screen[Any] = self.screen
200
+ if isinstance(screen, MainScreen):
201
+ mtime_ns, _ = sig
202
+ screen.set_status(
203
+ f"[yellow]Update available[/yellow] — press [bold]u[/bold] to reload ({self._fmt_mtime(mtime_ns)})"
204
+ )
205
+
206
+ # one-time popup hint
207
+ if not self._notified_update_available:
208
+ self._notified_update_available = True
209
+ self.notify("Update available — press 'u' to reload.", timeout=2.0)
210
+
211
+ # -------------------------------------------------------------------------
212
+ # manual update action
213
+ # -------------------------------------------------------------------------
214
+
215
+ def action_update_state(self) -> None:
216
+ if not self._update_available or not self.state_path or not self._pending_state_sig:
217
+ self.notify("No update available.", timeout=1.0)
218
+ return
219
+
220
+ # Debounce: avoid repeated clicks in quick succession
221
+ now = time.time()
222
+ if now - self._last_reload_ts < 0.25:
223
+ return
224
+
225
+ try:
226
+ data = self.state_path.read_text(encoding="utf-8", errors="replace")
227
+ new_state = State.model_validate_json(data) # type: ignore[attr-defined]
228
+ except Exception:
229
+ self.notify(
230
+ "Failed to load updated state (file may be mid-write).",
231
+ severity="warning",
232
+ timeout=1.5,
233
+ )
234
+ return
235
+
236
+ self._last_reload_ts = now
237
+
238
+ # accept update
239
+ self._state_sig = self._pending_state_sig
240
+ self._pending_state_sig = None
241
+ self._update_available = False
242
+
243
+ # update subtitle back to last-updated
244
+ self._set_last_updated_indicator(self._state_sig)
245
+
246
+ # apply + refresh UI
247
+ self._apply_new_state(new_state)
248
+
249
+ # allow the popup again next time a new update arrives
250
+ self._notified_update_available = False
251
+
252
+ # clear footer status if on main
253
+ screen: Screen[Any] = self.screen
254
+ if isinstance(screen, MainScreen):
255
+ screen.clear_status()
256
+
257
+ self.notify("State updated.", timeout=1.2)
@@ -0,0 +1,160 @@
1
+ /* Root screen should have real height */
2
+ Screen {
3
+ height: 100%;
4
+ }
5
+
6
+ #status_line {
7
+ dock: bottom;
8
+ height: auto;
9
+ padding: 0 1;
10
+ color: $text-muted;
11
+ background: $panel;
12
+ }
13
+
14
+ /* -------------------------------
15
+ Top toolbar (Update button)
16
+ -------------------------------- */
17
+
18
+ #top_toolbar {
19
+ height: auto;
20
+ width: 100%;
21
+ padding: 0 1;
22
+ margin-bottom: 1;
23
+ align: left middle;
24
+ }
25
+
26
+ #update_badge {
27
+ padding-left: 1;
28
+ color: $text-muted;
29
+ }
30
+
31
+ /* -------------------------------
32
+ Body layout
33
+ -------------------------------- */
34
+
35
+ #body {
36
+ width: 100%;
37
+ height: 100%;
38
+ layout: vertical;
39
+ }
40
+
41
+ #instances_split {
42
+ width: 100%;
43
+ height: 1fr;
44
+ layout: horizontal;
45
+ }
46
+
47
+ /* -------------------------------
48
+ Left: instances panel
49
+ -------------------------------- */
50
+
51
+ #instances_panel {
52
+ width: 80; /* adjust as needed */
53
+ height: 1fr;
54
+ overflow: hidden; /* key: prevents visual spill */
55
+ }
56
+
57
+ #instances_list {
58
+ width: 100%;
59
+ height: 1fr;
60
+ border: solid gray round;
61
+ overflow: hidden; /* also key */
62
+ }
63
+
64
+ /* -------------------------------
65
+ Instance list rows + badges
66
+ -------------------------------- */
67
+
68
+ .inst_row {
69
+ width: 100%;
70
+ height: auto;
71
+ }
72
+
73
+ .inst_when {
74
+ width: 1fr;
75
+ padding: 0 1;
76
+ }
77
+
78
+ .inst_badges {
79
+ width: auto;
80
+ height: auto;
81
+ align: right middle;
82
+ }
83
+
84
+ /* badge “pill” look */
85
+ .inst_badge {
86
+ padding: 0 1;
87
+ margin-left: 1;
88
+ text-style: bold;
89
+ /* If your Textual version supports it, these make it look nice.
90
+ If any of these error, remove them. */
91
+ border: round $panel;
92
+ background: $panel;
93
+ color: $text-muted;
94
+ }
95
+
96
+ /* severity colors (foreground) */
97
+ .inst_badge--error {
98
+ color: $error;
99
+ }
100
+
101
+ .inst_badge--warning {
102
+ color: $warning;
103
+ }
104
+
105
+ .inst_badge--ok {
106
+ color: $success;
107
+ }
108
+
109
+ .inst_badge--info {
110
+ color: $accent;
111
+ }
112
+
113
+ .inst_badge--muted {
114
+ color: $text-muted;
115
+ }
116
+
117
+ /* -------------------------------
118
+ Right: tabbed content
119
+ -------------------------------- */
120
+
121
+ TabbedContent {
122
+ width: 1fr;
123
+ height: 1fr;
124
+ }
125
+
126
+ TabPane {
127
+ width: 1fr;
128
+ height: 1fr;
129
+ }
130
+
131
+ /* -------------------------------
132
+ RichLog content
133
+ -------------------------------- */
134
+
135
+ RichLog {
136
+ width: 100%;
137
+ height: 1fr;
138
+ }
139
+
140
+ /* -------------------------------
141
+ Full IML panel
142
+ -------------------------------- */
143
+
144
+ #full_iml_panel {
145
+ width: 1fr;
146
+ height: 1fr;
147
+ layout: vertical;
148
+ }
149
+
150
+ #full_iml_toolbar {
151
+ height: auto;
152
+ width: 100%;
153
+ padding: 0 1;
154
+ align: left middle;
155
+ }
156
+
157
+ #full_iml_model {
158
+ width: 100%;
159
+ height: 1fr;
160
+ }
@@ -0,0 +1,45 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/tui/demo.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+ from textual.screen import Screen
11
+ from textual.app import ComposeResult
12
+ from textual.widgets import Header, Footer, RichLog
13
+
14
+ from rich.markdown import Markdown
15
+ from rich.panel import Panel
16
+
17
+
18
+ class DemoScreen(Screen[Any]):
19
+ """Shows demo collateral (artifact markdown) before entering the main TUI."""
20
+
21
+ CSS = """
22
+ #mdlog { height: 1fr; width: 100%; }
23
+ """
24
+
25
+ def __init__(self, markdown_text: str, *, title: str = "Demo artifacts") -> None:
26
+ super().__init__()
27
+ self._md_text = markdown_text
28
+ self._title = title
29
+
30
+ def compose(self) -> ComposeResult:
31
+ yield Header()
32
+ self.mdlog = RichLog(id="mdlog", wrap=True, highlight=True, markup=False, auto_scroll=False)
33
+ yield self.mdlog
34
+ yield Footer()
35
+
36
+ def on_mount(self) -> None:
37
+ # Render markdown inside a panel and write it as a single renderable.
38
+ self.mdlog.clear()
39
+ self.mdlog.write(
40
+ Panel(
41
+ Markdown(self._md_text or "_(empty)_"),
42
+ title=f"[bold]{self._title}[/bold]",
43
+ border_style="magenta",
44
+ )
45
+ )