speclogician 0.0.0b1__py3-none-any.whl → 0.0.0.dev1__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/agent/funcs.py +29 -0
- speclogician/cmd/agent_cmd.py +89 -0
- speclogician/cmd/data_cmd.py +24 -0
- speclogician/cmd/model_cmd.py +42 -0
- speclogician/cmd/overlay_cmd.py +30 -0
- speclogician/cmd/scenario_cmd.py +61 -0
- speclogician/cmd/state_cmd.py +52 -0
- speclogician/data/artifact.py +8 -50
- speclogician/data/container.py +18 -384
- speclogician/data/mapping.py +18 -17
- speclogician/data/refs.py +12 -11
- speclogician/data/reports.py +11 -0
- speclogician/data/traces.py +15 -6
- speclogician/llms/llmtools.py +102 -0
- speclogician/llms/overlay.py +264 -0
- speclogician/main.py +36 -102
- speclogician/modeling/__init__.py +0 -31
- speclogician/modeling/component.py +4 -60
- speclogician/modeling/conflict.py +5 -19
- speclogician/modeling/domain.py +93 -280
- speclogician/modeling/model.py +206 -0
- speclogician/modeling/predicates.py +20 -22
- speclogician/modeling/report.py +33 -0
- speclogician/modeling/scenario.py +119 -87
- speclogician/sl_cmd.py +76 -0
- speclogician/state/change.py +98 -378
- speclogician/state/state.py +183 -399
- speclogician/tui/box.tcss +10 -0
- speclogician/tui/tui.py +131 -0
- speclogician/utils/__init__.py +1 -70
- speclogician/utils/imx.py +195 -0
- speclogician/utils/load.py +25 -147
- speclogician/utils/prompt.md +1 -325
- speclogician-0.0.0.dev1.dist-info/METADATA +21 -0
- speclogician-0.0.0.dev1.dist-info/RECORD +43 -0
- speclogician/commands/__init__.py +0 -15
- speclogician/commands/cmd_ch.py +0 -616
- speclogician/commands/cmd_find.py +0 -256
- speclogician/commands/cmd_view.py +0 -202
- speclogician/commands/runner.py +0 -149
- speclogician/commands/utils.py +0 -101
- speclogician/demos/.DS_Store +0 -0
- speclogician/demos/cmd_demo.py +0 -278
- speclogician/demos/loader.py +0 -135
- speclogician/demos/model.py +0 -27
- speclogician/demos/runner.py +0 -51
- speclogician/logic/__init__.py +0 -11
- speclogician/logic/api/__init__.py +0 -29
- speclogician/logic/api/client.py +0 -606
- speclogician/logic/api/decomp.py +0 -67
- speclogician/logic/api/scenario.py +0 -102
- speclogician/logic/api/traces.py +0 -59
- speclogician/logic/lib/__init__.py +0 -19
- speclogician/logic/lib/complement.py +0 -107
- speclogician/logic/lib/domain_model.py +0 -59
- speclogician/logic/lib/predicates.py +0 -151
- speclogician/logic/lib/scenarios.py +0 -369
- speclogician/logic/lib/traces.py +0 -114
- speclogician/logic/lib/transitions.py +0 -104
- speclogician/logic/main.py +0 -246
- speclogician/logic/strings.py +0 -194
- speclogician/logic/utils.py +0 -135
- speclogician/modeling/complement.py +0 -104
- speclogician/modeling/spec.py +0 -306
- speclogician/modeling/spec_stats.py +0 -39
- speclogician/presentation/api.py +0 -244
- speclogician/presentation/builders/_links.py +0 -44
- speclogician/presentation/builders/container.py +0 -53
- speclogician/presentation/builders/data_artifact.py +0 -42
- speclogician/presentation/builders/domain.py +0 -54
- speclogician/presentation/builders/instances_list.py +0 -38
- speclogician/presentation/builders/predicate.py +0 -51
- speclogician/presentation/builders/recommendations.py +0 -41
- speclogician/presentation/builders/scenario.py +0 -41
- speclogician/presentation/builders/scenario_complement.py +0 -82
- speclogician/presentation/builders/smart_find.py +0 -39
- speclogician/presentation/builders/spec.py +0 -39
- speclogician/presentation/builders/state_diff.py +0 -150
- speclogician/presentation/builders/state_instance.py +0 -42
- speclogician/presentation/builders/state_instance_summary.py +0 -84
- speclogician/presentation/builders/trace.py +0 -58
- speclogician/presentation/ctx.py +0 -38
- speclogician/presentation/models/container.py +0 -44
- speclogician/presentation/models/data_artifact.py +0 -33
- speclogician/presentation/models/domain.py +0 -50
- speclogician/presentation/models/instances_list.py +0 -23
- speclogician/presentation/models/predicate.py +0 -60
- speclogician/presentation/models/recommendations.py +0 -34
- speclogician/presentation/models/scenario.py +0 -31
- speclogician/presentation/models/scenario_complement.py +0 -40
- speclogician/presentation/models/smart_find.py +0 -34
- speclogician/presentation/models/spec.py +0 -32
- speclogician/presentation/models/state_diff.py +0 -34
- speclogician/presentation/models/state_instance.py +0 -31
- speclogician/presentation/models/state_instance_summary.py +0 -102
- speclogician/presentation/models/trace.py +0 -42
- speclogician/presentation/preview/__init__.py +0 -13
- speclogician/presentation/preview/cli.py +0 -50
- speclogician/presentation/preview/fixtures/__init__.py +0 -205
- speclogician/presentation/preview/fixtures/artifact_container.py +0 -150
- speclogician/presentation/preview/fixtures/data_artifact.py +0 -144
- speclogician/presentation/preview/fixtures/domain_model.py +0 -162
- speclogician/presentation/preview/fixtures/instances_list.py +0 -162
- speclogician/presentation/preview/fixtures/predicate.py +0 -184
- speclogician/presentation/preview/fixtures/scenario.py +0 -84
- speclogician/presentation/preview/fixtures/scenario_complement.py +0 -81
- speclogician/presentation/preview/fixtures/smart_find.py +0 -140
- speclogician/presentation/preview/fixtures/spec.py +0 -95
- speclogician/presentation/preview/fixtures/state_diff.py +0 -158
- speclogician/presentation/preview/fixtures/state_instance.py +0 -128
- speclogician/presentation/preview/fixtures/state_instance_summary.py +0 -80
- speclogician/presentation/preview/fixtures/trace.py +0 -206
- speclogician/presentation/preview/registry.py +0 -42
- speclogician/presentation/renderers/__init__.py +0 -24
- speclogician/presentation/renderers/container.py +0 -136
- speclogician/presentation/renderers/data_artifact.py +0 -144
- speclogician/presentation/renderers/domain.py +0 -123
- speclogician/presentation/renderers/instances_list.py +0 -120
- speclogician/presentation/renderers/predicate.py +0 -180
- speclogician/presentation/renderers/recommendations.py +0 -90
- speclogician/presentation/renderers/scenario.py +0 -94
- speclogician/presentation/renderers/scenario_complement.py +0 -59
- speclogician/presentation/renderers/smart_find.py +0 -307
- speclogician/presentation/renderers/spec.py +0 -105
- speclogician/presentation/renderers/state_diff.py +0 -102
- speclogician/presentation/renderers/state_instance.py +0 -82
- speclogician/presentation/renderers/state_instance_summary.py +0 -143
- speclogician/presentation/renderers/trace.py +0 -122
- speclogician/shell/app.py +0 -170
- speclogician/shell/shell_ch.py +0 -263
- speclogician/shell/shell_view.py +0 -153
- speclogician/state/change_result.py +0 -32
- speclogician/state/diff.py +0 -191
- speclogician/state/inst.py +0 -574
- speclogician/state/recommendation.py +0 -13
- speclogician/state/recommender.py +0 -577
- speclogician/state/state_stats.py +0 -133
- speclogician/tui/__init__.py +0 -0
- speclogician/tui/app.py +0 -257
- speclogician/tui/app.tcss +0 -160
- speclogician/tui/demo.py +0 -45
- speclogician/tui/images/speclogician-full.png +0 -0
- speclogician/tui/images/speclogician-minimal.png +0 -0
- speclogician/tui/main_screen.py +0 -454
- speclogician/tui/splash_screen.py +0 -51
- speclogician/tui/stats_screen.py +0 -125
- speclogician/utils/testing.py +0 -151
- speclogician-0.0.0b1.dist-info/METADATA +0 -116
- speclogician-0.0.0b1.dist-info/RECORD +0 -139
- /speclogician/{presentation → agent}/__init__.py +0 -0
- /speclogician/{presentation/builders → cmd}/__init__.py +0 -0
- /speclogician/{presentation/models → llms}/__init__.py +0 -0
- {speclogician-0.0.0b1.dist-info → speclogician-0.0.0.dev1.dist-info}/WHEEL +0 -0
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Imandra Inc.
|
|
3
|
-
#
|
|
4
|
-
# speclogician/state/state_stats.py
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
from dataclasses import dataclass
|
|
10
|
-
|
|
11
|
-
from .inst import StateInstance
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass(frozen=True)
|
|
15
|
-
class StatsCalculator:
|
|
16
|
-
"""
|
|
17
|
-
Compute time-series stats over a collection of State objects.
|
|
18
|
-
|
|
19
|
-
Assumptions:
|
|
20
|
-
- Each State has `instances` ordered newest-first (index 0 = latest).
|
|
21
|
-
- We return lists oldest-first for plotting.
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
@staticmethod
|
|
25
|
-
def stats(instances: list[StateInstance]) -> dict[str, list[int | str]]:
|
|
26
|
-
"""
|
|
27
|
-
Note: we reverse the order of StateInstances here so the 0s element (latest state instance) is all the way on the right
|
|
28
|
-
"""
|
|
29
|
-
out: dict[str, list[int | str]] = {
|
|
30
|
-
# identity / time
|
|
31
|
-
"created_at": [],
|
|
32
|
-
"num_changes": [],
|
|
33
|
-
|
|
34
|
-
# ---- domain model counts (from DomainModel) ----
|
|
35
|
-
"num_state_preds_total": [],
|
|
36
|
-
"num_state_preds_valid_logic": [],
|
|
37
|
-
"num_state_preds_matched": [],
|
|
38
|
-
|
|
39
|
-
"num_action_preds_total": [],
|
|
40
|
-
"num_action_preds_valid_logic": [],
|
|
41
|
-
"num_action_preds_matched": [],
|
|
42
|
-
|
|
43
|
-
"num_preds_total": [],
|
|
44
|
-
"num_preds_valid_logic": [],
|
|
45
|
-
"num_preds_matched": [],
|
|
46
|
-
|
|
47
|
-
"num_trans_total": [],
|
|
48
|
-
"num_trans_valid_logic": [],
|
|
49
|
-
"num_trans_matched": [],
|
|
50
|
-
|
|
51
|
-
# ---- scenario stats (from Spec) ----
|
|
52
|
-
"num_sc_total": [],
|
|
53
|
-
"num_sc_missing": [],
|
|
54
|
-
"num_sc_matched": [],
|
|
55
|
-
"num_sc_inconsistent": [],
|
|
56
|
-
"num_sc_conflicted": [],
|
|
57
|
-
|
|
58
|
-
# ---- artifact stats (from ArtifactContainer) ----
|
|
59
|
-
"num_test_traces_total": [],
|
|
60
|
-
"num_test_traces_matched": [],
|
|
61
|
-
"num_test_traces_logic_good": [],
|
|
62
|
-
|
|
63
|
-
"num_log_traces_total": [],
|
|
64
|
-
"num_log_traces_matched": [],
|
|
65
|
-
"num_log_traces_logic_good": [],
|
|
66
|
-
|
|
67
|
-
"num_doc_arts_total": [],
|
|
68
|
-
"num_doc_arts_matched": [],
|
|
69
|
-
"num_src_code_arts_total": [],
|
|
70
|
-
"num_src_code_arts_matched": [],
|
|
71
|
-
|
|
72
|
-
# ---- optional derived metrics (handy for “progress”) ----
|
|
73
|
-
"num_traces_total": [],
|
|
74
|
-
"num_traces_logic_good": [],
|
|
75
|
-
"num_traces_matched": [],
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
for inst in instances[::-1]:
|
|
79
|
-
# identity / time
|
|
80
|
-
out["created_at"].append(inst.created_at.isoformat())
|
|
81
|
-
out["num_changes"].append(len(inst.changes or []))
|
|
82
|
-
|
|
83
|
-
# domain model
|
|
84
|
-
dm = inst.spec.domain_model
|
|
85
|
-
out["num_state_preds_total"].append(dm.num_state_preds_total)
|
|
86
|
-
out["num_state_preds_valid_logic"].append(dm.num_state_preds_valid_logic)
|
|
87
|
-
out["num_state_preds_matched"].append(dm.num_state_preds_matched)
|
|
88
|
-
|
|
89
|
-
out["num_action_preds_total"].append(dm.num_action_preds_total)
|
|
90
|
-
out["num_action_preds_valid_logic"].append(dm.num_action_preds_valid_logic)
|
|
91
|
-
out["num_action_preds_matched"].append(dm.num_action_preds_matched)
|
|
92
|
-
|
|
93
|
-
out["num_preds_total"].append(dm.num_preds_total)
|
|
94
|
-
out["num_preds_valid_logic"].append(dm.num_preds_valid_logic)
|
|
95
|
-
out["num_preds_matched"].append(dm.num_preds_matched)
|
|
96
|
-
|
|
97
|
-
out["num_trans_total"].append(dm.num_trans_total)
|
|
98
|
-
out["num_trans_valid_logic"].append(dm.num_trans_valid_logic)
|
|
99
|
-
out["num_trans_matched"].append(dm.num_trans_matched)
|
|
100
|
-
|
|
101
|
-
# spec / scenarios
|
|
102
|
-
sp = inst.spec
|
|
103
|
-
out["num_sc_total"].append(sp.num_sc_total)
|
|
104
|
-
out["num_sc_missing"].append(sp.num_sc_missing)
|
|
105
|
-
out["num_sc_matched"].append(sp.num_sc_matched)
|
|
106
|
-
out["num_sc_inconsistent"].append(sp.num_sc_inconsistent)
|
|
107
|
-
out["num_sc_conflicted"].append(sp.num_sc_conflicted)
|
|
108
|
-
|
|
109
|
-
# artifacts
|
|
110
|
-
ac = inst.art_container
|
|
111
|
-
out["num_test_traces_total"].append(ac.num_test_traces_total)
|
|
112
|
-
out["num_test_traces_matched"].append(ac.num_test_traces_matched)
|
|
113
|
-
out["num_test_traces_logic_good"].append(ac.num_test_traces_logic_good)
|
|
114
|
-
|
|
115
|
-
out["num_log_traces_total"].append(ac.num_log_traces_total)
|
|
116
|
-
out["num_log_traces_matched"].append(ac.num_log_traces_matched)
|
|
117
|
-
out["num_log_traces_logic_good"].append(ac.num_log_traces_logic_good)
|
|
118
|
-
|
|
119
|
-
out["num_doc_arts_total"].append(ac.num_doc_arts_total)
|
|
120
|
-
out["num_doc_arts_matched"].append(ac.num_doc_arts_matched)
|
|
121
|
-
out["num_src_code_arts_total"].append(ac.num_src_code_arts_total)
|
|
122
|
-
out["num_src_code_arts_matched"].append(ac.num_src_code_arts_matched)
|
|
123
|
-
|
|
124
|
-
# derived
|
|
125
|
-
traces_total = ac.num_test_traces_total + ac.num_log_traces_total
|
|
126
|
-
traces_good = ac.num_test_traces_logic_good + ac.num_log_traces_logic_good
|
|
127
|
-
traces_matched = ac.num_test_traces_matched + ac.num_log_traces_matched
|
|
128
|
-
|
|
129
|
-
out["num_traces_total"].append(traces_total)
|
|
130
|
-
out["num_traces_logic_good"].append(traces_good)
|
|
131
|
-
out["num_traces_matched"].append(traces_matched)
|
|
132
|
-
|
|
133
|
-
return out
|
speclogician/tui/__init__.py
DELETED
|
File without changes
|
speclogician/tui/app.py
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
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)
|
speclogician/tui/app.tcss
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
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
|
-
}
|
speclogician/tui/demo.py
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
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
|
-
)
|
|
Binary file
|
|
Binary file
|