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.
- speclogician/__init__.py +0 -0
- speclogician/commands/__init__.py +15 -0
- speclogician/commands/cmd_ch.py +616 -0
- speclogician/commands/cmd_find.py +256 -0
- speclogician/commands/cmd_view.py +202 -0
- speclogician/commands/runner.py +149 -0
- speclogician/commands/utils.py +101 -0
- speclogician/data/__init__.py +0 -0
- speclogician/data/artifact.py +63 -0
- speclogician/data/container.py +402 -0
- speclogician/data/mapping.py +88 -0
- speclogician/data/refs.py +24 -0
- speclogician/data/traces.py +26 -0
- speclogician/demos/.DS_Store +0 -0
- speclogician/demos/cmd_demo.py +278 -0
- speclogician/demos/loader.py +135 -0
- speclogician/demos/model.py +27 -0
- speclogician/demos/runner.py +51 -0
- speclogician/logic/__init__.py +11 -0
- speclogician/logic/api/__init__.py +29 -0
- speclogician/logic/api/client.py +606 -0
- speclogician/logic/api/decomp.py +67 -0
- speclogician/logic/api/scenario.py +102 -0
- speclogician/logic/api/traces.py +59 -0
- speclogician/logic/lib/__init__.py +19 -0
- speclogician/logic/lib/complement.py +107 -0
- speclogician/logic/lib/domain_model.py +59 -0
- speclogician/logic/lib/predicates.py +151 -0
- speclogician/logic/lib/scenarios.py +369 -0
- speclogician/logic/lib/traces.py +114 -0
- speclogician/logic/lib/transitions.py +104 -0
- speclogician/logic/main.py +246 -0
- speclogician/logic/strings.py +194 -0
- speclogician/logic/utils.py +135 -0
- speclogician/main.py +139 -0
- speclogician/modeling/__init__.py +31 -0
- speclogician/modeling/complement.py +104 -0
- speclogician/modeling/component.py +71 -0
- speclogician/modeling/conflict.py +26 -0
- speclogician/modeling/domain.py +349 -0
- speclogician/modeling/predicates.py +59 -0
- speclogician/modeling/scenario.py +162 -0
- speclogician/modeling/spec.py +306 -0
- speclogician/modeling/spec_stats.py +39 -0
- speclogician/presentation/__init__.py +0 -0
- speclogician/presentation/api.py +244 -0
- speclogician/presentation/builders/__init__.py +0 -0
- speclogician/presentation/builders/_links.py +44 -0
- speclogician/presentation/builders/container.py +53 -0
- speclogician/presentation/builders/data_artifact.py +42 -0
- speclogician/presentation/builders/domain.py +54 -0
- speclogician/presentation/builders/instances_list.py +38 -0
- speclogician/presentation/builders/predicate.py +51 -0
- speclogician/presentation/builders/recommendations.py +41 -0
- speclogician/presentation/builders/scenario.py +41 -0
- speclogician/presentation/builders/scenario_complement.py +82 -0
- speclogician/presentation/builders/smart_find.py +39 -0
- speclogician/presentation/builders/spec.py +39 -0
- speclogician/presentation/builders/state_diff.py +150 -0
- speclogician/presentation/builders/state_instance.py +42 -0
- speclogician/presentation/builders/state_instance_summary.py +84 -0
- speclogician/presentation/builders/trace.py +58 -0
- speclogician/presentation/ctx.py +38 -0
- speclogician/presentation/models/__init__.py +0 -0
- speclogician/presentation/models/container.py +44 -0
- speclogician/presentation/models/data_artifact.py +33 -0
- speclogician/presentation/models/domain.py +50 -0
- speclogician/presentation/models/instances_list.py +23 -0
- speclogician/presentation/models/predicate.py +60 -0
- speclogician/presentation/models/recommendations.py +34 -0
- speclogician/presentation/models/scenario.py +31 -0
- speclogician/presentation/models/scenario_complement.py +40 -0
- speclogician/presentation/models/smart_find.py +34 -0
- speclogician/presentation/models/spec.py +32 -0
- speclogician/presentation/models/state_diff.py +34 -0
- speclogician/presentation/models/state_instance.py +31 -0
- speclogician/presentation/models/state_instance_summary.py +102 -0
- speclogician/presentation/models/trace.py +42 -0
- speclogician/presentation/preview/__init__.py +13 -0
- speclogician/presentation/preview/cli.py +50 -0
- speclogician/presentation/preview/fixtures/__init__.py +205 -0
- speclogician/presentation/preview/fixtures/artifact_container.py +150 -0
- speclogician/presentation/preview/fixtures/data_artifact.py +144 -0
- speclogician/presentation/preview/fixtures/domain_model.py +162 -0
- speclogician/presentation/preview/fixtures/instances_list.py +162 -0
- speclogician/presentation/preview/fixtures/predicate.py +184 -0
- speclogician/presentation/preview/fixtures/scenario.py +84 -0
- speclogician/presentation/preview/fixtures/scenario_complement.py +81 -0
- speclogician/presentation/preview/fixtures/smart_find.py +140 -0
- speclogician/presentation/preview/fixtures/spec.py +95 -0
- speclogician/presentation/preview/fixtures/state_diff.py +158 -0
- speclogician/presentation/preview/fixtures/state_instance.py +128 -0
- speclogician/presentation/preview/fixtures/state_instance_summary.py +80 -0
- speclogician/presentation/preview/fixtures/trace.py +206 -0
- speclogician/presentation/preview/registry.py +42 -0
- speclogician/presentation/renderers/__init__.py +24 -0
- speclogician/presentation/renderers/container.py +136 -0
- speclogician/presentation/renderers/data_artifact.py +144 -0
- speclogician/presentation/renderers/domain.py +123 -0
- speclogician/presentation/renderers/instances_list.py +120 -0
- speclogician/presentation/renderers/predicate.py +180 -0
- speclogician/presentation/renderers/recommendations.py +90 -0
- speclogician/presentation/renderers/scenario.py +94 -0
- speclogician/presentation/renderers/scenario_complement.py +59 -0
- speclogician/presentation/renderers/smart_find.py +307 -0
- speclogician/presentation/renderers/spec.py +105 -0
- speclogician/presentation/renderers/state_diff.py +102 -0
- speclogician/presentation/renderers/state_instance.py +82 -0
- speclogician/presentation/renderers/state_instance_summary.py +143 -0
- speclogician/presentation/renderers/trace.py +122 -0
- speclogician/py.typed +0 -0
- speclogician/shell/app.py +170 -0
- speclogician/shell/shell_ch.py +263 -0
- speclogician/shell/shell_view.py +153 -0
- speclogician/state/__init__.py +0 -0
- speclogician/state/change.py +428 -0
- speclogician/state/change_result.py +32 -0
- speclogician/state/diff.py +191 -0
- speclogician/state/inst.py +574 -0
- speclogician/state/recommendation.py +13 -0
- speclogician/state/recommender.py +577 -0
- speclogician/state/state.py +465 -0
- speclogician/state/state_stats.py +133 -0
- speclogician/tui/__init__.py +0 -0
- speclogician/tui/app.py +257 -0
- speclogician/tui/app.tcss +160 -0
- speclogician/tui/demo.py +45 -0
- speclogician/tui/images/speclogician-full.png +0 -0
- speclogician/tui/images/speclogician-minimal.png +0 -0
- speclogician/tui/main_screen.py +454 -0
- speclogician/tui/splash_screen.py +51 -0
- speclogician/tui/stats_screen.py +125 -0
- speclogician/utils/__init__.py +78 -0
- speclogician/utils/load.py +166 -0
- speclogician/utils/prompt.md +325 -0
- speclogician/utils/testing.py +151 -0
- speclogician-0.0.0b1.dist-info/METADATA +116 -0
- speclogician-0.0.0b1.dist-info/RECORD +139 -0
- speclogician-0.0.0b1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Imandra Inc.
|
|
3
|
+
#
|
|
4
|
+
# speclogician/state/state.py
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from ..modeling.spec_stats import refresh_spec_stats
|
|
12
|
+
from ..logic import analyze_change
|
|
13
|
+
from ..data.refs import DocRef, SrcCodeRef
|
|
14
|
+
from ..data.traces import TestTrace, LogTrace
|
|
15
|
+
from ..utils import console
|
|
16
|
+
from .inst import StateInstance, InstancesList, calc_diff
|
|
17
|
+
from .recommender import Recommender, Recommendation, ChangeFailure
|
|
18
|
+
|
|
19
|
+
from .change import (
|
|
20
|
+
StateChange,
|
|
21
|
+
DomainModelBaseEdit,
|
|
22
|
+
PredicateAdd,
|
|
23
|
+
PredicateEdit,
|
|
24
|
+
PredicateRemove,
|
|
25
|
+
TransitionAdd,
|
|
26
|
+
TransitionEdit,
|
|
27
|
+
TransitionRemove,
|
|
28
|
+
ScenarioAdd,
|
|
29
|
+
ScenarioEdit,
|
|
30
|
+
ScenarioRemove,
|
|
31
|
+
AddTestTrace,
|
|
32
|
+
EditTestTrace,
|
|
33
|
+
AddLogTrace,
|
|
34
|
+
EditLogTrace,
|
|
35
|
+
RemoveTraceArtifact,
|
|
36
|
+
AddDocRef,
|
|
37
|
+
EditDocRef,
|
|
38
|
+
AddSrcCodeRef,
|
|
39
|
+
EditSrcCodeRef,
|
|
40
|
+
RemoveDataArtifact,
|
|
41
|
+
LinkArtifactsComponents,
|
|
42
|
+
RemoveArtComponentLink
|
|
43
|
+
)
|
|
44
|
+
from .change_result import (
|
|
45
|
+
ProcessChangeResult,
|
|
46
|
+
ProcessChangeError,
|
|
47
|
+
ProcessChangeSuccess,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
class State(BaseModel):
|
|
51
|
+
"""
|
|
52
|
+
SpecLogician state instance container
|
|
53
|
+
"""
|
|
54
|
+
file_location : str | None = Field(default=None)
|
|
55
|
+
instances : list[StateInstance] = [StateInstance()]
|
|
56
|
+
stash : list[StateInstance] = Field(default_factory=list[StateInstance]) # we'll use this to store discarded instances
|
|
57
|
+
|
|
58
|
+
def curr_state(self) -> StateInstance:
|
|
59
|
+
""" """
|
|
60
|
+
return self.instances[0]
|
|
61
|
+
|
|
62
|
+
def inst_list(self):
|
|
63
|
+
""" Return a InstancesList (for pretty printing) """
|
|
64
|
+
return InstancesList(states=self.instances)
|
|
65
|
+
|
|
66
|
+
def process_change(
|
|
67
|
+
self,
|
|
68
|
+
change: StateChange,
|
|
69
|
+
json_only: bool = False,
|
|
70
|
+
) -> ProcessChangeResult:
|
|
71
|
+
"""
|
|
72
|
+
Apply a change and return a structured result.
|
|
73
|
+
|
|
74
|
+
- json_only=False: returns object payload for rich printing / internal callers.
|
|
75
|
+
- json_only=True : returns JSON-safe payload (summaries + json dumps).
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
old_state: StateInstance = self.curr_state()
|
|
79
|
+
|
|
80
|
+
# Create a new candidate instance (deterministic snapshot + change list)
|
|
81
|
+
state_copy = old_state.model_copy(deep=True)
|
|
82
|
+
state_copy.update_created_at()
|
|
83
|
+
state_copy.changes = [change]
|
|
84
|
+
|
|
85
|
+
# 1) Apply change to state
|
|
86
|
+
try:
|
|
87
|
+
new_state: StateInstance = self.run_change_command(state_copy, change)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
# recommendations still useful on failure (triage)
|
|
90
|
+
failure = ChangeFailure(
|
|
91
|
+
change=change.__class__.__name__,
|
|
92
|
+
stage="apply_change",
|
|
93
|
+
error_type=type(e).__name__,
|
|
94
|
+
error=str(e),
|
|
95
|
+
)
|
|
96
|
+
# NOTE: no new_state exists; we recommend against old_state
|
|
97
|
+
recs = Recommender().recommend(old_state, old_state, failure=failure)
|
|
98
|
+
|
|
99
|
+
return ProcessChangeError(
|
|
100
|
+
ok=False,
|
|
101
|
+
stage="apply_change",
|
|
102
|
+
error=e,
|
|
103
|
+
change=change,
|
|
104
|
+
old_state=old_state,
|
|
105
|
+
# If your ProcessChangeError supports it, include recs:
|
|
106
|
+
# recommendations=recs,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# 2) Perform logical analysis
|
|
110
|
+
try:
|
|
111
|
+
analyze_change(
|
|
112
|
+
spec=new_state.spec,
|
|
113
|
+
art_map=new_state.art_map,
|
|
114
|
+
art_cont=new_state.art_container,
|
|
115
|
+
change=change,
|
|
116
|
+
json_only=json_only,
|
|
117
|
+
)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
failure = ChangeFailure(
|
|
120
|
+
change=change.__class__.__name__,
|
|
121
|
+
stage="analyze_change",
|
|
122
|
+
error_type=type(e).__name__,
|
|
123
|
+
error=str(e),
|
|
124
|
+
)
|
|
125
|
+
recs = Recommender().recommend(old_state, new_state, failure=failure)
|
|
126
|
+
# Optionally store on the candidate for debugging:
|
|
127
|
+
new_state.recommendations = recs
|
|
128
|
+
|
|
129
|
+
return ProcessChangeError(
|
|
130
|
+
ok=False,
|
|
131
|
+
stage="analyze_change",
|
|
132
|
+
error=e,
|
|
133
|
+
change=change,
|
|
134
|
+
old_state=old_state,
|
|
135
|
+
# If supported:
|
|
136
|
+
# new_state=new_state,
|
|
137
|
+
# recommendations=recs,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# 3) Update stats (should not crash the whole pipeline ideally, but keeping your behavior)
|
|
141
|
+
try:
|
|
142
|
+
new_state.art_container.refresh_stats(new_state.art_map)
|
|
143
|
+
refresh_spec_stats(new_state.spec, art_map=new_state.art_map)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
failure = ChangeFailure(
|
|
146
|
+
change=change.__class__.__name__,
|
|
147
|
+
stage="refresh_stats",
|
|
148
|
+
error_type=type(e).__name__,
|
|
149
|
+
error=str(e),
|
|
150
|
+
)
|
|
151
|
+
recs = Recommender().recommend(old_state, new_state, failure=failure)
|
|
152
|
+
new_state.recommendations = recs
|
|
153
|
+
|
|
154
|
+
return ProcessChangeError(
|
|
155
|
+
ok=False,
|
|
156
|
+
stage="refresh_stats",
|
|
157
|
+
error=e,
|
|
158
|
+
change=change,
|
|
159
|
+
old_state=old_state,
|
|
160
|
+
# new_state=new_state,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# 4) Commit (newest at index 0)
|
|
164
|
+
self.instances.insert(0, new_state)
|
|
165
|
+
|
|
166
|
+
# 5) Diff (old -> new), only if we actually have a previous instance
|
|
167
|
+
try:
|
|
168
|
+
if len(self.instances) >= 2:
|
|
169
|
+
# old = self.instances[1], new = self.instances[0]
|
|
170
|
+
self.instances[0].state_diff = calc_diff(self.instances[1], self.instances[0])
|
|
171
|
+
else:
|
|
172
|
+
self.instances[0].state_diff = None
|
|
173
|
+
except Exception as e:
|
|
174
|
+
failure = ChangeFailure(
|
|
175
|
+
change=change.__class__.__name__,
|
|
176
|
+
stage="calc_diff",
|
|
177
|
+
error_type=type(e).__name__,
|
|
178
|
+
error=str(e),
|
|
179
|
+
)
|
|
180
|
+
# We *do* have a committed new state now
|
|
181
|
+
recs = Recommender().recommend(old_state, self.instances[0], failure=failure)
|
|
182
|
+
self.instances[0].recommendations = recs
|
|
183
|
+
|
|
184
|
+
return ProcessChangeError(
|
|
185
|
+
ok=False,
|
|
186
|
+
stage="calc_diff",
|
|
187
|
+
error=e,
|
|
188
|
+
change=change,
|
|
189
|
+
old_state=old_state,
|
|
190
|
+
# new_state=self.instances[0],
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# 6) Compute recommendations (success path)
|
|
194
|
+
try:
|
|
195
|
+
# old is previous instance if it exists; otherwise old_state (same as new baseline)
|
|
196
|
+
prev = self.instances[1] if len(self.instances) >= 2 else self.instances[0]
|
|
197
|
+
self.instances[0].recommendations = Recommender().recommend(prev, self.instances[0])
|
|
198
|
+
except Exception as e:
|
|
199
|
+
# Recommender should never take down the CLI; but if it does, degrade gracefully.
|
|
200
|
+
self.instances[0].recommendations = [
|
|
201
|
+
Recommendation(
|
|
202
|
+
text=f"Recommender failed: {type(e).__name__}: {e}",
|
|
203
|
+
kind="warning",
|
|
204
|
+
priority=5,
|
|
205
|
+
)
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
# 7) Success
|
|
209
|
+
return ProcessChangeSuccess(
|
|
210
|
+
ok=True,
|
|
211
|
+
stage="ok",
|
|
212
|
+
change=change,
|
|
213
|
+
old_state=(self.instances[1] if len(self.instances) >= 2 else old_state),
|
|
214
|
+
new_state=self.instances[0],
|
|
215
|
+
state_diff=self.instances[0].state_diff,
|
|
216
|
+
new_num_instances=len(self.instances),
|
|
217
|
+
# If your success model includes it:
|
|
218
|
+
# recommendations=self.instances[0].recommendations,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def run_change_command (
|
|
222
|
+
self,
|
|
223
|
+
s : StateInstance,
|
|
224
|
+
change : StateChange,
|
|
225
|
+
) -> StateInstance:
|
|
226
|
+
"""
|
|
227
|
+
Process change
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
if isinstance (change, DomainModelBaseEdit):
|
|
231
|
+
s.spec.domain_model.set_base(change.new_base_src)
|
|
232
|
+
|
|
233
|
+
elif isinstance (change, PredicateAdd):
|
|
234
|
+
if s.spec.domain_model.pred_exists(change.pred_name):
|
|
235
|
+
raise ValueError (f"Predicate with name {change.pred_name} already exists")
|
|
236
|
+
|
|
237
|
+
s.spec.domain_model.add_predicate(change.pred_name, change.pred_type, change.pred_src)
|
|
238
|
+
|
|
239
|
+
elif isinstance (change, PredicateEdit):
|
|
240
|
+
if not s.spec.domain_model.pred_exists(change.pred_name):
|
|
241
|
+
raise ValueError (f"Predicate with name {change.pred_name} doesn't exist")
|
|
242
|
+
|
|
243
|
+
s.spec.domain_model.edit_predicate(change.pred_name, change.pred_src)
|
|
244
|
+
|
|
245
|
+
elif isinstance (change, PredicateRemove):
|
|
246
|
+
if not s.spec.domain_model.pred_exists(change.pred_name):
|
|
247
|
+
raise ValueError (f"Predicate with name {change.pred_name} doesn't exist")
|
|
248
|
+
|
|
249
|
+
s.spec.domain_model.rem_predicate(change.pred_name)
|
|
250
|
+
|
|
251
|
+
elif isinstance (change, TransitionAdd):
|
|
252
|
+
if s.spec.domain_model.trans_exists(change.trans_name):
|
|
253
|
+
raise ValueError (f"Transition with this name {change.trans_name} already exists!")
|
|
254
|
+
|
|
255
|
+
s.spec.domain_model.add_transition(name=change.trans_name, src_code=change.trans_src)
|
|
256
|
+
|
|
257
|
+
elif isinstance (change, TransitionEdit):
|
|
258
|
+
if not s.spec.domain_model.trans_exists(change.trans_name):
|
|
259
|
+
raise ValueError (f"Transition with this name {change.trans_name} doesn't exist")
|
|
260
|
+
s.spec.domain_model.edit_transition(name=change.trans_name, src_code=change.trans_src)
|
|
261
|
+
|
|
262
|
+
elif isinstance (change, TransitionRemove):
|
|
263
|
+
if not s.spec.domain_model.trans_exists(change.trans_name):
|
|
264
|
+
raise ValueError (f"Transition with this name {change.trans_name} doesn't exist")
|
|
265
|
+
|
|
266
|
+
s.spec.domain_model.rem_transition(change.trans_name)
|
|
267
|
+
|
|
268
|
+
elif isinstance(change, ScenarioAdd):
|
|
269
|
+
if s.spec.scenario_exists(change.scenario_name):
|
|
270
|
+
raise ValueError(f"Scenario with this name {change.scenario_name} already exists")
|
|
271
|
+
|
|
272
|
+
s.spec.add_scenario(
|
|
273
|
+
change.scenario_name,
|
|
274
|
+
change.given,
|
|
275
|
+
change.when,
|
|
276
|
+
change.then,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
elif isinstance (change, ScenarioEdit):
|
|
280
|
+
if not s.spec.scenario_exists(change.scenario_name):
|
|
281
|
+
raise ValueError (f"Scenario with the name '{change.scenario_name}' does not exist!")
|
|
282
|
+
|
|
283
|
+
s.spec.edit_scenario(
|
|
284
|
+
change.scenario_name,
|
|
285
|
+
change.given,
|
|
286
|
+
change.when, change.then
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
elif isinstance (change, ScenarioRemove):
|
|
290
|
+
if not s.spec.scenario_exists(change.scenario_name):
|
|
291
|
+
raise ValueError (f"Scenario with the name '{change.scenario_name}' does not exist!")
|
|
292
|
+
|
|
293
|
+
s.spec.rem_scenario(change.scenario_name)
|
|
294
|
+
|
|
295
|
+
# Test Traces
|
|
296
|
+
|
|
297
|
+
elif isinstance(change, AddTestTrace):
|
|
298
|
+
tr = TestTrace(
|
|
299
|
+
art_id=change.art_id,
|
|
300
|
+
# TraceArtifact core
|
|
301
|
+
given=change.given,
|
|
302
|
+
when=change.when,
|
|
303
|
+
then=change.then,
|
|
304
|
+
time=change.time,
|
|
305
|
+
# TestTrace specific
|
|
306
|
+
name=change.name,
|
|
307
|
+
filepath=change.filepath,
|
|
308
|
+
language=change.language,
|
|
309
|
+
contents=change.contents,
|
|
310
|
+
)
|
|
311
|
+
s.art_container.add_trace(tr)
|
|
312
|
+
|
|
313
|
+
elif isinstance(change, EditTestTrace):
|
|
314
|
+
# Use model_fields_set so passing None explicitly can clear fields.
|
|
315
|
+
patch: dict[str, object] = {}
|
|
316
|
+
for f in change.model_fields_set:
|
|
317
|
+
if f == "art_id":
|
|
318
|
+
continue
|
|
319
|
+
patch[f] = getattr(change, f)
|
|
320
|
+
s.art_container.edit_trace(change.art_id, patch)
|
|
321
|
+
|
|
322
|
+
elif isinstance(change, AddLogTrace):
|
|
323
|
+
tr = LogTrace(
|
|
324
|
+
art_id=change.art_id,
|
|
325
|
+
# TraceArtifact core
|
|
326
|
+
given=change.given,
|
|
327
|
+
when=change.when,
|
|
328
|
+
then=change.then,
|
|
329
|
+
time=change.time,
|
|
330
|
+
# LogTrace specific
|
|
331
|
+
filename=change.filename,
|
|
332
|
+
contents=change.contents,
|
|
333
|
+
)
|
|
334
|
+
s.art_container.add_trace(tr)
|
|
335
|
+
|
|
336
|
+
elif isinstance(change, EditLogTrace):
|
|
337
|
+
patch: dict[str, object] = {}
|
|
338
|
+
for f in change.model_fields_set:
|
|
339
|
+
if f == "art_id":
|
|
340
|
+
continue
|
|
341
|
+
patch[f] = getattr(change, f)
|
|
342
|
+
s.art_container.edit_trace(change.art_id, patch)
|
|
343
|
+
|
|
344
|
+
elif isinstance(change, RemoveTraceArtifact):
|
|
345
|
+
s.art_container.rem_trace(change.art_id)
|
|
346
|
+
|
|
347
|
+
# --- Data artifact changes (DocRef / SrcCodeRef) -----------------------------
|
|
348
|
+
|
|
349
|
+
elif isinstance(change, AddDocRef):
|
|
350
|
+
# Build DocRef and add via container API
|
|
351
|
+
s.art_container.add_data_art(
|
|
352
|
+
DocRef(
|
|
353
|
+
art_id=change.art_id,
|
|
354
|
+
text=change.text,
|
|
355
|
+
meta=change.meta,
|
|
356
|
+
)
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
elif isinstance(change, EditDocRef):
|
|
360
|
+
# Patch-edit via container API (only update provided fields)
|
|
361
|
+
patch: dict[str, object] = {}
|
|
362
|
+
if change.text is not None:
|
|
363
|
+
patch["text"] = change.text
|
|
364
|
+
if change.meta is not None:
|
|
365
|
+
patch["meta"] = change.meta
|
|
366
|
+
|
|
367
|
+
if patch:
|
|
368
|
+
s.art_container.edit_data_art(change.art_id, patch)
|
|
369
|
+
|
|
370
|
+
elif isinstance(change, AddSrcCodeRef):
|
|
371
|
+
# Build SrcCodeRef and add via container API
|
|
372
|
+
s.art_container.add_data_art(
|
|
373
|
+
SrcCodeRef(
|
|
374
|
+
art_id=change.art_id,
|
|
375
|
+
src_code=change.src_code,
|
|
376
|
+
language=change.language,
|
|
377
|
+
file_path=change.file_path,
|
|
378
|
+
iml_code=change.iml_code,
|
|
379
|
+
meta=change.meta,
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
elif isinstance(change, EditSrcCodeRef):
|
|
384
|
+
# Patch-edit via container API (only update provided fields)
|
|
385
|
+
patch: dict[str, object] = {}
|
|
386
|
+
if change.src_code is not None:
|
|
387
|
+
patch["src_code"] = change.src_code
|
|
388
|
+
if change.language is not None:
|
|
389
|
+
patch["language"] = change.language
|
|
390
|
+
if change.file_path is not None:
|
|
391
|
+
patch["file_path"] = change.file_path
|
|
392
|
+
if change.iml_code is not None:
|
|
393
|
+
patch["iml_code"] = change.iml_code
|
|
394
|
+
if change.meta is not None:
|
|
395
|
+
patch["meta"] = change.meta
|
|
396
|
+
|
|
397
|
+
if patch:
|
|
398
|
+
s.art_container.edit_data_art(change.art_id, patch)
|
|
399
|
+
|
|
400
|
+
elif isinstance(change, RemoveDataArtifact):
|
|
401
|
+
s.art_container.rem_data_art(change.art_id)
|
|
402
|
+
|
|
403
|
+
elif isinstance (change, LinkArtifactsComponents):
|
|
404
|
+
s.art_map.add_connection(art_id=change.art_id, comp_name=change.comp_name)
|
|
405
|
+
|
|
406
|
+
elif isinstance (change, RemoveArtComponentLink):
|
|
407
|
+
s.art_map.rem_connection(artifact_id=change.art_id, comp_name=change.comp_name)
|
|
408
|
+
|
|
409
|
+
else:
|
|
410
|
+
raise Exception(f"Unrecognized state change: {type(change).__name__}")
|
|
411
|
+
|
|
412
|
+
return s
|
|
413
|
+
|
|
414
|
+
def to_json(self):
|
|
415
|
+
""" """
|
|
416
|
+
return self.model_dump_json()
|
|
417
|
+
|
|
418
|
+
@staticmethod
|
|
419
|
+
def from_json(j : str):
|
|
420
|
+
"""
|
|
421
|
+
Load in the State value
|
|
422
|
+
"""
|
|
423
|
+
return State.model_validate_json(j)
|
|
424
|
+
|
|
425
|
+
def save(self, dirpath : str | None | Path = None):
|
|
426
|
+
"""
|
|
427
|
+
Save State to the specified directory
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
if dirpath is None:
|
|
431
|
+
if self.file_location is None:
|
|
432
|
+
dirpath = os.getcwd()
|
|
433
|
+
filepath = os.path.join(dirpath, 'sl_state.json')
|
|
434
|
+
console.print(f"Saving the state in CWD: {dirpath}")
|
|
435
|
+
else:
|
|
436
|
+
filepath = self.file_location
|
|
437
|
+
else:
|
|
438
|
+
filepath = os.path.join(dirpath, 'sl_state.json')
|
|
439
|
+
|
|
440
|
+
with open(filepath, 'w') as outfile:
|
|
441
|
+
print(self.to_json(), file=outfile)
|
|
442
|
+
|
|
443
|
+
def save_to_path (self, path : Path) -> None:
|
|
444
|
+
""" Save State to a specific filepath """
|
|
445
|
+
try:
|
|
446
|
+
with open(path, 'w') as outfile:
|
|
447
|
+
print(self.to_json(), file=outfile)
|
|
448
|
+
except Exception as e:
|
|
449
|
+
raise ValueError(f"Failed to write state to disk [path = {path}]: {e}")
|
|
450
|
+
|
|
451
|
+
@staticmethod
|
|
452
|
+
def from_dir (dirpath : str | Path):
|
|
453
|
+
"""
|
|
454
|
+
Load State from specified directory
|
|
455
|
+
"""
|
|
456
|
+
try:
|
|
457
|
+
state_path = os.path.join(dirpath, "sl_state.json")
|
|
458
|
+
data = Path(state_path).read_text()
|
|
459
|
+
state = State.from_json(data)
|
|
460
|
+
state.file_location = state_path
|
|
461
|
+
except Exception as e:
|
|
462
|
+
console.print(f"Encountered error: {e}")
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
return state
|
|
@@ -0,0 +1,133 @@
|
|
|
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
|
|
File without changes
|