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,206 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/presentation/preview/fixtures/trace.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+ from speclogician.presentation.ctx import RenderCtx
9
+ from speclogician.presentation.models.trace import (
10
+ TraceIMLValidityPM,
11
+ TraceCorePM,
12
+ TestTracePM,
13
+ LogTracePM,
14
+ )
15
+ from speclogician.presentation.renderers.trace import (
16
+ render_test_trace,
17
+ render_log_trace,
18
+ )
19
+
20
+
21
+ # -----------------------------------------------------------------------------
22
+ # Shared helpers
23
+ # -----------------------------------------------------------------------------
24
+
25
+ def minimal_iml_validity() -> TraceIMLValidityPM:
26
+ return TraceIMLValidityPM(iml_valid="unknown", err=None)
27
+
28
+ def typical_iml_validity_valid() -> TraceIMLValidityPM:
29
+ return TraceIMLValidityPM(iml_valid="valid", err=None)
30
+
31
+ def typical_iml_validity_invalid() -> TraceIMLValidityPM:
32
+ return TraceIMLValidityPM(
33
+ iml_valid="invalid",
34
+ err="Parse error: expected ')' at line 1, col 12",
35
+ )
36
+
37
+ def edge_iml_validity_invalid_long() -> TraceIMLValidityPM:
38
+ return TraceIMLValidityPM(
39
+ iml_valid="invalid",
40
+ err=(
41
+ "Type error: cannot unify int and bool in expression "
42
+ "`if s.x > 0 then 1 else false` (line 42, col 3). "
43
+ "Hint: check predicate body and variable bindings."
44
+ ),
45
+ )
46
+
47
+ def minimal_trace_core() -> TraceCorePM:
48
+ return TraceCorePM(
49
+ art_id="00000000-0000-0000-0000-000000000000",
50
+ given="",
51
+ when=None,
52
+ then=None,
53
+ time="",
54
+ iml_validity=minimal_iml_validity(),
55
+ )
56
+
57
+ def typical_trace_core_ok(*, art_id: str = "11111111-1111-1111-1111-111111111111") -> TraceCorePM:
58
+ return TraceCorePM(
59
+ art_id=art_id,
60
+ given="{ x = 10 }",
61
+ when="{ a = 2 }",
62
+ then="{ x = 12 }",
63
+ time="2026-01-09T10:12:00Z",
64
+ iml_validity=typical_iml_validity_valid(),
65
+ )
66
+
67
+ def typical_trace_core_invalid(*, art_id: str = "22222222-2222-2222-2222-222222222222") -> TraceCorePM:
68
+ return TraceCorePM(
69
+ art_id=art_id,
70
+ given="{ x = 10 }",
71
+ when="{ a = 2 }",
72
+ then="{ x = 12 }",
73
+ time="2026-01-09T10:13:00Z",
74
+ iml_validity=typical_iml_validity_invalid(),
75
+ )
76
+
77
+ def edge_trace_core_long(*, art_id: str = "99999999-9999-9999-9999-999999999999") -> TraceCorePM:
78
+ # Deliberately long / multi-line / weird to stress renderers
79
+ return TraceCorePM(
80
+ art_id=art_id,
81
+ given=(
82
+ "{ x = 10; notes = \"a very long string value that will wrap in tables\"; "
83
+ "nested = { y = 1234567890; z = -42 } }"
84
+ ),
85
+ when="{ a = 2; meta = \"multiline\\nwith newlines\\nand spaces\\n\" }",
86
+ then="{ x = 12; result = Some \"OK\"; debug = \"a\" ^ \"b\" ^ \"c\" }",
87
+ time="2026-01-09T10:12:00.123456Z",
88
+ iml_validity=edge_iml_validity_invalid_long(),
89
+ )
90
+
91
+ # -----------------------------------------------------------------------------
92
+ # TestTracePM fixtures
93
+ # -----------------------------------------------------------------------------
94
+
95
+ def minimal_test_trace_pm() -> TestTracePM:
96
+ return TestTracePM(
97
+ core=minimal_trace_core(),
98
+ name="",
99
+ filepath="",
100
+ language=None,
101
+ contents=None,
102
+ )
103
+
104
+ def typical_test_trace_pm(*, with_contents: bool = False, invalid: bool = False) -> TestTracePM:
105
+ core = typical_trace_core_invalid() if invalid else typical_trace_core_ok()
106
+ return TestTracePM(
107
+ core=core,
108
+ name="Two parent orders, one rejected",
109
+ filepath="tests/risk/OrderValidationSpec.groovy",
110
+ language="groovy",
111
+ contents=(
112
+ "given: '...'\nwhen: '...'\nthen: '...'\n"
113
+ if with_contents
114
+ else None
115
+ ),
116
+ )
117
+
118
+ def edge_test_trace_pm() -> TestTracePM:
119
+ return TestTracePM(
120
+ core=edge_trace_core_long(),
121
+ name="test_" + ("really_long_name_" * 6) + "end",
122
+ filepath=("very/long/path/" * 8) + "OrderValidationSpec.groovy",
123
+ language="groovy",
124
+ contents=(
125
+ "given: \"A very long test body that includes code\"\n"
126
+ + ("// line\n" * 60)
127
+ + "then: \"assert something\"\n"
128
+ ),
129
+ )
130
+
131
+ # -----------------------------------------------------------------------------
132
+ # LogTracePM fixtures
133
+ # -----------------------------------------------------------------------------
134
+
135
+ def minimal_log_trace_pm() -> LogTracePM:
136
+ return LogTracePM(
137
+ core=minimal_trace_core(),
138
+ filename="",
139
+ contents=None,
140
+ )
141
+
142
+ def typical_log_trace_pm(*, with_contents: bool = False, invalid: bool = False) -> LogTracePM:
143
+ core = typical_trace_core_invalid() if invalid else typical_trace_core_ok(
144
+ art_id="33333333-3333-3333-3333-333333333333"
145
+ )
146
+ return LogTracePM(
147
+ core=core,
148
+ filename="prod_2026-01-09_FIX.log",
149
+ contents=("8=FIX.4.4|9=...|35=D|..." if with_contents else None),
150
+ )
151
+
152
+ def edge_log_trace_pm() -> LogTracePM:
153
+ return LogTracePM(
154
+ core=edge_trace_core_long(art_id="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
155
+ filename="prod_" + ("2026-01-09_" * 5) + "huge_FIX.log",
156
+ contents=("8=FIX.4.4|9=...|35=D|...\n" * 200),
157
+ )
158
+
159
+ # -----------------------------------------------------------------------------
160
+ # Fixture entrypoints for PreviewSpec(make=...)
161
+ # -----------------------------------------------------------------------------
162
+
163
+ def make_test_trace_pm(ctx: RenderCtx, variant: str) -> TestTracePM:
164
+ """
165
+ Entry point used by PreviewSpec(make=...).
166
+
167
+ Variants:
168
+ - typical: valid + includes original contents
169
+ - minimal: smallest possible (no code blocks / metadata)
170
+ - edge: long values + invalid + lots of contents
171
+ """
172
+ v = (variant or "").strip().lower()
173
+ if v == "typical":
174
+ # typical should exercise code rendering; include original contents
175
+ return typical_test_trace_pm(with_contents=True, invalid=False)
176
+ if v == "minimal":
177
+ return minimal_test_trace_pm()
178
+ if v == "edge":
179
+ return edge_test_trace_pm()
180
+ raise ValueError(f"Unknown variant: {variant!r}")
181
+
182
+
183
+ def make_log_trace_pm(ctx: RenderCtx, variant: str) -> LogTracePM:
184
+ """
185
+ Entry point used by PreviewSpec(make=...).
186
+
187
+ Variants:
188
+ - typical: valid + includes log contents
189
+ - minimal: smallest possible
190
+ - edge: long values + invalid + lots of contents
191
+ """
192
+ v = (variant or "").strip().lower()
193
+ if v == "typical":
194
+ return typical_log_trace_pm(with_contents=True, invalid=False)
195
+ if v == "minimal":
196
+ return minimal_log_trace_pm()
197
+ if v == "edge":
198
+ return edge_log_trace_pm()
199
+ raise ValueError(f"Unknown variant: {variant!r}")
200
+
201
+ def render_test(pm: TestTracePM, ctx: RenderCtx):
202
+ return render_test_trace(pm, ctx=ctx)
203
+
204
+
205
+ def render_log(pm: LogTracePM, ctx: RenderCtx):
206
+ return render_log_trace(pm, ctx=ctx)
@@ -0,0 +1,42 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/presentation/preview/registry.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import Callable, Any
11
+
12
+ from speclogician.presentation.ctx import RenderCtx
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class PreviewSpec:
17
+ """
18
+ A preview entry in the registry.
19
+ - make(ctx, variant) returns a *PM object* (pydantic model)
20
+ - render(pm, ctx) returns a Rich renderable
21
+ """
22
+ name: str
23
+ variants: tuple[str, ...]
24
+ make: Callable[[RenderCtx, str], Any]
25
+ render: Callable[[Any, RenderCtx], Any]
26
+
27
+
28
+ _REGISTRY: dict[str, PreviewSpec] = {}
29
+
30
+
31
+ def register(spec: PreviewSpec) -> None:
32
+ _REGISTRY[spec.name] = spec
33
+
34
+
35
+ def get(name: str) -> PreviewSpec:
36
+ if name not in _REGISTRY:
37
+ raise KeyError(f"Unknown preview: {name}. Available: {', '.join(sorted(_REGISTRY))}")
38
+ return _REGISTRY[name]
39
+
40
+
41
+ def list_previews() -> list[PreviewSpec]:
42
+ return [*_REGISTRY.values()]
@@ -0,0 +1,24 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/presentation/renderers/__init__.py (or similar)
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ def iml_validity_icon_style_label(iml_valid: str | None) -> tuple[str, str, str]:
10
+ """
11
+ Returns: (icon, rich_style, human_label)
12
+ """
13
+ v = (iml_valid or "").lower()
14
+
15
+ match v:
16
+ case "valid":
17
+ return ("✓", "green", "valid")
18
+ case "invalid":
19
+ return ("✗", "red", "invalid")
20
+ case "unknown" | "":
21
+ return ("…", "yellow", "unknown")
22
+ case _:
23
+ # forward-compatible / defensive
24
+ return ("?", "dim", v)
@@ -0,0 +1,136 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/presentation/renderers/container.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ from rich import box
10
+ from rich.console import Group
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+ from rich.text import Text
14
+ from rich.rule import Rule
15
+
16
+ from speclogician.presentation.ctx import RenderCtx
17
+ from speclogician.presentation.models.container import ArtifactContainerPM
18
+ from speclogician.presentation.renderers.trace import render_test_trace, render_log_trace
19
+ from speclogician.presentation.renderers.data_artifact import render_doc_ref, render_src_code_ref
20
+
21
+
22
+ def _group_with_separators(renderables: list[object], *, style: str = "dim") -> Group:
23
+ """
24
+ Render list items with a horizontal separator between them.
25
+ """
26
+ parts: list[object] = []
27
+ for i, r in enumerate(renderables):
28
+ parts.append(r)
29
+ if i < len(renderables) - 1:
30
+ parts.append(Rule(style=style))
31
+ return Group(*parts)
32
+
33
+
34
+ def render_artifact_container(pm: ArtifactContainerPM, *, ctx: RenderCtx):
35
+ b = box.MINIMAL if ctx.compact else box.SIMPLE
36
+
37
+ # --- Counts table ---
38
+ t = Table(
39
+ show_header=False,
40
+ box=b,
41
+ show_edge=False,
42
+ pad_edge=False,
43
+ expand=False,
44
+ )
45
+
46
+ t.add_column(no_wrap=True)
47
+ t.add_column(
48
+ justify="right",
49
+ width=6,
50
+ )
51
+
52
+ def section(title: str) -> None:
53
+ t.add_row(
54
+ Text(title, style="bold magenta"),
55
+ Text(""),
56
+ )
57
+
58
+ def metric(label: str, value: int, *, icon: str = "•") -> None:
59
+ t.add_row(
60
+ Text(f" {icon} {label}", style="dim"),
61
+ Text(str(value)),
62
+ )
63
+
64
+ c = pm.counts
65
+
66
+ section("Test traces")
67
+ metric("Total", c.num_test_traces_total)
68
+ metric("Matched", c.num_test_traces_matched, icon="✓")
69
+ metric("Valid IML", c.num_test_traces_logic_good, icon="✓")
70
+
71
+ section("Log traces")
72
+ metric("Total", c.num_log_traces_total)
73
+ metric("Matched", c.num_log_traces_matched, icon="✓")
74
+ metric("Valid IML", c.num_log_traces_logic_good, icon="✓")
75
+
76
+ section("Source code refs")
77
+ metric("Total", c.num_src_code_arts_total)
78
+ metric("Matched", c.num_src_code_arts_matched, icon="✓")
79
+
80
+ section("Doc refs")
81
+ metric("Total", c.num_doc_arts_total)
82
+ metric("Matched", c.num_doc_arts_matched, icon="✓")
83
+
84
+ if ctx.show_stats_only:
85
+ return Panel(
86
+ t,
87
+ title="[bold]Artifacts[/bold]",
88
+ border_style="magenta",
89
+ )
90
+
91
+ blocks: list[object] = [Panel(t, title="[bold]Counts[/bold]", border_style="magenta")]
92
+
93
+ # --- Split by type ---
94
+ items = pm.items
95
+
96
+ if items.test_traces:
97
+ rendered = [render_test_trace(x, ctx=ctx) for x in items.test_traces]
98
+ blocks.append(
99
+ Panel(
100
+ _group_with_separators(rendered),
101
+ title=f"[bold]Test traces[/bold] ({len(items.test_traces)})",
102
+ border_style="cyan",
103
+ )
104
+ )
105
+
106
+ if items.log_traces:
107
+ rendered = [render_log_trace(x, ctx=ctx) for x in items.log_traces]
108
+ blocks.append(
109
+ Panel(
110
+ _group_with_separators(rendered),
111
+ title=f"[bold]Log traces[/bold] ({len(items.log_traces)})",
112
+ border_style="cyan",
113
+ )
114
+ )
115
+
116
+ if items.doc_refs:
117
+ rendered = [render_doc_ref(x, ctx=ctx) for x in items.doc_refs]
118
+ blocks.append(
119
+ Panel(
120
+ _group_with_separators(rendered),
121
+ title=f"[bold]Doc refs[/bold] ({len(items.doc_refs)})",
122
+ border_style="green",
123
+ )
124
+ )
125
+
126
+ if items.src_code_refs:
127
+ rendered = [render_src_code_ref(x, ctx=ctx) for x in items.src_code_refs]
128
+ blocks.append(
129
+ Panel(
130
+ _group_with_separators(rendered),
131
+ title=f"[bold]Src code refs[/bold] ({len(items.src_code_refs)})",
132
+ border_style="green",
133
+ )
134
+ )
135
+
136
+ return Panel(Group(*blocks), title="[bold]Artifact container[/bold]", border_style="magenta")
@@ -0,0 +1,144 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/presentation/renderers/data_artifact.py
5
+ #
6
+
7
+ from rich.console import Group
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+ from rich.text import Text
11
+ from rich.syntax import Syntax
12
+ from rich.rule import Rule
13
+
14
+ from speclogician.presentation.ctx import RenderCtx
15
+ from speclogician.presentation.models.data_artifact import DocRefPM, SrcCodeRefPM
16
+
17
+
18
+ def _kv_table(ctx: RenderCtx) -> Table:
19
+ # tighter + predictable widths
20
+ t = Table(
21
+ show_header=False,
22
+ box=None, # no box for header table
23
+ show_edge=False,
24
+ pad_edge=False,
25
+ expand=True,
26
+ )
27
+ t.add_column(style="bold", no_wrap=True, width=10) # fixed key col
28
+ t.add_column(ratio=1) # value col fills remaining
29
+ return t
30
+
31
+
32
+ def _add_kv(t: Table, k: str, v: str | None, *, style: str | None = None) -> None:
33
+ if v is None:
34
+ return
35
+ v = v.strip()
36
+ if not v:
37
+ return
38
+ t.add_row(Text(k, style="bold"), Text(v, style=style or ""))
39
+
40
+
41
+ def render_doc_ref(pm: DocRefPM, *, ctx: RenderCtx):
42
+ blocks = []
43
+
44
+ # Header
45
+ t = _kv_table(ctx)
46
+ _add_kv(t, "kind", "doc_ref", style="magenta")
47
+ if getattr(ctx, "show_ids", False):
48
+ _add_kv(t, "art_id", pm.core.art_id, style="dim")
49
+ _add_kv(t, "meta", pm.meta)
50
+
51
+ if t.row_count > 0:
52
+ blocks.append(t)
53
+
54
+ # Text section
55
+ text = (pm.text or "").strip()
56
+
57
+ blocks.append(Rule("text"))
58
+
59
+ if getattr(ctx, "show_code", True):
60
+ if text:
61
+ blocks.append(Syntax(text, "markdown", word_wrap=True))
62
+ else:
63
+ blocks.append(Text("(empty)", style="dim"))
64
+ else:
65
+ if not text:
66
+ blocks.append(Text("(empty)", style="dim"))
67
+ else:
68
+ snippet = text if len(text) <= 200 else text[:200] + "…"
69
+ blocks.append(Text(snippet))
70
+
71
+ return Panel(Group(*blocks), title="[bold]Doc reference[/bold]", border_style="magenta")
72
+
73
+ # -----------------------------------------------------------------------------
74
+ # SrcCodeRef
75
+ # -----------------------------------------------------------------------------
76
+
77
+ def _maybe_code_block(
78
+ *,
79
+ title: str,
80
+ content: str | None,
81
+ lang: str,
82
+ show_code: bool,
83
+ ) -> list[object]:
84
+ """
85
+ Returns renderables for a section that always shows something:
86
+ - if show_code: Syntax when present, else '(empty)'
87
+ - if not show_code: snippet when present, else '(empty)'
88
+ """
89
+ blocks: list[object] = [Rule(title)]
90
+ text = (content or "").strip()
91
+
92
+ if not text:
93
+ blocks.append(Text("(empty)", style="dim"))
94
+ return blocks
95
+
96
+ if show_code:
97
+ blocks.append(Syntax(text, lang, word_wrap=True))
98
+ else:
99
+ snippet = text if len(text) <= 200 else text[:200] + "…"
100
+ blocks.append(Text(snippet))
101
+ return blocks
102
+
103
+
104
+ def render_src_code_ref(pm: SrcCodeRefPM, *, ctx: RenderCtx):
105
+ blocks: list[object] = []
106
+
107
+ # --- header ---
108
+ t = _kv_table(ctx)
109
+ _add_kv(t, "kind", "src_code_ref", style="magenta")
110
+
111
+ if getattr(ctx, "show_ids", False):
112
+ _add_kv(t, "art_id", pm.core.art_id, style="dim")
113
+
114
+ _add_kv(t, "language", pm.language)
115
+ _add_kv(t, "file_path", pm.file_path)
116
+ _add_kv(t, "meta", pm.meta)
117
+
118
+ if t.row_count > 0:
119
+ blocks.append(t)
120
+
121
+ show_code = getattr(ctx, "show_code", True)
122
+
123
+ # --- src_code section (always visible; shows (empty) if missing) ---
124
+ lang = (pm.language or "text").lower()
125
+ blocks.extend(
126
+ _maybe_code_block(
127
+ title="src_code",
128
+ content=pm.src_code,
129
+ lang=lang,
130
+ show_code=show_code,
131
+ )
132
+ )
133
+
134
+ # --- iml_code section (always visible; shows (empty) if missing) ---
135
+ blocks.extend(
136
+ _maybe_code_block(
137
+ title="iml_code",
138
+ content=pm.iml_code,
139
+ lang="ocaml",
140
+ show_code=show_code,
141
+ )
142
+ )
143
+
144
+ return Panel(Group(*blocks), title="[bold]Source code reference[/bold]", border_style="magenta")
@@ -0,0 +1,123 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/presentation/renderers/domain.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ from rich import box
10
+ from rich.console import Group
11
+ from rich.panel import Panel
12
+ from rich.syntax import Syntax
13
+ from rich.table import Table
14
+ from rich.text import Text
15
+
16
+ from speclogician.presentation.ctx import RenderCtx
17
+ from speclogician.presentation.models.domain import DomainModelPM
18
+
19
+
20
+ def render_domain_model(pm: DomainModelPM, *, ctx: RenderCtx):
21
+ b = box.MINIMAL if ctx.compact else box.SIMPLE
22
+
23
+ c = pm.counts
24
+ code = pm.code
25
+
26
+ stats = Table(
27
+ show_header=False,
28
+ box=b,
29
+ show_edge=False,
30
+ pad_edge=False,
31
+ expand=False, # ← critical change
32
+ )
33
+ stats.add_column(style="dim", no_wrap=True)
34
+ stats.add_column(justify="right")
35
+
36
+ def hdr(label: str) -> None:
37
+ stats.add_row(Text(label, style="bold magenta"), Text(""))
38
+
39
+ def row(label: str, value: object, *, bold: bool = False) -> None:
40
+ stats.add_row(
41
+ Text(" " + label),
42
+ Text(str(value), style="bold" if bold else "")
43
+ )
44
+
45
+ # ----------------------------
46
+ # Base
47
+ # ----------------------------
48
+ hdr("Base")
49
+ row("Base status", c.base_status, bold=True)
50
+ row("Has state", c.base_has_state)
51
+ row("Has action", c.base_has_action)
52
+
53
+ # ----------------------------
54
+ # Predicates
55
+ # ----------------------------
56
+ hdr("Predicates")
57
+ row("State total", c.num_state_preds_total, bold=True)
58
+ row("State valid", c.num_state_preds_valid_logic)
59
+ row("State matched", c.num_state_preds_matched)
60
+
61
+ row("Action total", c.num_action_preds_total, bold=True)
62
+ row("Action valid", c.num_action_preds_valid_logic)
63
+ row("Action matched", c.num_action_preds_matched)
64
+
65
+ row("All total", c.num_preds_total, bold=True)
66
+ row("All valid", c.num_preds_valid_logic)
67
+ row("All matched", c.num_preds_matched)
68
+
69
+ # ----------------------------
70
+ # Transitions
71
+ # ----------------------------
72
+ hdr("Transitions")
73
+ row("Total", c.num_trans_total, bold=True)
74
+ row("Valid", c.num_trans_valid_logic)
75
+ row("Matched", c.num_trans_matched)
76
+
77
+ if ctx.show_stats_only or not ctx.show_code:
78
+ return Panel(stats, title="[bold]Domain model[/bold]", border_style="magenta")
79
+
80
+ blocks: list[object] = [
81
+ Panel(stats, title="[bold]Stats[/bold]", border_style="magenta"),
82
+ ]
83
+
84
+ # Base code
85
+ if code.base.strip():
86
+ blocks.append(
87
+ Panel(
88
+ Syntax(code.base, "ocaml", word_wrap=True, line_numbers=not ctx.compact),
89
+ title="Base",
90
+ border_style="cyan",
91
+ )
92
+ )
93
+
94
+ # Predicates code
95
+ if code.state_preds_iml.strip():
96
+ blocks.append(
97
+ Panel(
98
+ Syntax(code.state_preds_iml, "ocaml", word_wrap=True, line_numbers=not ctx.compact),
99
+ title="State predicates",
100
+ border_style="green",
101
+ )
102
+ )
103
+
104
+ if code.action_preds_iml.strip():
105
+ blocks.append(
106
+ Panel(
107
+ Syntax(code.action_preds_iml, "ocaml", word_wrap=True, line_numbers=not ctx.compact),
108
+ title="Action predicates",
109
+ border_style="green",
110
+ )
111
+ )
112
+
113
+ # Transitions code
114
+ if code.transitions_iml.strip():
115
+ blocks.append(
116
+ Panel(
117
+ Syntax(code.transitions_iml, "ocaml", word_wrap=True, line_numbers=not ctx.compact),
118
+ title="Transitions",
119
+ border_style="blue",
120
+ )
121
+ )
122
+
123
+ return Panel(Group(*blocks), title="[bold]Domain model[/bold]", border_style="magenta")