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,574 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/state/inst.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime, timezone
10
+ from typing import Optional
11
+
12
+ from pydantic import BaseModel, Field
13
+
14
+ from .change import StateChange
15
+ from .diff import (
16
+ StateDiff,
17
+ ValueDiff,
18
+ base_status_comp,
19
+ bool_true_is_good,
20
+ numeric_increase_bad,
21
+ numeric_increase_good,
22
+ )
23
+ from .recommendation import Recommendation
24
+ from ..data.container import ArtifactContainer
25
+ from ..data.mapping import ArtifactMap
26
+ from ..modeling.domain import BaseStatus
27
+ from ..modeling.spec import Spec
28
+ from ..utils import JSONObject
29
+
30
+ def _safe_iso(dt: Optional[datetime]) -> str | None:
31
+ if dt is None:
32
+ return None
33
+ return dt.isoformat()
34
+
35
+
36
+ def _comp_regions(comp: object | None) -> list[object]:
37
+ """
38
+ Best-effort extraction of complement regions.
39
+
40
+ Supports a couple of plausible layouts:
41
+ - comp.regions
42
+ - comp.cover_regions
43
+ """
44
+ if comp is None:
45
+ return []
46
+ regs = getattr(comp, "regions", None)
47
+ if regs is None:
48
+ regs = getattr(comp, "cover_regions", None)
49
+ try:
50
+ return list(regs or [])
51
+ except Exception:
52
+ return []
53
+
54
+
55
+ class StateInstance(BaseModel):
56
+ """
57
+ State Instance class
58
+ """
59
+
60
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
61
+
62
+ spec: Spec = Field(default_factory=Spec) # `Spec` is the domain model + scenarios
63
+ art_container: ArtifactContainer = Field(default_factory=ArtifactContainer)
64
+ art_map: ArtifactMap = Field(default_factory=ArtifactMap)
65
+
66
+ # These describe the changes from the previous state
67
+ changes: list[StateChange] = Field(default_factory=list[StateChange])
68
+ state_diff: StateDiff | None = None
69
+
70
+ # The generated recommendations
71
+ recommendations: list[Recommendation] = Field(default_factory=list[Recommendation])
72
+
73
+ def update_created_at(self) -> None:
74
+ """Update the `created_at` timestamp field"""
75
+ self.created_at = datetime.now(timezone.utc)
76
+
77
+ def json_summary(self) -> JSONObject:
78
+ """
79
+ JSON snapshot of the *inputs* used by calc_diff() to compute StateDiff.
80
+
81
+ Goal: stable, machine-readable summary suitable for --json output and tests.
82
+ """
83
+ dm = self.spec.domain_model
84
+ spec = self.spec
85
+ art = self.art_container
86
+
87
+ comp = getattr(spec, "scenario_comp", None)
88
+ regions = _comp_regions(comp)
89
+
90
+ return {
91
+ "created_at": _safe_iso(self.created_at),
92
+ "changes": [c.model_dump(mode="json") for c in (self.changes or [])],
93
+ # ---- Domain model (base) ----
94
+ "base_status": getattr(dm, "base_status", None),
95
+ "base_has_state": getattr(dm, "base_has_state", None),
96
+ "base_has_action": getattr(dm, "base_has_action", None),
97
+ # ---- Domain model (state predicates) ----
98
+ "num_state_preds_total": getattr(dm, "num_state_preds_total", None),
99
+ "num_state_preds_matched": getattr(dm, "num_state_preds_matched", None),
100
+ "num_state_preds_valid_logic": getattr(dm, "num_state_preds_valid_logic", None),
101
+ "num_state_preds_errored": getattr(dm, "num_state_preds_errored", None),
102
+ # ---- Domain model (action predicates) ----
103
+ "num_action_preds_total": getattr(dm, "num_action_preds_total", None),
104
+ "num_action_preds_matched": getattr(dm, "num_action_preds_matched", None),
105
+ "num_action_preds_valid_logic": getattr(dm, "num_action_preds_valid_logic", None),
106
+ "num_action_preds_errored": getattr(dm, "num_action_preds_errored", None),
107
+ # ---- Domain model (predicates totals) ----
108
+ "num_preds_total": getattr(dm, "num_preds_total", None),
109
+ "num_preds_matched": getattr(dm, "num_preds_matched", None),
110
+ "num_preds_valid_logic": getattr(dm, "num_preds_valid_logic", None),
111
+ "num_preds_errored": getattr(dm, "num_preds_errored", None),
112
+ # ---- Domain model (transitions) ----
113
+ "num_trans_total": getattr(dm, "num_trans_total", None),
114
+ "num_trans_matched": getattr(dm, "num_trans_matched", None),
115
+ "num_trans_valid_logic": getattr(dm, "num_trans_valid_logic", None),
116
+ "num_trans_errored": getattr(dm, "num_trans_errored", None),
117
+ # ---- Scenarios (Spec-level) ----
118
+ "num_sc_total": getattr(spec, "num_sc_total", None),
119
+ "num_sc_missing": getattr(spec, "num_sc_missing", None),
120
+ "num_sc_matched": getattr(spec, "num_sc_matched", None),
121
+ "num_sc_inconsistent": getattr(spec, "num_sc_inconsistent", None),
122
+ # Conflicts (total + by kind).
123
+ "num_sc_conflicted": getattr(spec, "num_sc_conflicted", None),
124
+ "num_sc_overlap": getattr(spec, "num_sc_overlap", 0),
125
+ "num_sc_consumed": getattr(spec, "num_sc_consumed", 0),
126
+ # ---- Complement (Spec-level) ----
127
+ # NOTE: match StateDiff field names (new ones).
128
+ "scenario_comp_present": comp is not None,
129
+ "num_comp_regions_total": len(regions),
130
+ # ---- ArtifactContainer: test traces ----
131
+ "num_test_traces_total": getattr(art, "num_test_traces_total", None),
132
+ "num_test_traces_logic_good": getattr(art, "num_test_traces_logic_good", None),
133
+ "num_test_traces_matched": getattr(art, "num_test_traces_matched", None),
134
+ # ---- ArtifactContainer: log traces ----
135
+ "num_log_traces_total": getattr(art, "num_log_traces_total", None),
136
+ "num_log_traces_logic_good": getattr(art, "num_log_traces_logic_good", None),
137
+ "num_log_traces_matched": getattr(art, "num_log_traces_matched", None),
138
+ }
139
+
140
+
141
+ class InstancesList(BaseModel):
142
+ """A helper container for pretty printing a list of instances"""
143
+
144
+ states: list[StateInstance]
145
+
146
+
147
+ def calc_diff(new: StateInstance, old: StateInstance) -> StateDiff:
148
+ """
149
+ Generate a diff between two StateInstances.
150
+
151
+ Assumes `old` and `new` have fully computed stats on:
152
+ - new.spec.domain_model
153
+ - new.spec (scenario stats)
154
+ - new.art_container
155
+ - new.spec.scenario_comp (optional)
156
+ """
157
+ vals: list[tuple[str, ValueDiff]] = []
158
+
159
+ dm_old = old.spec.domain_model
160
+ dm_new = new.spec.domain_model
161
+ spec_old = old.spec
162
+ spec_new = new.spec
163
+ art_old = old.art_container
164
+ art_new = new.art_container
165
+
166
+ # -------------------------------------------------------------------------
167
+ # Helper: only emit diffs for fields that actually exist on StateDiff.
168
+ # This makes calc_diff resilient while you evolve StateDiff.
169
+ # -------------------------------------------------------------------------
170
+ state_diff_fields = getattr(StateDiff, "model_fields", {}) # pydantic v2
171
+
172
+ def _emit(key: str, vd: ValueDiff) -> None:
173
+ if key in state_diff_fields:
174
+ vals.append((key, vd))
175
+
176
+ # >>> Base
177
+ _emit(
178
+ "base_status",
179
+ ValueDiff[BaseStatus](
180
+ label="Base code IML status",
181
+ before=dm_old.base_status,
182
+ after=dm_new.base_status,
183
+ comp_func=base_status_comp,
184
+ hint={},
185
+ ),
186
+ )
187
+
188
+ _emit(
189
+ "base_has_state",
190
+ ValueDiff[bool](
191
+ label="State type is present",
192
+ before=dm_old.base_has_state,
193
+ after=dm_new.base_has_state,
194
+ comp_func=bool_true_is_good,
195
+ hint={},
196
+ ),
197
+ )
198
+
199
+ _emit(
200
+ "base_has_action",
201
+ ValueDiff[bool](
202
+ label="Action type is present",
203
+ before=dm_old.base_has_action,
204
+ after=dm_new.base_has_action,
205
+ comp_func=bool_true_is_good,
206
+ hint={},
207
+ ),
208
+ )
209
+
210
+ # >>> State predicates
211
+ _emit(
212
+ "num_state_preds_total",
213
+ ValueDiff[int](
214
+ label="Number of state predicates",
215
+ before=dm_old.num_state_preds_total,
216
+ after=dm_new.num_state_preds_total,
217
+ comp_func=numeric_increase_good,
218
+ hint={},
219
+ ),
220
+ )
221
+
222
+ _emit(
223
+ "num_state_preds_matched",
224
+ ValueDiff[int](
225
+ label="Number of matched state predicates",
226
+ before=dm_old.num_state_preds_matched,
227
+ after=dm_new.num_state_preds_matched,
228
+ comp_func=numeric_increase_good,
229
+ hint={},
230
+ ),
231
+ )
232
+
233
+ _emit(
234
+ "num_state_preds_valid_logic",
235
+ ValueDiff[int](
236
+ label="Number of state predicates with valid logic",
237
+ before=dm_old.num_state_preds_valid_logic,
238
+ after=dm_new.num_state_preds_valid_logic,
239
+ comp_func=numeric_increase_good,
240
+ hint={},
241
+ ),
242
+ )
243
+
244
+ _emit(
245
+ "num_state_preds_errored",
246
+ ValueDiff[int](
247
+ label="Number of state predicates with errors",
248
+ before=dm_old.num_state_preds_errored,
249
+ after=dm_new.num_state_preds_errored,
250
+ comp_func=numeric_increase_bad,
251
+ hint={},
252
+ ),
253
+ )
254
+
255
+ # >>> Action predicates
256
+ _emit(
257
+ "num_action_preds_total",
258
+ ValueDiff[int](
259
+ label="Number of action predicates",
260
+ before=dm_old.num_action_preds_total,
261
+ after=dm_new.num_action_preds_total,
262
+ comp_func=numeric_increase_good,
263
+ hint={},
264
+ ),
265
+ )
266
+
267
+ _emit(
268
+ "num_action_preds_matched",
269
+ ValueDiff[int](
270
+ label="Number of matched action predicates",
271
+ before=dm_old.num_action_preds_matched,
272
+ after=dm_new.num_action_preds_matched,
273
+ comp_func=numeric_increase_good,
274
+ hint={},
275
+ ),
276
+ )
277
+
278
+ _emit(
279
+ "num_action_preds_valid_logic",
280
+ ValueDiff[int](
281
+ label="Number of action predicates with valid logic",
282
+ before=dm_old.num_action_preds_valid_logic,
283
+ after=dm_new.num_action_preds_valid_logic,
284
+ comp_func=numeric_increase_good,
285
+ hint={},
286
+ ),
287
+ )
288
+
289
+ _emit(
290
+ "num_action_preds_errored",
291
+ ValueDiff[int](
292
+ label="Number of action predicates with errors",
293
+ before=dm_old.num_action_preds_errored,
294
+ after=dm_new.num_action_preds_errored,
295
+ comp_func=numeric_increase_bad,
296
+ hint={},
297
+ ),
298
+ )
299
+
300
+ # >>> Predicates (totals)
301
+ _emit(
302
+ "num_preds_total",
303
+ ValueDiff[int](
304
+ label="Number of predicates (total)",
305
+ before=dm_old.num_preds_total,
306
+ after=dm_new.num_preds_total,
307
+ comp_func=numeric_increase_good,
308
+ hint={},
309
+ ),
310
+ )
311
+
312
+ _emit(
313
+ "num_preds_matched",
314
+ ValueDiff[int](
315
+ label="Number of matched predicates (total)",
316
+ before=dm_old.num_preds_matched,
317
+ after=dm_new.num_preds_matched,
318
+ comp_func=numeric_increase_good,
319
+ hint={},
320
+ ),
321
+ )
322
+
323
+ _emit(
324
+ "num_preds_valid_logic",
325
+ ValueDiff[int](
326
+ label="Number of predicates with valid logic (total)",
327
+ before=dm_old.num_preds_valid_logic,
328
+ after=dm_new.num_preds_valid_logic,
329
+ comp_func=numeric_increase_good,
330
+ hint={},
331
+ ),
332
+ )
333
+
334
+ _emit(
335
+ "num_preds_errored",
336
+ ValueDiff[int](
337
+ label="Number of predicates with errors (total)",
338
+ before=dm_old.num_preds_errored,
339
+ after=dm_new.num_preds_errored,
340
+ comp_func=numeric_increase_bad,
341
+ hint={},
342
+ ),
343
+ )
344
+
345
+ # >>> Transitions
346
+ _emit(
347
+ "num_trans_total",
348
+ ValueDiff[int](
349
+ label="Number of transitions",
350
+ before=dm_old.num_trans_total,
351
+ after=dm_new.num_trans_total,
352
+ comp_func=numeric_increase_good,
353
+ hint={},
354
+ ),
355
+ )
356
+
357
+ _emit(
358
+ "num_trans_matched",
359
+ ValueDiff[int](
360
+ label="Number of matched transitions",
361
+ before=dm_old.num_trans_matched,
362
+ after=dm_new.num_trans_matched,
363
+ comp_func=numeric_increase_good,
364
+ hint={},
365
+ ),
366
+ )
367
+
368
+ _emit(
369
+ "num_trans_valid_logic",
370
+ ValueDiff[int](
371
+ label="Number of transitions with valid logic",
372
+ before=dm_old.num_trans_valid_logic,
373
+ after=dm_new.num_trans_valid_logic,
374
+ comp_func=numeric_increase_good,
375
+ hint={},
376
+ ),
377
+ )
378
+
379
+ _emit(
380
+ "num_trans_errored",
381
+ ValueDiff[int](
382
+ label="Number of transitions with errors",
383
+ before=dm_old.num_trans_errored,
384
+ after=dm_new.num_trans_errored,
385
+ comp_func=numeric_increase_bad,
386
+ hint={},
387
+ ),
388
+ )
389
+
390
+ # >>> Scenarios
391
+ _emit(
392
+ "num_sc_total",
393
+ ValueDiff[int](
394
+ label="Number of scenarios",
395
+ before=spec_old.num_sc_total,
396
+ after=spec_new.num_sc_total,
397
+ comp_func=numeric_increase_good,
398
+ hint={},
399
+ ),
400
+ )
401
+
402
+ _emit(
403
+ "num_sc_missing",
404
+ ValueDiff[int](
405
+ label="Number of scenarios with missing model components",
406
+ before=spec_old.num_sc_missing,
407
+ after=spec_new.num_sc_missing,
408
+ comp_func=numeric_increase_bad,
409
+ hint={},
410
+ ),
411
+ )
412
+
413
+ _emit(
414
+ "num_sc_matched",
415
+ ValueDiff[int](
416
+ label="Number of scenarios matched",
417
+ before=spec_old.num_sc_matched,
418
+ after=spec_new.num_sc_matched,
419
+ comp_func=numeric_increase_good,
420
+ hint={},
421
+ ),
422
+ )
423
+
424
+ _emit(
425
+ "num_sc_inconsistent",
426
+ ValueDiff[int](
427
+ label="Number of inconsistent scenarios",
428
+ before=spec_old.num_sc_inconsistent,
429
+ after=spec_new.num_sc_inconsistent,
430
+ comp_func=numeric_increase_bad,
431
+ hint={},
432
+ ),
433
+ )
434
+
435
+ # >>> Conflicts
436
+ _emit(
437
+ "num_sc_conflicted",
438
+ ValueDiff[int](
439
+ label="Number of scenario conflicts (total)",
440
+ before=spec_old.num_sc_conflicted,
441
+ after=spec_new.num_sc_conflicted,
442
+ comp_func=numeric_increase_bad,
443
+ hint={},
444
+ ),
445
+ )
446
+
447
+ _emit(
448
+ "num_sc_overlap",
449
+ ValueDiff[int](
450
+ label="Number of scenario overlap conflicts",
451
+ before=getattr(spec_old, "num_sc_overlap", 0),
452
+ after=getattr(spec_new, "num_sc_overlap", 0),
453
+ comp_func=numeric_increase_bad,
454
+ hint={},
455
+ ),
456
+ )
457
+
458
+ _emit(
459
+ "num_sc_consumed",
460
+ ValueDiff[int](
461
+ label="Number of scenario consumed conflicts",
462
+ before=getattr(spec_old, "num_sc_consumed", 0),
463
+ after=getattr(spec_new, "num_sc_consumed", 0),
464
+ comp_func=numeric_increase_bad,
465
+ hint={},
466
+ ),
467
+ )
468
+
469
+ # -------------------------------------------------------------------------
470
+ # >>> Complement (Spec-level): scenario_comp
471
+ # NOTE: match the new StateDiff field names you mentioned:
472
+ # - scenario_comp_present
473
+ # - num_comp_regions_total
474
+ # -------------------------------------------------------------------------
475
+ comp_old = getattr(spec_old, "scenario_comp", None)
476
+ comp_new = getattr(spec_new, "scenario_comp", None)
477
+
478
+ old_regions = _comp_regions(comp_old)
479
+ new_regions = _comp_regions(comp_new)
480
+
481
+ # Presence: having a computed complement is generally "good" (enables coverage reasoning).
482
+ _emit(
483
+ "scenario_comp_present",
484
+ ValueDiff[bool](
485
+ label="Scenario complement is present",
486
+ before=(comp_old is not None),
487
+ after=(comp_new is not None),
488
+ comp_func=bool_true_is_good,
489
+ hint={},
490
+ ),
491
+ )
492
+
493
+ # Region count: smaller complement is better (more coverage), so increases are "bad".
494
+ _emit(
495
+ "num_comp_regions_total",
496
+ ValueDiff[int](
497
+ label="Complement region count",
498
+ before=len(old_regions),
499
+ after=len(new_regions),
500
+ comp_func=numeric_increase_bad,
501
+ hint={},
502
+ ),
503
+ )
504
+
505
+ # -------------------------------------------------------------------------
506
+ # >>> Traces
507
+ # -------------------------------------------------------------------------
508
+ _emit(
509
+ "num_test_traces_total",
510
+ ValueDiff[int](
511
+ label="Number of test traces",
512
+ before=art_old.num_test_traces_total,
513
+ after=art_new.num_test_traces_total,
514
+ comp_func=numeric_increase_good,
515
+ hint={},
516
+ ),
517
+ )
518
+
519
+ _emit(
520
+ "num_test_traces_logic_good",
521
+ ValueDiff[int](
522
+ label="Number of test traces logically valid",
523
+ before=art_old.num_test_traces_logic_good,
524
+ after=art_new.num_test_traces_logic_good,
525
+ comp_func=numeric_increase_good,
526
+ hint={},
527
+ ),
528
+ )
529
+
530
+ _emit(
531
+ "num_test_traces_matched",
532
+ ValueDiff[int](
533
+ label="Number of test traces matched",
534
+ before=art_old.num_test_traces_matched,
535
+ after=art_new.num_test_traces_matched,
536
+ comp_func=numeric_increase_good,
537
+ hint={},
538
+ ),
539
+ )
540
+
541
+ _emit(
542
+ "num_log_traces_total",
543
+ ValueDiff[int](
544
+ label="Number of log traces",
545
+ before=art_old.num_log_traces_total,
546
+ after=art_new.num_log_traces_total,
547
+ comp_func=numeric_increase_good,
548
+ hint={},
549
+ ),
550
+ )
551
+
552
+ _emit(
553
+ "num_log_traces_logic_good",
554
+ ValueDiff[int](
555
+ label="Number of log traces logically valid",
556
+ before=art_old.num_log_traces_logic_good,
557
+ after=art_new.num_log_traces_logic_good,
558
+ comp_func=numeric_increase_good,
559
+ hint={},
560
+ ),
561
+ )
562
+
563
+ _emit(
564
+ "num_log_traces_matched",
565
+ ValueDiff[int](
566
+ label="Number of log traces matched",
567
+ before=art_old.num_log_traces_matched,
568
+ after=art_new.num_log_traces_matched,
569
+ comp_func=numeric_increase_good,
570
+ hint={},
571
+ ),
572
+ )
573
+
574
+ return StateDiff(**dict(vals))
@@ -0,0 +1,13 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/state/recommendation.py
5
+ #
6
+
7
+ from pydantic import BaseModel
8
+ from typing import Literal
9
+
10
+ class Recommendation(BaseModel):
11
+ text: str
12
+ kind: Literal["error", "warning", "next", "info"] = "next"
13
+ priority: int = 50 # lower = higher priority