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,143 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # presentation/renderers/state_instance_summary.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ from rich.console import Group
10
+ from rich.panel import Panel
11
+ from rich.rule import Rule
12
+ from rich.text import Text
13
+
14
+ from speclogician.presentation.ctx import RenderCtx
15
+ from speclogician.presentation.models.state_instance_summary import StateInstanceSummaryPM
16
+
17
+
18
+ def render_state_instance_summary(pm: StateInstanceSummaryPM, *, ctx: RenderCtx):
19
+ def kv(k: str, v: object, *, k_w: int = 22) -> Text:
20
+ # left-align key, keep values readable; no fancy tables
21
+ return Text(f"{k:<{k_w}} {v}")
22
+
23
+ blocks: list[Text] = []
24
+
25
+ # Header
26
+ blocks.append(Text("State instance", style="bold"))
27
+ blocks.append(Rule(style="dim"))
28
+
29
+ # Top-level
30
+ blocks.append(kv("created_at:", pm.created_at))
31
+ blocks.append(kv("changes:", pm.num_changes))
32
+ blocks.append(Text(""))
33
+
34
+ # Domain model
35
+ blocks.append(Text("Domain model", style="bold"))
36
+ blocks.append(kv("base_status:", pm.base_status))
37
+ blocks.append(kv("has_state/action:", f"{pm.base_has_state} / {pm.base_has_action}"))
38
+
39
+ blocks.append(
40
+ kv(
41
+ "state preds:",
42
+ f"{pm.num_state_preds_total} total, "
43
+ f"{pm.num_state_preds_matched} matched, "
44
+ f"{pm.num_state_preds_valid_logic} logic-valid, "
45
+ f"{pm.num_state_preds_errored} errored",
46
+ )
47
+ )
48
+ blocks.append(
49
+ kv(
50
+ "action preds:",
51
+ f"{pm.num_action_preds_total} total, "
52
+ f"{pm.num_action_preds_matched} matched, "
53
+ f"{pm.num_action_preds_valid_logic} logic-valid, "
54
+ f"{pm.num_action_preds_errored} errored",
55
+ )
56
+ )
57
+ blocks.append(
58
+ kv(
59
+ "predicates:",
60
+ f"{pm.num_preds_total} total, "
61
+ f"{pm.num_preds_matched} matched, "
62
+ f"{pm.num_preds_valid_logic} logic-valid, "
63
+ f"{pm.num_preds_errored} errored",
64
+ )
65
+ )
66
+ blocks.append(
67
+ kv(
68
+ "transitions:",
69
+ f"{pm.num_trans_total} total, "
70
+ f"{pm.num_trans_matched} matched, "
71
+ f"{pm.num_trans_valid_logic} logic-valid, "
72
+ f"{pm.num_trans_errored} errored",
73
+ )
74
+ )
75
+ blocks.append(Text(""))
76
+
77
+ # Spec / scenarios
78
+ blocks.append(Text("Spec / scenarios", style="bold"))
79
+ blocks.append(
80
+ kv(
81
+ "scenarios:",
82
+ f"{pm.num_sc_total} total, "
83
+ f"{pm.num_sc_missing} missing, "
84
+ f"{pm.num_sc_matched} matched, "
85
+ f"{pm.num_sc_inconsistent} inconsistent",
86
+ )
87
+ )
88
+ blocks.append(
89
+ kv(
90
+ "conflicts:",
91
+ f"{pm.num_sc_conflicted} total, "
92
+ f"{pm.num_sc_overlap} overlap, "
93
+ f"{pm.num_sc_consumed} consumed",
94
+ )
95
+ )
96
+ blocks.append(Text(""))
97
+
98
+ # Artifacts
99
+ blocks.append(Text("Artifacts", style="bold"))
100
+ blocks.append(
101
+ kv(
102
+ "test traces:",
103
+ f"{pm.num_test_traces_total} total, "
104
+ f"{pm.num_test_traces_logic_good} logic-good, "
105
+ f"{pm.num_test_traces_matched} matched",
106
+ )
107
+ )
108
+ blocks.append(
109
+ kv(
110
+ "log traces:",
111
+ f"{pm.num_log_traces_total} total, "
112
+ f"{pm.num_log_traces_logic_good} logic-good, "
113
+ f"{pm.num_log_traces_matched} matched",
114
+ )
115
+ )
116
+
117
+ # NEW: src + docs (optional)
118
+ # Only show if ctx allows and the fields exist on the PM (they should, after your model update).
119
+ if getattr(ctx, "show_artifacts", True):
120
+ if (pm.num_src_code_arts_total is not None) or (pm.num_src_code_arts_matched is not None):
121
+ blocks.append(
122
+ kv(
123
+ "src code:",
124
+ f"{pm.num_src_code_arts_total} total, "
125
+ f"{pm.num_src_code_arts_matched} matched",
126
+ )
127
+ )
128
+ if (pm.num_doc_arts_total is not None) or (pm.num_doc_arts_matched is not None):
129
+ blocks.append(
130
+ kv(
131
+ "docs:",
132
+ f"{pm.num_doc_arts_total} total, "
133
+ f"{pm.num_doc_arts_matched} matched",
134
+ )
135
+ )
136
+
137
+ body = Group(*blocks)
138
+
139
+ return Panel(
140
+ body,
141
+ title="[bold]Summary[/bold]",
142
+ border_style="magenta",
143
+ )
@@ -0,0 +1,122 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/presentation/renderers/trace.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ from rich import box
10
+ from rich.panel import Panel
11
+ from rich.console import Group
12
+ from rich.rule import Rule
13
+ from rich.table import Table
14
+ from rich.text import Text
15
+ from rich.syntax import Syntax
16
+
17
+ from speclogician.presentation.ctx import RenderCtx
18
+ from speclogician.presentation.models.trace import TestTracePM, LogTracePM
19
+ from speclogician.presentation.renderers import iml_validity_icon_style_label
20
+
21
+
22
+ def _validity_text(iml_valid: str | None) -> Text:
23
+ icon, style, label = iml_validity_icon_style_label(iml_valid)
24
+ return Text(f"{icon} {label}", style=f"bold {style}")
25
+
26
+
27
+ def _kv_table(ctx: RenderCtx) -> Table:
28
+ b = box.MINIMAL if getattr(ctx, "compact", False) else box.SIMPLE
29
+ t = Table(show_header=False, box=b, show_edge=False, pad_edge=False, expand=True)
30
+ t.add_column(style="italic dim", no_wrap=True)
31
+ t.add_column()
32
+ return t
33
+
34
+
35
+ def _code_block(title: str, code: str, *, lang: str) -> Group:
36
+ # No panel: just a rule + syntax
37
+ return Group(
38
+ Rule(f"[bold]{title}[/bold]"),
39
+ Syntax(code or "", lang, word_wrap=True),
40
+ )
41
+
42
+
43
+ def render_test_trace(pm: TestTracePM, *, ctx: RenderCtx):
44
+ core = pm.core
45
+
46
+ t = _kv_table(ctx)
47
+
48
+ if getattr(ctx, "show_ids", False):
49
+ t.add_row("art_id", Text(core.art_id, style="dim"))
50
+
51
+ t.add_row("name", Text(pm.name or "-", style="bold"))
52
+ if pm.filepath:
53
+ t.add_row("filepath", Text(pm.filepath))
54
+ if pm.language:
55
+ t.add_row("language", Text(pm.language))
56
+
57
+ t.add_row("iml_valid", _validity_text(core.iml_validity.iml_valid))
58
+ if core.iml_validity.err:
59
+ t.add_row("iml_error", Text(core.iml_validity.err, style="red"))
60
+
61
+ if core.time:
62
+ t.add_row("time", Text(core.time, style="dim"))
63
+
64
+ blocks: list[object] = [t]
65
+
66
+ if getattr(ctx, "show_code", True):
67
+ blocks.append(_code_block("given : state", core.given, lang="ocaml"))
68
+ if core.when:
69
+ blocks.append(_code_block("when : action", core.when, lang="ocaml"))
70
+ if core.then:
71
+ blocks.append(_code_block("then : state", core.then, lang="ocaml"))
72
+
73
+ if pm.contents:
74
+ blocks.append(
75
+ _code_block(
76
+ "original test contents",
77
+ pm.contents,
78
+ lang=(pm.language or "text"),
79
+ )
80
+ )
81
+
82
+ return Panel(
83
+ Group(*blocks),
84
+ title="[bold]Test trace[/bold]",
85
+ border_style="cyan",
86
+ )
87
+
88
+
89
+ def render_log_trace(pm: LogTracePM, *, ctx: RenderCtx):
90
+ core = pm.core
91
+
92
+ t = _kv_table(ctx)
93
+
94
+ if getattr(ctx, "show_ids", False):
95
+ t.add_row("art_id", Text(core.art_id, style="dim"))
96
+
97
+ t.add_row("filename", Text(pm.filename or "-", style="bold"))
98
+
99
+ t.add_row("iml_valid", _validity_text(core.iml_validity.iml_valid))
100
+ if core.iml_validity.err:
101
+ t.add_row("iml_error", Text(core.iml_validity.err, style="red"))
102
+
103
+ if core.time:
104
+ t.add_row("time", Text(core.time, style="dim"))
105
+
106
+ blocks: list[object] = [t]
107
+
108
+ if getattr(ctx, "show_code", True):
109
+ blocks.append(_code_block("given : state", core.given, lang="ocaml"))
110
+ if core.when:
111
+ blocks.append(_code_block("when : action", core.when, lang="ocaml"))
112
+ if core.then:
113
+ blocks.append(_code_block("then : state", core.then, lang="ocaml"))
114
+
115
+ if pm.contents:
116
+ blocks.append(_code_block("log contents", pm.contents, lang="text"))
117
+
118
+ return Panel(
119
+ Group(*blocks),
120
+ title="[bold]Log trace[/bold]",
121
+ border_style="cyan",
122
+ )
speclogician/py.typed ADDED
File without changes
@@ -0,0 +1,170 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/shell/app.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import argparse
11
+ from pathlib import Path
12
+ from cmd2 import Cmd, with_argparser
13
+ from rich.text import Text
14
+ from rich.align import Align
15
+ from rich.console import RenderableType
16
+ from rich.rule import Rule
17
+
18
+ from speclogician.utils import console
19
+ from speclogician.state.state import State
20
+ from speclogician.shell.shell_ch import ChangeShell
21
+
22
+
23
+ SPEC_LOGICIAN_BANNER = r"""
24
+ .___ .___ _________ .____ .__ .__
25
+ | | _____ _____ ____ __| _/___________ / _____/_____ ____ ____ | | ____ ____ |__| ____ |__|____ ____
26
+ | |/ \\__ \ / \ / __ |\_ __ \__ \ \_____ \\____ \_/ __ \_/ ___\| | / _ \ / ___\| |/ ___\| \__ \ / \
27
+ | | Y Y \/ __ \| | \/ /_/ | | | \// __ \_ / \ |_> > ___/\ \___| |__( <_> ) /_/ > \ \___| |/ __ \| | \
28
+ |___|__|_| (____ /___| /\____ | |__| (____ / /_______ / __/ \___ >\___ >_______ \____/\___ /|__|\___ >__(____ /___| /
29
+ \/ \/ \/ \/ \/ \/|__| \/ \/ \/ /_____/ \/ \/ \/
30
+ """
31
+
32
+ def _print_result(res: object) -> None:
33
+ """Best-effort printing for ProcessChange* results."""
34
+ # If your ProcessChange JSON-safe models have .model_dump(), use that.
35
+ if hasattr(res, "model_dump"):
36
+ console.print_json(json.dumps(res.model_dump(), indent=2))
37
+ return
38
+
39
+ # Otherwise fall back to repr / dict
40
+ if isinstance(res, dict):
41
+ console.print_json(json.dumps(res, indent=2, default=str))
42
+ return
43
+
44
+ console.print(res)
45
+
46
+
47
+ class SpecLogicianShell(Cmd):
48
+ intro = "SpecLogician shell. Type 'help' or '?' to list commands.\n"
49
+ prompt = "sl> "
50
+ allow_cli_args = False
51
+
52
+ def __init__(self, *args, **kwargs):
53
+ # Important: prevent cmd2 from parsing outer CLI args
54
+ self.allow_cli_args = False
55
+
56
+ super().__init__(*args, **kwargs)
57
+
58
+ # Rich console that writes to the SAME stream cmd2 uses
59
+ # (cmd2 writes to self.stdout)
60
+ self.rich = console
61
+
62
+ self.state = None
63
+ self.state_path = None
64
+
65
+
66
+ def preloop(self) -> None:
67
+ console.print(
68
+ Align.left(
69
+ Text(SPEC_LOGICIAN_BANNER, style="bold magenta")
70
+ )
71
+ )
72
+ console.print(
73
+ Align.left(
74
+ Text("SpecLogician Interactive Shell - www.speclogician.dev", style="italic dim")
75
+ )
76
+ )
77
+ console.print()
78
+
79
+ # ---- helpers ----
80
+ def rprint(self, *renderables: RenderableType) -> None:
81
+ """Print rich renderables to cmd2 stdout."""
82
+ for r in renderables:
83
+ self.rich.print(r)
84
+
85
+ def info(self, msg: str) -> None:
86
+ self.rich.print(f"[info]{msg}[/info]")
87
+
88
+ def ok(self, msg: str) -> None:
89
+ self.rich.print(f"[ok]✓ {msg}[/ok]")
90
+
91
+ def warn(self, msg: str) -> None:
92
+ self.rich.print(f"[warn]⚠ {msg}[/warn]")
93
+
94
+ def err(self, msg: str) -> None:
95
+ self.rich.print(f"[err]✗ {msg}[/err]")
96
+
97
+ # -------------------------
98
+ # Session management
99
+ # -------------------------
100
+
101
+ # ✅ programmatic API: pass a real State object
102
+ def open_state(self, st: State, *, path: str | Path | None = None) -> None:
103
+ self.state = st
104
+ self.state_path = Path(path) if path is not None else None
105
+ if self.state_path:
106
+ self.poutput(f"Loaded state object (path={self.state_path})")
107
+ else:
108
+ self.poutput("Loaded state object (in-memory)")
109
+
110
+ open_parser = argparse.ArgumentParser()
111
+ open_parser.add_argument("path", help="Path to state.json")
112
+ @with_argparser(open_parser)
113
+ def do_open(self, args) -> None:
114
+ p = Path(args.path)
115
+ st = State.from_json(p) # your actual loader
116
+ self.open_state(st, path=p)
117
+
118
+
119
+ def do_status(self, _arg) -> None:
120
+ """Show current state/spec summary."""
121
+ if self.state is None:
122
+ self.warn("No state loaded.")
123
+ return
124
+
125
+ cur = self.state.curr_state()
126
+
127
+ # Header that won't render as a black rectangle
128
+ self.rprint(Rule("[bold]Status[/bold]"))
129
+
130
+ # Your DomainModel.__rich__ already returns a Table
131
+ self.rprint(cur)
132
+
133
+ def do_save(self, _args) -> None:
134
+ if not self.state or not self.state_path:
135
+ self.perror("No state loaded.")
136
+ return
137
+ self.state.save_to_path(self.state_path) # adapt to your actual saver
138
+ self.poutput(f"Saved {self.state_path}")
139
+
140
+
141
+ # -------------------------
142
+ # Output mode
143
+ # -------------------------
144
+
145
+ def do_json(self, arg) -> None:
146
+ """
147
+ Toggle JSON output mode (affects process_change json flag).
148
+ Usage:
149
+ json on
150
+ json off
151
+ """
152
+ a = (arg or "").strip().lower()
153
+ if a in ("on", "true", "1"):
154
+ self.json_default = True
155
+ elif a in ("off", "false", "0"):
156
+ self.json_default = False
157
+ else:
158
+ self.poutput(f"json_default={self.json_default}")
159
+ return
160
+ self.poutput(f"json_default={self.json_default}")
161
+
162
+ def do_ch(self, _arg) -> None:
163
+ """Enter the change sub-shell (pred/trans/scenario/base/...)."""
164
+ ChangeShell(self).cmdloop()
165
+
166
+ def main():
167
+ SpecLogicianShell().cmdloop()
168
+
169
+ if __name__ == "__main__":
170
+ main()
@@ -0,0 +1,263 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/shell/ch_shell.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ from cmd2 import Cmd, with_argparser
15
+
16
+ from speclogician.modeling.predicates import PredicateType
17
+ from speclogician.modeling.scenario import ScenarioDelta
18
+ from speclogician.state.change import (
19
+ DomainModelBaseEdit,
20
+ PredicateAdd,
21
+ PredicateEdit,
22
+ PredicateRemove,
23
+ TransitionAdd,
24
+ TransitionEdit,
25
+ TransitionRemove,
26
+ ScenarioAdd,
27
+ ScenarioEdit,
28
+ ScenarioRemove,
29
+ # later: AddTestTrace, EditTestTrace, AddLogTrace, ...
30
+ )
31
+ from speclogician.utils.load import load_state
32
+
33
+
34
+ def _print_result(cmd: Cmd, res: object) -> None:
35
+ """
36
+ Best-effort printer for ProcessChangeResult / ProcessChangeObjectResult.
37
+ - dict: dump as JSON
38
+ - pydantic-ish: model_dump
39
+ - fallback: str()
40
+ """
41
+ if isinstance(res, dict):
42
+ cmd.poutput(json.dumps(res, indent=2, default=str))
43
+ return
44
+ if hasattr(res, "model_dump"):
45
+ cmd.poutput(json.dumps(res.model_dump(mode="json"), indent=2, default=str)) # type: ignore[attr-defined]
46
+ return
47
+ cmd.poutput(str(res))
48
+
49
+
50
+ def _split_csv(s: Optional[str]) -> list[str]:
51
+ if not s:
52
+ return []
53
+ return [x.strip() for x in s.split(",") if x.strip()]
54
+
55
+
56
+ def _merge_dedup_preserve_order(*lists: list[str]) -> list[str]:
57
+ out: list[str] = []
58
+ seen: set[str] = set()
59
+ for xs in lists:
60
+ for x in xs:
61
+ if x not in seen:
62
+ out.append(x)
63
+ seen.add(x)
64
+ return out
65
+
66
+
67
+ def _delta_from_add(xs: list[str]) -> ScenarioDelta | None:
68
+ d = ScenarioDelta(add=xs)
69
+ return None if d.is_empty() else d
70
+
71
+
72
+ class ChangeShell(Cmd):
73
+ """
74
+ Nested cmd2 shell that exposes the same 'ch' operations as the CLI.
75
+ """
76
+
77
+ prompt = "sl ch> "
78
+ intro = "Change commands (type 'help' to list, 'exit' to return)\n"
79
+ allow_cli_args = False
80
+
81
+ def __init__(self, parent: Cmd):
82
+ super().__init__(allow_cli_args=False)
83
+ self.parent = parent
84
+
85
+ # -------------------------
86
+ # shared accessors
87
+ # -------------------------
88
+
89
+ def _get_state(self):
90
+ """
91
+ Use parent.state if present, otherwise load state from default location.
92
+ """
93
+ st = getattr(self.parent, "state", None)
94
+ if st is None:
95
+ st = load_state()
96
+ setattr(self.parent, "state", st)
97
+ return st
98
+
99
+ def _json(self) -> bool:
100
+ return bool(getattr(self.parent, "json_default", False))
101
+
102
+ # -------------------------
103
+ # base-edit (matches Typer: ch base-edit --src/--file)
104
+ # -------------------------
105
+
106
+ base_edit_parser = argparse.ArgumentParser()
107
+ g = base_edit_parser.add_mutually_exclusive_group(required=True)
108
+ g.add_argument("--src", help="IML source code for the domain base component")
109
+ g.add_argument("--file", type=Path, help="Path to a file containing IML base source code")
110
+ @with_argparser(base_edit_parser)
111
+ def do_base_edit(self, args) -> None:
112
+ st = self._get_state()
113
+
114
+ src: str
115
+ if args.src is not None:
116
+ src = args.src
117
+ else:
118
+ p: Path = args.file
119
+ src = p.read_text()
120
+
121
+ change = DomainModelBaseEdit(new_base_src=src)
122
+ res = st.process_change(change, json_only=self._json())
123
+ _print_result(self, res)
124
+
125
+ # -------------------------
126
+ # predicates
127
+ # -------------------------
128
+
129
+ pred_add_parser = argparse.ArgumentParser()
130
+ pred_add_parser.add_argument("--pred-type", choices=["state", "action"], required=True)
131
+ pred_add_parser.add_argument("--pred-name", required=True)
132
+ pred_add_parser.add_argument("--pred-src", required=True)
133
+ @with_argparser(pred_add_parser)
134
+ def do_pred_add(self, args) -> None:
135
+ st = self._get_state()
136
+ ptype = PredicateType.STATE if args.pred_type == "state" else PredicateType.ACTION
137
+ change = PredicateAdd(pred_type=ptype, pred_name=args.pred_name, pred_src=args.pred_src)
138
+ res = st.process_change(change, json_only=self._json())
139
+ _print_result(self, res)
140
+
141
+ pred_edit_parser = argparse.ArgumentParser()
142
+ pred_edit_parser.add_argument("--pred-name", required=True)
143
+ pred_edit_parser.add_argument("--pred-src", required=True)
144
+ @with_argparser(pred_edit_parser)
145
+ def do_pred_edit(self, args) -> None:
146
+ st = self._get_state()
147
+ change = PredicateEdit(pred_name=args.pred_name, pred_src=args.pred_src)
148
+ res = st.process_change(change, json_only=self._json())
149
+ _print_result(self, res)
150
+
151
+ pred_remove_parser = argparse.ArgumentParser()
152
+ pred_remove_parser.add_argument("--pred-name", required=True)
153
+ @with_argparser(pred_remove_parser)
154
+ def do_pred_remove(self, args) -> None:
155
+ st = self._get_state()
156
+ change = PredicateRemove(pred_name=args.pred_name)
157
+ res = st.process_change(change, json_only=self._json())
158
+ _print_result(self, res)
159
+
160
+ # -------------------------
161
+ # transitions
162
+ # -------------------------
163
+
164
+ trans_add_parser = argparse.ArgumentParser()
165
+ trans_add_parser.add_argument("--trans-name", required=True)
166
+ trans_add_parser.add_argument("--trans-src", required=True)
167
+ @with_argparser(trans_add_parser)
168
+ def do_trans_add(self, args) -> None:
169
+ st = self._get_state()
170
+ change = TransitionAdd(trans_name=args.trans_name, trans_src=args.trans_src)
171
+ res = st.process_change(change, json_only=self._json())
172
+ _print_result(self, res)
173
+
174
+ trans_edit_parser = argparse.ArgumentParser()
175
+ trans_edit_parser.add_argument("--trans-name", required=True)
176
+ trans_edit_parser.add_argument("--trans-src", required=True)
177
+ @with_argparser(trans_edit_parser)
178
+ def do_trans_edit(self, args) -> None:
179
+ st = self._get_state()
180
+ change = TransitionEdit(trans_name=args.trans_name, trans_src=args.trans_src)
181
+ res = st.process_change(change, json_only=self._json())
182
+ _print_result(self, res)
183
+
184
+ trans_remove_parser = argparse.ArgumentParser()
185
+ trans_remove_parser.add_argument("--trans-name", required=True)
186
+ @with_argparser(trans_remove_parser)
187
+ def do_trans_remove(self, args) -> None:
188
+ st = self._get_state()
189
+ change = TransitionRemove(trans_name=args.trans_name)
190
+ res = st.process_change(change, json_only=self._json())
191
+ _print_result(self, res)
192
+
193
+ # -------------------------
194
+ # scenarios
195
+ # -------------------------
196
+
197
+ scenario_add_parser = argparse.ArgumentParser()
198
+ scenario_add_parser.add_argument("scenario_name")
199
+ scenario_add_parser.add_argument("--given", action="append", default=[], help="Repeatable")
200
+ scenario_add_parser.add_argument("--when", action="append", default=[], help="Repeatable")
201
+ scenario_add_parser.add_argument("--then", action="append", default=[], help="Repeatable")
202
+ scenario_add_parser.add_argument("--given-list", default=None)
203
+ scenario_add_parser.add_argument("--when-list", default=None)
204
+ scenario_add_parser.add_argument("--then-list", default=None)
205
+ @with_argparser(scenario_add_parser)
206
+ def do_scenario_add(self, args) -> None:
207
+ st = self._get_state()
208
+
209
+ given_all = _merge_dedup_preserve_order(args.given, _split_csv(args.given_list))
210
+ when_all = _merge_dedup_preserve_order(args.when, _split_csv(args.when_list))
211
+ then_all = _merge_dedup_preserve_order(args.then, _split_csv(args.then_list))
212
+
213
+ change = ScenarioAdd(
214
+ scenario_name=args.scenario_name,
215
+ given=_delta_from_add(given_all),
216
+ when=_delta_from_add(when_all),
217
+ then=_delta_from_add(then_all),
218
+ )
219
+ res = st.process_change(change, json_only=self._json())
220
+ _print_result(self, res)
221
+
222
+ scenario_edit_parser = argparse.ArgumentParser()
223
+ scenario_edit_parser.add_argument("scenario_name")
224
+ scenario_edit_parser.add_argument("--given", default=None)
225
+ scenario_edit_parser.add_argument("--when", default=None)
226
+ scenario_edit_parser.add_argument("--then", default=None)
227
+ @with_argparser(scenario_edit_parser)
228
+ def do_scenario_edit(self, args) -> None:
229
+ st = self._get_state()
230
+
231
+ # NOTE: to match CLI behavior, reuse the SAME parser you use in Typer.
232
+ # If you already have parse_scenario_delta(), import it and use it here.
233
+ from speclogician.commands.utils import parse_scenario_delta # adjust import to your actual module
234
+
235
+ change = ScenarioEdit(
236
+ scenario_name=args.scenario_name,
237
+ given=parse_scenario_delta(args.given),
238
+ when=parse_scenario_delta(args.when),
239
+ then=parse_scenario_delta(args.then),
240
+ )
241
+ res = st.process_change(change, json_only=self._json())
242
+ _print_result(self, res)
243
+
244
+ scenario_remove_parser = argparse.ArgumentParser()
245
+ scenario_remove_parser.add_argument("scenario_name")
246
+ @with_argparser(scenario_remove_parser)
247
+ def do_scenario_remove(self, args) -> None:
248
+ st = self._get_state()
249
+ change = ScenarioRemove(scenario_name=args.scenario_name)
250
+ res = st.process_change(change, json_only=self._json())
251
+ _print_result(self, res)
252
+
253
+ # -------------------------
254
+ # navigation
255
+ # -------------------------
256
+
257
+ def do_exit(self, _arg) -> bool:
258
+ """Return to the main shell."""
259
+ return True
260
+
261
+ def do_EOF(self, _arg) -> bool:
262
+ """Ctrl-D exits this sub-shell."""
263
+ return True