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,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