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,153 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/shell/shell_view.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from cmd2 import Cmd, with_argparser
11
+ import argparse
12
+
13
+ from speclogician.presentation.ctx import RenderCtx
14
+ from speclogician.presentation.api import (
15
+ present_state_instance,
16
+ present_spec,
17
+ present_domain_model,
18
+ present_artifact_container,
19
+ present_instances_list,
20
+ )
21
+ from speclogician.utils import console
22
+
23
+
24
+ def _mk_ctx(args) -> RenderCtx:
25
+ return RenderCtx(
26
+ compact=getattr(args, "compact", False),
27
+ show_stats_only=getattr(args, "stats_only", False),
28
+ show_code=not getattr(args, "no_code", False),
29
+ show_ids=getattr(args, "ids", False),
30
+ show_art_ids=getattr(args, "art_ids", False),
31
+ )
32
+
33
+
34
+ class ViewShell(Cmd):
35
+ prompt = "sl:view> "
36
+ allow_cli_args = False
37
+
38
+ def __init__(self, parent_shell, *args, **kwargs):
39
+ super().__init__(*args, **kwargs)
40
+ self.parent = parent_shell
41
+ self.rich = console
42
+
43
+ # ---- shared flags for all view commands ----
44
+ def _add_common(self, p: argparse.ArgumentParser) -> None:
45
+ p.add_argument("--idx", type=int, default=0)
46
+ p.add_argument("--json", action="store_true")
47
+ p.add_argument("--compact", action="store_true")
48
+ p.add_argument("--stats-only", dest="stats_only", action="store_true")
49
+ p.add_argument("--no-code", action="store_true")
50
+ p.add_argument("--ids", action="store_true")
51
+ p.add_argument("--art-ids", dest="art_ids", action="store_true")
52
+
53
+ def _emit(self, x, *, json_only: bool) -> None:
54
+ if json_only:
55
+ self.rich.print_json(json.dumps(x, indent=2, default=str))
56
+ else:
57
+ self.rich.print(x)
58
+
59
+ def do_back(self, _arg) -> bool:
60
+ """Return to main shell."""
61
+ return True
62
+
63
+ do_exit = do_back
64
+ do_quit = do_back
65
+
66
+ # ----------------------
67
+ # view state/spec/domain/artifacts/instances
68
+ # ----------------------
69
+
70
+ state_p = argparse.ArgumentParser()
71
+ _ = state_p.add_argument
72
+ # common
73
+ # (cmd2 doesn't support easy composition; just call helper)
74
+ # We'll patch it in __init__? easiest: do it here:
75
+ # (but need self, so do it in method; use a helper below instead)
76
+
77
+ @with_argparser(argparse.ArgumentParser())
78
+ def do_state(self, args) -> None:
79
+ """View full state instance."""
80
+ if self.parent.state is None:
81
+ self.perror("No state loaded.")
82
+ return
83
+
84
+ # build parser on the fly so we can reuse helper
85
+ p = argparse.ArgumentParser()
86
+ self._add_common(p)
87
+ a = p.parse_args(args.argv)
88
+
89
+ si = self.parent.state.instances[a.idx]
90
+ rctx = _mk_ctx(a)
91
+ out = present_state_instance(si, ctx=rctx, json_only=a.json)
92
+ self._emit(out, json_only=a.json)
93
+
94
+ @with_argparser(argparse.ArgumentParser())
95
+ def do_spec(self, args) -> None:
96
+ if self.parent.state is None:
97
+ self.perror("No state loaded.")
98
+ return
99
+ p = argparse.ArgumentParser()
100
+ self._add_common(p)
101
+ a = p.parse_args(args.argv)
102
+
103
+ si = self.parent.state.instances[a.idx]
104
+ rctx = _mk_ctx(a)
105
+ out = present_spec(si.spec, ctx=rctx, json_only=a.json)
106
+ self._emit(out, json_only=a.json)
107
+
108
+ @with_argparser(argparse.ArgumentParser())
109
+ def do_domain(self, args) -> None:
110
+ if self.parent.state is None:
111
+ self.perror("No state loaded.")
112
+ return
113
+ p = argparse.ArgumentParser()
114
+ self._add_common(p)
115
+ a = p.parse_args(args.argv)
116
+
117
+ si = self.parent.state.instances[a.idx]
118
+ rctx = _mk_ctx(a)
119
+ out = present_domain_model(si.spec.domain_model, ctx=rctx, json_only=a.json)
120
+ self._emit(out, json_only=a.json)
121
+
122
+ @with_argparser(argparse.ArgumentParser())
123
+ def do_artifacts(self, args) -> None:
124
+ if self.parent.state is None:
125
+ self.perror("No state loaded.")
126
+ return
127
+ p = argparse.ArgumentParser()
128
+ self._add_common(p)
129
+ a = p.parse_args(args.argv)
130
+
131
+ si = self.parent.state.instances[a.idx]
132
+ rctx = _mk_ctx(a)
133
+ out = present_artifact_container(si.art_container, ctx=rctx, json_only=a.json)
134
+ self._emit(out, json_only=a.json)
135
+
136
+ @with_argparser(argparse.ArgumentParser())
137
+ def do_instances(self, args) -> None:
138
+ if self.parent.state is None:
139
+ self.perror("No state loaded.")
140
+ return
141
+ p = argparse.ArgumentParser()
142
+ # instances view doesn’t need --idx but keep flags consistent:
143
+ p.add_argument("--json", action="store_true")
144
+ p.add_argument("--compact", action="store_true")
145
+ p.add_argument("--stats-only", dest="stats_only", action="store_true")
146
+ p.add_argument("--no-code", action="store_true")
147
+ p.add_argument("--ids", action="store_true")
148
+ p.add_argument("--art-ids", dest="art_ids", action="store_true")
149
+ a = p.parse_args(args.argv)
150
+
151
+ rctx = _mk_ctx(a)
152
+ out = present_instances_list(self.parent.state.instances, ctx=rctx, json_only=a.json)
153
+ self._emit(out, json_only=a.json)
File without changes
@@ -0,0 +1,428 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/state/change.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ from rich.panel import Panel
10
+ from rich.text import Text
11
+ from rich.console import Group
12
+ from pydantic import BaseModel, Field, computed_field, model_validator
13
+ from typing import Literal, Optional
14
+
15
+ from ..modeling.domain import PredicateType
16
+ from ..modeling.scenario import ScenarioDelta
17
+
18
+ ProcessChangeStage = Literal["apply_change", "analyze_change", "calc_diff", "ok"]
19
+
20
+ class StateChange(BaseModel):
21
+ """ StateChange """
22
+
23
+ meta : str | None = None
24
+
25
+ @computed_field
26
+ @property
27
+ def kind(self) -> str:
28
+ return self.__class__.__name__
29
+
30
+ def short_str(self) -> str:
31
+ return f"[bold deep_blue_sky]{self.kind}[/bold deep_blue_sky]"
32
+
33
+ def __rich__(self) -> Panel:
34
+ rows: list[Text] = []
35
+ for k, v in self.model_dump(exclude_none=True, exclude={"kind"}).items():
36
+ rows.append(Text(f"{k} = {v!r}"))
37
+ return Panel(
38
+ Group(*rows),
39
+ title=f"[italic]{self.__class__.__name__}[/]",
40
+ title_align="left",
41
+ )
42
+
43
+ # ----------------------------
44
+ # Domain Base
45
+ # ----------------------------
46
+
47
+ class DomainModelBaseEdit(StateChange):
48
+ """ Edit the base component of domain model """
49
+ new_base_src : str = Field(..., description="IML source code of the base component")
50
+
51
+ def short_str(self) -> str:
52
+ return "[bold deep_sky_blue]DomainModelBaseEdit[/bold deep_sky_blue]"
53
+
54
+ def __str__ (self) -> str:
55
+ return ""
56
+
57
+ # ----------------------------
58
+ # Predicates
59
+ # ----------------------------
60
+
61
+ class PredicateAdd(StateChange):
62
+ """ Add a new predicate """
63
+ pred_type : PredicateType = Field(..., description="Predicate add")
64
+ pred_name : str = Field(..., description="predicate name (no spaces). no signature.")
65
+ pred_src : str = Field(..., description="predicate source code")
66
+
67
+ def short_str(self):
68
+ """ """
69
+ return "[bold deep_blue_sky]PredicateAdd[/bold deep_blue_sky]"
70
+
71
+ def __str__(self):
72
+ return ""
73
+
74
+ class PredicateEdit(StateChange):
75
+ """ Edit an existing predicate """
76
+ pred_name : str = Field(..., description="Predicate name (no spaces)")
77
+ pred_src : str = Field(..., description="Predicate source code. No signature.")
78
+
79
+ def short_str(self):
80
+ return "[bold deep_blue_sky]PredicateEdit[/bold deep_blue_sky]: [bold]name[/bold]={self.pred_name}"
81
+
82
+ def __str__(self):
83
+ return f'PredicateEdit: {self.pred_name}'
84
+
85
+ class PredicateRemove(StateChange):
86
+ """ Remove an existing predicate """
87
+ pred_name : str = Field(..., description="Predicate name to remove")
88
+
89
+ def short_str(self) -> str:
90
+ return f'PredicateRemove: name = {self.pred_name}'
91
+
92
+ def __str__(self) -> str:
93
+ return 'PredicateRemove'
94
+
95
+ # ----------------------------
96
+ # Transitions
97
+ # ----------------------------
98
+
99
+ class TransitionAdd(StateChange):
100
+ """ Add a new transition function """
101
+ trans_name : str = Field(..., description="Transition name")
102
+ trans_src : str = Field(..., description="Transition source code. No signature.")
103
+
104
+ def short_str(self) -> str:
105
+ return ''
106
+
107
+ def __str__(self) -> str:
108
+ return 'TransitionAdd: '
109
+
110
+ class TransitionEdit(StateChange):
111
+ """ Edit an existing transition function """
112
+ trans_name : str = Field(..., description="Transition name")
113
+ trans_src : str = Field(..., description="Updated transition code. No signature.")
114
+
115
+ def short_str(self):
116
+ return f"[bold deep_blue_sky]TransitionEdit[/bold deep_blue_sky]: [bold]trans_name[/bold]: {self.trans_name}"
117
+
118
+ def __str__(self):
119
+ return "TransitionEdit: "
120
+
121
+ class TransitionRemove(StateChange):
122
+ """ Remove an existing transition function """
123
+ trans_name : str = Field(..., description="Transition name")
124
+
125
+ def short_str(self):
126
+ return f"[bold deep_blue_sky]TransitionRemove[/bold deep_blue_sky]: [bold]trans_name[/bold]: {self.trans_name}"
127
+
128
+ def __str__(self):
129
+ return "TransitionRemove: "
130
+
131
+ # ----------------------------
132
+ # Scenarios
133
+ # ----------------------------
134
+
135
+ class ScenarioAdd(StateChange):
136
+ """
137
+ Add a new scenario using set-delta semantics.
138
+ For creation, deltas must be add-only (remove must be empty).
139
+ """
140
+ scenario_name: str
141
+ given: Optional[ScenarioDelta] = None
142
+ when: Optional[ScenarioDelta] = None
143
+ then: Optional[ScenarioDelta] = None
144
+
145
+ @model_validator(mode="after")
146
+ def _add_only(self) -> "ScenarioAdd":
147
+ for sec_name, delta in (("given", self.given), ("when", self.when), ("then", self.then)):
148
+ if delta is not None and not delta.is_add_only():
149
+ raise ValueError(f"ScenarioAdd.{sec_name}: 'remove' is not allowed when creating a scenario")
150
+ return self
151
+
152
+ def short_str(self) -> str:
153
+ return f"ScenarioAdd: name={self.scenario_name}"
154
+
155
+ Section = Literal["given", "when", "then"]
156
+
157
+ class ScenarioEdit(StateChange):
158
+ scenario_name: str
159
+ given: ScenarioDelta | None = None
160
+ when: ScenarioDelta | None = None
161
+ then: ScenarioDelta | None = None
162
+
163
+ @model_validator(mode="after")
164
+ def _must_have_some_delta(self) -> "ScenarioEdit":
165
+ deltas = [d for d in (self.given, self.when, self.then) if d is not None]
166
+
167
+ if not deltas:
168
+ raise ValueError("At least one of given/when/then must be provided.")
169
+
170
+ if all(d.is_empty() for d in deltas):
171
+ raise ValueError("ScenarioEdit has no effect: all provided deltas are empty.")
172
+
173
+ return self
174
+
175
+ def short_str(self) -> str:
176
+ parts = []
177
+ for sec in ("given", "when", "then"):
178
+ delta = getattr(self, sec)
179
+ if delta:
180
+ if delta.add:
181
+ parts.append(f"{sec} +{delta.add}")
182
+ if delta.remove:
183
+ parts.append(f"{sec} -{delta.remove}")
184
+ return f"ScenarioStepsEdit[{self.scenario_name}]: " + ", ".join(parts)
185
+
186
+ class ScenarioRemove(StateChange):
187
+ """ Remove an existing scenario """
188
+ scenario_name : str = Field(..., description="Name of the scenario to remove")
189
+
190
+ def short_str(self):
191
+ return f"[bold deep_blue_sky]ScenarioRemove[/bold deep_blue_sky]: [bold]Name:[/bold]{self.scenario_name}"
192
+
193
+ # ----------------------------
194
+ # Test traces
195
+ # ----------------------------
196
+
197
+ class RemoveTraceArtifact(StateChange):
198
+ """Remove an existing trace artifact (TestTrace or LogTrace)."""
199
+ art_id: str = Field(description="Artifact ID")
200
+
201
+ def short_str(self) -> str:
202
+ return (
203
+ f"[bold deep_blue_sky]RemoveTraceArtifact[/bold deep_blue_sky]: "
204
+ f"[bold]ArtID[/bold]: {self.art_id}"
205
+ )
206
+
207
+ class AddTestTrace(StateChange):
208
+ """Add a new TestTrace."""
209
+ art_id: str | None = Field(description="Artifact ID")
210
+
211
+ # TraceArtifact core
212
+ given: str = Field(description="Concrete state value (IML expression)")
213
+ when: Optional[str] = Field(default=None, description="Optional concrete action value (IML expression)")
214
+ then: Optional[str] = Field(default=None, description="Optional resulting state value (IML expression)")
215
+ time: str = Field(default="", description="Optional timestamp")
216
+
217
+ # TestTrace fields
218
+ name: str = Field(description="Test case name (not unique)")
219
+ filepath: str = Field(default="", description="Original test filepath")
220
+ language: Optional[str] = Field(default=None, description="Language of the original test source")
221
+ contents: Optional[str] = Field(default=None, description="Original test contents")
222
+
223
+ def short_str(self) -> str:
224
+ return (
225
+ f"[bold deep_blue_sky]AddTestTrace[/bold deep_blue_sky]: "
226
+ f"[bold]ArtID[/bold]: {self.art_id}"
227
+ )
228
+
229
+ class EditTestTrace(StateChange):
230
+ """Edit an existing TestTrace (patch semantics: only non-None fields are applied)."""
231
+ art_id: str = Field(description="Artifact ID")
232
+
233
+ # TraceArtifact core (patch)
234
+ given: Optional[str] = Field(default=None, description="Concrete state value (IML expression)")
235
+ when: Optional[str] = Field(default=None, description="Optional concrete action value (IML expression)")
236
+ then: Optional[str] = Field(default=None, description="Optional resulting state value (IML expression)")
237
+ time: Optional[str] = Field(default=None, description="Optional timestamp")
238
+
239
+ # TestTrace fields (patch)
240
+ name: Optional[str] = Field(default=None, description="Test case name (not unique)")
241
+ filepath: Optional[str] = Field(default=None, description="Original test filepath")
242
+ language: Optional[str] = Field(default=None, description="Language of the original test source")
243
+ contents: Optional[str] = Field(default=None, description="Original test contents")
244
+
245
+ @model_validator(mode="after")
246
+ def _must_change_something(self) -> EditTestTrace:
247
+ if (
248
+ self.given is None
249
+ and self.when is None
250
+ and self.then is None
251
+ and self.time is None
252
+ and self.name is None
253
+ and self.filepath is None
254
+ and self.language is None
255
+ and self.contents is None
256
+ ):
257
+ raise ValueError(
258
+ "EditTestTrace must provide at least one of: "
259
+ "given, when, then, time, name, filepath, language, or contents"
260
+ )
261
+ return self
262
+
263
+ def short_str(self) -> str:
264
+ return (
265
+ f"[bold deep_blue_sky]EditTestTrace[/bold deep_blue_sky]: "
266
+ f"[bold]ArtID[/bold]: {self.art_id}"
267
+ )
268
+
269
+ # ----------------------------
270
+ # Log traces
271
+ # ----------------------------
272
+
273
+ class AddLogTrace(StateChange):
274
+ """Add a new LogTrace."""
275
+ art_id: str | None = Field(description="Artifact ID")
276
+
277
+ # TraceArtifact core
278
+ given: str = Field(description="Concrete state value (IML expression)")
279
+ when: Optional[str] = Field(default=None, description="Optional concrete action value (IML expression)")
280
+ then: Optional[str] = Field(default=None, description="Optional resulting state value (IML expression)")
281
+ time: str = Field(default="", description="Optional timestamp")
282
+
283
+ # LogTrace fields
284
+ filename: str = Field(description="Log filename (not unique)")
285
+ contents: str = Field(description="Log entry contents")
286
+
287
+ def short_str(self) -> str:
288
+ return (
289
+ f"[bold deep_blue_sky]AddLogTrace[/bold deep_blue_sky]: "
290
+ f"[bold]ArtID[/bold]: {self.art_id}"
291
+ )
292
+
293
+ class EditLogTrace(StateChange):
294
+ """Edit an existing LogTrace (patch semantics: only non-None fields are applied)."""
295
+ art_id: str = Field(description="Artifact ID")
296
+
297
+ # TraceArtifact core (patch)
298
+ given: Optional[str] = Field(default=None, description="Concrete state value (IML expression)")
299
+ when: Optional[str] = Field(default=None, description="Optional concrete action value (IML expression)")
300
+ then: Optional[str] = Field(default=None, description="Optional resulting state value (IML expression)")
301
+ time: Optional[str] = Field(default=None, description="Optional timestamp")
302
+
303
+ # LogTrace fields (patch)
304
+ filename: Optional[str] = Field(default=None, description="Log filename (not unique)")
305
+ contents: Optional[str] = Field(default=None, description="Log entry contents")
306
+
307
+ @model_validator(mode="after")
308
+ def _must_change_something(self) -> EditLogTrace:
309
+ if (
310
+ self.given is None
311
+ and self.when is None
312
+ and self.then is None
313
+ and self.time is None
314
+ and self.filename is None
315
+ and self.contents is None
316
+ ):
317
+ raise ValueError(
318
+ "EditLogTrace must provide at least one of: "
319
+ "given, when, then, time, filename, or contents"
320
+ )
321
+ return self
322
+
323
+ def short_str(self) -> str:
324
+ return (
325
+ f"[bold deep_blue_sky]EditLogTrace[/bold deep_blue_sky]: "
326
+ f"[bold]ArtID[/bold]: {self.art_id}"
327
+ )
328
+
329
+ # ----------------------------
330
+ # RemoveDataArtifact
331
+ # ----------------------------
332
+
333
+ class RemoveDataArtifact(StateChange):
334
+ """ Remove an existing data artifact """
335
+ art_id: str = Field(description="Artifact ID")
336
+ def short_str(self) -> str:
337
+ return "[bold deep_blue_sky]RemoveDataArtifact[/bold deep_blue_sky]"
338
+
339
+ # ----------------------------
340
+ # Documentation references
341
+ # ----------------------------
342
+
343
+ class AddDocRef(StateChange):
344
+ """Add a new documentation reference artifact."""
345
+ art_id: str | None = Field(description="Artifact ID")
346
+ text: str = Field(description="Documentation text")
347
+ meta: str | None = Field(default=None, description="Optional metadata")
348
+
349
+ def short_str(self) -> str:
350
+ return f"[bold deep_blue_sky]AddDocRef[/bold deep_blue_sky]: [bold]ArtID[/bold]: {self.art_id}"
351
+
352
+ class EditDocRef(StateChange):
353
+ """Edit an existing DocRef."""
354
+ art_id: str = Field(description="Artifact ID")
355
+ text: str | None = Field(default=None, description="New documentation text")
356
+ meta: str | None = Field(default=None, description="New metadata")
357
+
358
+ @model_validator(mode="after")
359
+ def _must_change_something(self) -> "EditDocRef":
360
+ if self.text is None and self.meta is None:
361
+ raise ValueError("EditDocRef must provide at least one of: text or meta")
362
+ return self
363
+
364
+ def short_str(self) -> str:
365
+ return f"[bold deep_blue_sky]EditDocRef[/bold deep_blue_sky]: [bold]ArtID[/bold]: {self.art_id}"
366
+
367
+ # ----------------------------
368
+ # Source code references
369
+ # ----------------------------
370
+
371
+ class AddSrcCodeRef(StateChange):
372
+ """Add a new source code reference artifact."""
373
+ art_id: str | None = Field(description="Artifact ID")
374
+ src_code: str = Field(description="Source code")
375
+ language: str | None = Field(default=None, description="Programming language")
376
+ file_path: str | None = Field(default=None, description="Optional file path")
377
+ iml_code: str | None = Field(default=None, description="Optional IML formalization")
378
+ meta: str | None = Field(default=None, description="Optional metadata")
379
+
380
+ def short_str(self) -> str:
381
+ return f"[bold deep_blue_sky]AddSrcCodeRef[/bold deep_blue_sky]: [bold]ArtID[/bold]: {self.art_id}"
382
+
383
+ class EditSrcCodeRef(StateChange):
384
+ """Edit an existing SrcCodeRef."""
385
+ art_id: str = Field(description="Artifact ID")
386
+ src_code: str | None = Field(default=None, description="Updated source code")
387
+ language: str | None = Field(default=None, description="Updated language")
388
+ file_path: str | None = Field(default=None, description="Updated file path")
389
+ iml_code: str | None = Field(default=None, description="Updated IML formalization")
390
+ meta: str | None = Field(default=None, description="Updated metadata")
391
+
392
+ @model_validator(mode="after")
393
+ def _must_change_something(self) -> EditSrcCodeRef:
394
+ if (
395
+ self.src_code is None
396
+ and self.language is None
397
+ and self.file_path is None
398
+ and self.iml_code is None
399
+ and self.meta is None
400
+ ):
401
+ raise ValueError(
402
+ "EditSrcCodeRef must provide at least one of: "
403
+ "src_code, language, file_path, iml_code, or meta"
404
+ )
405
+ return self
406
+
407
+ def short_str(self) -> str:
408
+ return f"[bold deep_blue_sky]EditSrcCodeRef[/bold deep_blue_sky]: [bold]ArtID[/bold]: {self.art_id}"
409
+
410
+ # ----------------------------
411
+ # Links in the ArtifactMap
412
+ # ----------------------------
413
+
414
+ class LinkArtifactsComponents(StateChange):
415
+ """ Add a link between a non-executable arifact (e.g. source code or documentation) and a model component """
416
+ art_id : str = Field(description="Artifact ID")
417
+ comp_name : str = Field(description="Component name")
418
+
419
+ def short_str(self) -> str:
420
+ return "[bold deep_blue_sky]LinkArtifactComponents[/bold deep_blue_sky]"
421
+
422
+ class RemoveArtComponentLink(StateChange):
423
+ """ Remove a link between a non-executable arifact (e.g. source code or documentation) and a model component """
424
+ art_id : str = Field(description="Artifact ID")
425
+ comp_name : str = Field(description="Component name")
426
+
427
+ def short_str(self) -> str:
428
+ return "[bold deep_blue_sky]RemoveArtComponentLink[/bold deep_blue_sky]"
@@ -0,0 +1,32 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/state/change_result.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Literal, TypedDict
10
+
11
+ from ..state.change import StateChange
12
+ from ..state.inst import StateInstance
13
+ from ..state.diff import StateDiff
14
+ from typing import Union
15
+
16
+ class ProcessChangeError(TypedDict):
17
+ ok: Literal[False]
18
+ stage: Literal["apply_change", "analyze_change", "calc_diff", "refresh_stats"]
19
+ error: Exception
20
+ change: StateChange
21
+ old_state: StateInstance
22
+
23
+ class ProcessChangeSuccess(TypedDict):
24
+ ok: Literal[True]
25
+ stage: Literal["ok"]
26
+ change: StateChange
27
+ old_state: StateInstance
28
+ new_state: StateInstance
29
+ state_diff: StateDiff | None # this is because it could potentially be the first state instance
30
+ new_num_instances: int
31
+
32
+ ProcessChangeResult = Union[ProcessChangeError, ProcessChangeSuccess]