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,577 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Imandra Inc.
|
|
3
|
+
#
|
|
4
|
+
# speclogician/state/recommender.py
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Iterable, TypeAlias, Callable, Optional
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from .diff import ComparisonOutcome, ValueDiff, StateDiff
|
|
14
|
+
from .inst import StateInstance
|
|
15
|
+
from .recommendation import Recommendation
|
|
16
|
+
|
|
17
|
+
class ChangeFailure(BaseModel):
|
|
18
|
+
"""
|
|
19
|
+
Lightweight failure context for recommendation generation.
|
|
20
|
+
This replaces dependency on ProcessChangeResult.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
change: str # e.g. "AddPredicate", "EditScenario"
|
|
24
|
+
stage: str # e.g. "apply_change", "analyze_change"
|
|
25
|
+
error_type: str # e.g. "TypeError"
|
|
26
|
+
error: str # str(exception)
|
|
27
|
+
|
|
28
|
+
# Optional extras if you want later
|
|
29
|
+
message: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
Rule: TypeAlias = Callable[[StateInstance, StateInstance, Optional[ChangeFailure]], Iterable[Recommendation]]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Recommender:
|
|
36
|
+
"""
|
|
37
|
+
Turn (old_state, new_state) into human-readable next steps.
|
|
38
|
+
|
|
39
|
+
Typical use:
|
|
40
|
+
recs = Recommender().recommend(old_inst, new_inst)
|
|
41
|
+
# feed "\n".join(r.text for r in recs) into your LLM CLI loop
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, rules: list[Rule] | None = None) -> None:
|
|
45
|
+
self._rules: list[Rule] = rules or [
|
|
46
|
+
_rule_error_triage,
|
|
47
|
+
_rule_state_diff_summary,
|
|
48
|
+
_rule_missing_components,
|
|
49
|
+
_rule_inconsistent_scenarios,
|
|
50
|
+
_rule_conflicting_scenarios,
|
|
51
|
+
_rule_unmatched_traces,
|
|
52
|
+
_rule_invalid_iml_components,
|
|
53
|
+
_rule_traceability_nudge,
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
def recommend(
|
|
57
|
+
self,
|
|
58
|
+
old_inst: StateInstance,
|
|
59
|
+
new_inst: StateInstance,
|
|
60
|
+
*,
|
|
61
|
+
failure: ChangeFailure | None = None,
|
|
62
|
+
) -> list[Recommendation]:
|
|
63
|
+
out: list[Recommendation] = []
|
|
64
|
+
|
|
65
|
+
for rule in self._rules:
|
|
66
|
+
try:
|
|
67
|
+
out.extend(list(rule(old_inst, new_inst, failure)))
|
|
68
|
+
except Exception as e:
|
|
69
|
+
# recommender should never take down the CLI
|
|
70
|
+
out.append(
|
|
71
|
+
Recommendation(
|
|
72
|
+
text=f"Recommender rule failed ({getattr(rule, '__name__', type(rule).__name__)}): {e}",
|
|
73
|
+
kind="warning",
|
|
74
|
+
priority=5,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Deduplicate by `text` while preserving order.
|
|
79
|
+
# If the same text appears multiple times, keep the one with the lowest priority number.
|
|
80
|
+
chosen: dict[str, Recommendation] = {}
|
|
81
|
+
order: list[str] = []
|
|
82
|
+
|
|
83
|
+
for r in out:
|
|
84
|
+
if r.text not in chosen:
|
|
85
|
+
chosen[r.text] = r
|
|
86
|
+
order.append(r.text)
|
|
87
|
+
else:
|
|
88
|
+
prev = chosen[r.text]
|
|
89
|
+
if r.priority < prev.priority:
|
|
90
|
+
chosen[r.text] = r
|
|
91
|
+
|
|
92
|
+
return [chosen[t] for t in order]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ----------------------------
|
|
96
|
+
# Helpers
|
|
97
|
+
# ----------------------------
|
|
98
|
+
|
|
99
|
+
def _ok(failure: ChangeFailure | None) -> bool:
|
|
100
|
+
"""
|
|
101
|
+
Mirror old `res["ok"]` meaning:
|
|
102
|
+
- failure is None => ok
|
|
103
|
+
- failure provided => failed
|
|
104
|
+
"""
|
|
105
|
+
return failure is None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _outcome(v: ValueDiff[object]) -> ComparisonOutcome:
|
|
109
|
+
"""
|
|
110
|
+
Use the comp_func if present, otherwise fallback to raw equality.
|
|
111
|
+
"""
|
|
112
|
+
if v.comp_func is not None: # type: ignore[truthy-function]
|
|
113
|
+
return v.comp_func(v.before, v.after) # type: ignore[misc]
|
|
114
|
+
return ComparisonOutcome.NO_CHANGE if v.before == v.after else ComparisonOutcome.UNKNOWN
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _rec(
|
|
118
|
+
text: str,
|
|
119
|
+
*,
|
|
120
|
+
kind: str = "next",
|
|
121
|
+
priority: int = 50,
|
|
122
|
+
) -> Recommendation:
|
|
123
|
+
return Recommendation(text=text, kind=kind, priority=priority) # type: ignore[arg-type]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ----------------------------
|
|
127
|
+
# Rules (keep them small + boring)
|
|
128
|
+
# ----------------------------
|
|
129
|
+
|
|
130
|
+
def _rule_error_triage(
|
|
131
|
+
old_inst: StateInstance,
|
|
132
|
+
new_inst: StateInstance,
|
|
133
|
+
failure: ChangeFailure | None,
|
|
134
|
+
) -> Iterable[Recommendation]:
|
|
135
|
+
if _ok(failure):
|
|
136
|
+
return []
|
|
137
|
+
|
|
138
|
+
# If you have the actual StateChange object at the call site,
|
|
139
|
+
# you can set failure.change = ch.__class__.__name__.
|
|
140
|
+
return [
|
|
141
|
+
Recommendation(
|
|
142
|
+
kind="warning",
|
|
143
|
+
priority=0,
|
|
144
|
+
text=(
|
|
145
|
+
f"The last change failed during **{failure.stage}** for `{failure.change}`.\n"
|
|
146
|
+
f"Error: `{failure.error_type}: {failure.error}`\n"
|
|
147
|
+
"Recommendation: fix this error first (no later changes will be reliable)."
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _rule_missing_components(
|
|
154
|
+
old_inst: StateInstance,
|
|
155
|
+
new_inst: StateInstance,
|
|
156
|
+
failure: ChangeFailure | None,
|
|
157
|
+
) -> Iterable[Recommendation]:
|
|
158
|
+
if not _ok(failure):
|
|
159
|
+
return []
|
|
160
|
+
|
|
161
|
+
spec = new_inst.spec
|
|
162
|
+
missing = [
|
|
163
|
+
sc
|
|
164
|
+
for sc in spec.scenarios
|
|
165
|
+
if getattr(sc, "preds_missing", []) or getattr(sc, "trans_missing", [])
|
|
166
|
+
]
|
|
167
|
+
if not missing:
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
examples = missing[:3]
|
|
171
|
+
bits: list[str] = []
|
|
172
|
+
for sc in examples:
|
|
173
|
+
pm = getattr(sc, "preds_missing", [])
|
|
174
|
+
tm = getattr(sc, "trans_missing", [])
|
|
175
|
+
bits.append(f"- `{sc.name}` missing preds={pm or '—'} trans={tm or '—'}")
|
|
176
|
+
|
|
177
|
+
return [
|
|
178
|
+
Recommendation(
|
|
179
|
+
text=(
|
|
180
|
+
"Some scenarios reference predicates/transitions that do not exist yet.\n"
|
|
181
|
+
+ "\n".join(bits)
|
|
182
|
+
+ ("\n(…more)" if len(missing) > len(examples) else "")
|
|
183
|
+
+ "\nRecommendation: add the missing predicates/transitions or edit the scenarios to match the domain model."
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _rule_inconsistent_scenarios(
|
|
190
|
+
old_inst: StateInstance,
|
|
191
|
+
new_inst: StateInstance,
|
|
192
|
+
failure: ChangeFailure | None,
|
|
193
|
+
) -> Iterable[Recommendation]:
|
|
194
|
+
if not _ok(failure):
|
|
195
|
+
return []
|
|
196
|
+
|
|
197
|
+
spec = new_inst.spec
|
|
198
|
+
bad = [sc for sc in spec.scenarios if getattr(sc, "all_preds_consistent", True) is False]
|
|
199
|
+
if not bad:
|
|
200
|
+
return []
|
|
201
|
+
|
|
202
|
+
names = ", ".join(f"`{sc.name}`" for sc in bad[:5])
|
|
203
|
+
tail = " …" if len(bad) > 5 else ""
|
|
204
|
+
return [
|
|
205
|
+
Recommendation(
|
|
206
|
+
text=(
|
|
207
|
+
f"Some scenarios appear **inconsistent** (unsat) given the current predicates: {names}{tail}.\n"
|
|
208
|
+
"Recommendation: relax contradictory predicates, or split the scenario into smaller cases."
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _rule_conflicting_scenarios(
|
|
215
|
+
old_inst: StateInstance,
|
|
216
|
+
new_inst: StateInstance,
|
|
217
|
+
failure: ChangeFailure | None,
|
|
218
|
+
) -> Iterable[Recommendation]:
|
|
219
|
+
if not _ok(failure):
|
|
220
|
+
return []
|
|
221
|
+
|
|
222
|
+
spec = new_inst.spec
|
|
223
|
+
conflicted = [sc for sc in spec.scenarios if getattr(sc, "conflicts", [])]
|
|
224
|
+
if not conflicted:
|
|
225
|
+
return []
|
|
226
|
+
|
|
227
|
+
names = ", ".join(f"`{sc.name}`" for sc in conflicted[:5])
|
|
228
|
+
tail = " …" if len(conflicted) > 5 else ""
|
|
229
|
+
return [
|
|
230
|
+
Recommendation(
|
|
231
|
+
text=(
|
|
232
|
+
f"Some scenarios have **overlaps/conflicts** with others: {names}{tail}.\n"
|
|
233
|
+
"Recommendation: add distinguishing predicates (boundary conditions) so scenarios become disjoint, "
|
|
234
|
+
"or explicitly document intended priority/ordering."
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _rule_unmatched_traces(
|
|
241
|
+
old_inst: StateInstance,
|
|
242
|
+
new_inst: StateInstance,
|
|
243
|
+
failure: ChangeFailure | None,
|
|
244
|
+
) -> Iterable[Recommendation]:
|
|
245
|
+
if not _ok(failure):
|
|
246
|
+
return []
|
|
247
|
+
|
|
248
|
+
cont = new_inst.art_container
|
|
249
|
+
amap = new_inst.art_map
|
|
250
|
+
|
|
251
|
+
if amap is None:
|
|
252
|
+
return []
|
|
253
|
+
|
|
254
|
+
def _is_trace_matched(trace) -> bool:
|
|
255
|
+
tid = getattr(trace, "art_id", None)
|
|
256
|
+
if not tid:
|
|
257
|
+
return False
|
|
258
|
+
return bool(amap.get_comp_for_artifact(tid))
|
|
259
|
+
|
|
260
|
+
all_traces = list(cont.test_traces) + list(cont.log_traces)
|
|
261
|
+
if not all_traces:
|
|
262
|
+
return []
|
|
263
|
+
|
|
264
|
+
unmatched = [t for t in all_traces if not _is_trace_matched(t)]
|
|
265
|
+
if not unmatched:
|
|
266
|
+
return []
|
|
267
|
+
|
|
268
|
+
def _label(t) -> str:
|
|
269
|
+
return (
|
|
270
|
+
getattr(t, "name", None)
|
|
271
|
+
or getattr(t, "filename", None)
|
|
272
|
+
or (getattr(t, "art_id", None) or "<unknown>")
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
examples = unmatched[:3]
|
|
276
|
+
bullets = "\n".join(f"- `{_label(t)}`" for t in examples)
|
|
277
|
+
return [
|
|
278
|
+
Recommendation(
|
|
279
|
+
text=(
|
|
280
|
+
"Some traces are not matched to any scenario.\n"
|
|
281
|
+
f"{bullets}"
|
|
282
|
+
+ ("\n(…more)" if len(unmatched) > len(examples) else "")
|
|
283
|
+
+ "\nRecommendation: add/adjust scenarios so each important trace is explained "
|
|
284
|
+
"(or explicitly link traces to components if you support manual traceability)."
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _rule_invalid_iml_components(
|
|
291
|
+
old_inst: StateInstance,
|
|
292
|
+
new_inst: StateInstance,
|
|
293
|
+
failure: ChangeFailure | None,
|
|
294
|
+
) -> Iterable[Recommendation]:
|
|
295
|
+
if not _ok(failure):
|
|
296
|
+
return []
|
|
297
|
+
|
|
298
|
+
dm = new_inst.spec.domain_model
|
|
299
|
+
|
|
300
|
+
bad_preds = []
|
|
301
|
+
for p in list(getattr(dm, "state_preds", [])) + list(getattr(dm, "action_preds", [])):
|
|
302
|
+
if getattr(p, "is_iml_valid", None) and str(getattr(p, "is_iml_valid")) == "IMLValidity.INVALID":
|
|
303
|
+
bad_preds.append(p)
|
|
304
|
+
|
|
305
|
+
bad_trans = [
|
|
306
|
+
t
|
|
307
|
+
for t in getattr(dm, "transitions", [])
|
|
308
|
+
if getattr(t, "is_iml_valid", None) and str(getattr(t, "is_iml_valid")) == "IMLValidity.INVALID"
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
if not bad_preds and not bad_trans:
|
|
312
|
+
return []
|
|
313
|
+
|
|
314
|
+
lines: list[str] = []
|
|
315
|
+
if bad_preds:
|
|
316
|
+
lines.append(
|
|
317
|
+
f"- Invalid predicates: {', '.join(f'`{p.name}`' for p in bad_preds[:6])}{' …' if len(bad_preds) > 6 else ''}"
|
|
318
|
+
)
|
|
319
|
+
if bad_trans:
|
|
320
|
+
lines.append(
|
|
321
|
+
f"- Invalid transitions: {', '.join(f'`{t.name}`' for t in bad_trans[:6])}{' …' if len(bad_trans) > 6 else ''}"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return [
|
|
325
|
+
Recommendation(
|
|
326
|
+
kind="warning",
|
|
327
|
+
text=(
|
|
328
|
+
"Some model components fail IML/typechecking.\n"
|
|
329
|
+
+ "\n".join(lines)
|
|
330
|
+
+ "\nRecommendation: fix IML errors first; scenario matching and trace checking won’t be trustworthy otherwise."
|
|
331
|
+
),
|
|
332
|
+
)
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _rule_traceability_nudge(
|
|
337
|
+
old_inst: StateInstance,
|
|
338
|
+
new_inst: StateInstance,
|
|
339
|
+
failure: ChangeFailure | None,
|
|
340
|
+
) -> Iterable[Recommendation]:
|
|
341
|
+
if not _ok(failure):
|
|
342
|
+
return []
|
|
343
|
+
|
|
344
|
+
amap = new_inst.art_map
|
|
345
|
+
if amap is None:
|
|
346
|
+
return []
|
|
347
|
+
|
|
348
|
+
n_arts = (
|
|
349
|
+
len(new_inst.art_container.test_traces)
|
|
350
|
+
+ len(new_inst.art_container.log_traces)
|
|
351
|
+
+ len(new_inst.art_container.doc_ref)
|
|
352
|
+
+ len(new_inst.art_container.src_code)
|
|
353
|
+
)
|
|
354
|
+
n_links = sum(len(v) for v in amap.art_to_comp_map.values())
|
|
355
|
+
|
|
356
|
+
if n_arts >= 3 and n_links == 0:
|
|
357
|
+
return [
|
|
358
|
+
Recommendation(
|
|
359
|
+
text=(
|
|
360
|
+
"You have artifacts loaded, but **no traceability links** yet (ArtifactMap is empty).\n"
|
|
361
|
+
"Recommendation: run trace matching (or add explicit links) so docs/src/traces connect to "
|
|
362
|
+
"predicates/transitions/scenarios. This makes the synthesized spec explainable."
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
return []
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _rule_state_diff_summary(
|
|
371
|
+
old_inst: StateInstance,
|
|
372
|
+
new_inst: StateInstance,
|
|
373
|
+
failure: ChangeFailure | None,
|
|
374
|
+
) -> Iterable[Recommendation]:
|
|
375
|
+
if not _ok(failure):
|
|
376
|
+
return []
|
|
377
|
+
|
|
378
|
+
sd: StateDiff | None = new_inst.state_diff
|
|
379
|
+
if sd is None:
|
|
380
|
+
return []
|
|
381
|
+
|
|
382
|
+
recs: list[Recommendation] = []
|
|
383
|
+
|
|
384
|
+
# 1) If base got better/worse, say that first
|
|
385
|
+
base_out = _outcome(sd.base_status) # type: ignore[arg-type]
|
|
386
|
+
if base_out in (ComparisonOutcome.IMPROVED, ComparisonOutcome.DECLINED):
|
|
387
|
+
if base_out is ComparisonOutcome.IMPROVED:
|
|
388
|
+
recs.append(
|
|
389
|
+
_rec(
|
|
390
|
+
f"Domain model base improved ({sd.base_status.before} → {sd.base_status.after}). "
|
|
391
|
+
"Proceed to checking predicates/transitions and scenario consistency.",
|
|
392
|
+
kind="info",
|
|
393
|
+
priority=10,
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
else:
|
|
397
|
+
recs.append(
|
|
398
|
+
_rec(
|
|
399
|
+
f"Domain model base declined ({sd.base_status.before} → {sd.base_status.after}). "
|
|
400
|
+
"Fix base IML/type issues before trusting any further analysis.",
|
|
401
|
+
kind="error",
|
|
402
|
+
priority=0,
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# 2) Predicates
|
|
407
|
+
if _outcome(sd.num_preds_errored) is ComparisonOutcome.IMPROVED: # type: ignore[arg-type]
|
|
408
|
+
recs.append(
|
|
409
|
+
_rec(
|
|
410
|
+
f"Fewer predicate errors ({sd.num_preds_errored.before} → {sd.num_preds_errored.after}). "
|
|
411
|
+
"Re-run scenario consistency + trace matching to leverage the improved predicate set.",
|
|
412
|
+
kind="info",
|
|
413
|
+
priority=20,
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
elif _outcome(sd.num_preds_errored) is ComparisonOutcome.DECLINED: # type: ignore[arg-type]
|
|
417
|
+
recs.append(
|
|
418
|
+
_rec(
|
|
419
|
+
f"More predicate errors ({sd.num_preds_errored.before} → {sd.num_preds_errored.after}). "
|
|
420
|
+
"Inspect the most recently added/edited predicates and fix their IML first.",
|
|
421
|
+
kind="error",
|
|
422
|
+
priority=5,
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
st_err_out = _outcome(sd.num_state_preds_errored) # type: ignore[arg-type]
|
|
427
|
+
if st_err_out in (ComparisonOutcome.IMPROVED, ComparisonOutcome.DECLINED):
|
|
428
|
+
recs.append(
|
|
429
|
+
_rec(
|
|
430
|
+
f"State-predicate errors changed ({sd.num_state_preds_errored.before} → {sd.num_state_preds_errored.after}). "
|
|
431
|
+
"Focus on `state -> bool` predicates first; they gate most scenario `given` checks.",
|
|
432
|
+
kind="info" if st_err_out is ComparisonOutcome.IMPROVED else "warning",
|
|
433
|
+
priority=23 if st_err_out is ComparisonOutcome.IMPROVED else 9,
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
act_err_out = _outcome(sd.num_action_preds_errored) # type: ignore[arg-type]
|
|
438
|
+
if act_err_out in (ComparisonOutcome.IMPROVED, ComparisonOutcome.DECLINED):
|
|
439
|
+
recs.append(
|
|
440
|
+
_rec(
|
|
441
|
+
f"Action-predicate errors changed ({sd.num_action_preds_errored.before} → {sd.num_action_preds_errored.after}). "
|
|
442
|
+
"Fix `state * action -> bool` predicates; they affect scenario `when` and trace matching.",
|
|
443
|
+
kind="info" if act_err_out is ComparisonOutcome.IMPROVED else "warning",
|
|
444
|
+
priority=24 if act_err_out is ComparisonOutcome.IMPROVED else 10,
|
|
445
|
+
)
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
if _outcome(sd.num_preds_matched) is ComparisonOutcome.IMPROVED: # type: ignore[arg-type]
|
|
449
|
+
recs.append(
|
|
450
|
+
_rec(
|
|
451
|
+
f"More predicates are now matched to artifacts ({sd.num_preds_matched.before} → {sd.num_preds_matched.after}). "
|
|
452
|
+
"Explain these matches in prose and add missing traceability for remaining artifacts.",
|
|
453
|
+
kind="next",
|
|
454
|
+
priority=30,
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
# 3) Transitions
|
|
459
|
+
if _outcome(sd.num_trans_valid_logic) is ComparisonOutcome.IMPROVED: # type: ignore[arg-type]
|
|
460
|
+
recs.append(
|
|
461
|
+
_rec(
|
|
462
|
+
f"More transitions are logic-valid ({sd.num_trans_valid_logic.before} → {sd.num_trans_valid_logic.after}). "
|
|
463
|
+
"Attach these transitions to scenarios’ `then` lists so traces can match end-to-end.",
|
|
464
|
+
kind="next",
|
|
465
|
+
priority=25,
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
elif _outcome(sd.num_trans_valid_logic) is ComparisonOutcome.DECLINED: # type: ignore[arg-type]
|
|
469
|
+
recs.append(
|
|
470
|
+
_rec(
|
|
471
|
+
f"Fewer transitions are logic-valid ({sd.num_trans_valid_logic.before} → {sd.num_trans_valid_logic.after}). "
|
|
472
|
+
"Revert or fix the last transition edit; invalid transitions break scenario execution and matching.",
|
|
473
|
+
kind="error",
|
|
474
|
+
priority=5,
|
|
475
|
+
)
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
# 4) Scenarios
|
|
479
|
+
if _outcome(sd.num_sc_missing) is ComparisonOutcome.DECLINED:
|
|
480
|
+
recs.append(
|
|
481
|
+
_rec(
|
|
482
|
+
f"More scenarios are missing components ({sd.num_sc_missing.before} → {sd.num_sc_missing.after}). "
|
|
483
|
+
"Add the referenced predicates/transitions or remove them from those scenarios.",
|
|
484
|
+
kind="error",
|
|
485
|
+
priority=8,
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
elif _outcome(sd.num_sc_missing) is ComparisonOutcome.IMPROVED:
|
|
489
|
+
recs.append(
|
|
490
|
+
_rec(
|
|
491
|
+
f"Fewer scenarios are missing components ({sd.num_sc_missing.before} → {sd.num_sc_missing.after}). "
|
|
492
|
+
"Re-check scenario consistency and conflicts now that they are fully specified.",
|
|
493
|
+
kind="next",
|
|
494
|
+
priority=18,
|
|
495
|
+
)
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
if _outcome(sd.num_sc_inconsistent) is ComparisonOutcome.DECLINED:
|
|
499
|
+
recs.append(
|
|
500
|
+
_rec(
|
|
501
|
+
f"More scenarios are inconsistent ({sd.num_sc_inconsistent.before} → {sd.num_sc_inconsistent.after}). "
|
|
502
|
+
"Split scenarios or relax contradictory predicate combinations.",
|
|
503
|
+
kind="error",
|
|
504
|
+
priority=12,
|
|
505
|
+
)
|
|
506
|
+
)
|
|
507
|
+
elif _outcome(sd.num_sc_inconsistent) is ComparisonOutcome.IMPROVED:
|
|
508
|
+
recs.append(
|
|
509
|
+
_rec(
|
|
510
|
+
f"Fewer scenarios are inconsistent ({sd.num_sc_inconsistent.before} → {sd.num_sc_inconsistent.after}). "
|
|
511
|
+
"You’re in a good place to match traces and generate human-readable explanations.",
|
|
512
|
+
kind="info",
|
|
513
|
+
priority=22,
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
if _outcome(sd.num_sc_conflicted) is ComparisonOutcome.DECLINED:
|
|
518
|
+
recs.append(
|
|
519
|
+
_rec(
|
|
520
|
+
f"More scenario conflicts/overlaps detected ({sd.num_sc_conflicted.before} → {sd.num_sc_conflicted.after}). "
|
|
521
|
+
"Add boundary predicates to make scenarios disjoint, or document intended priority.",
|
|
522
|
+
kind="error",
|
|
523
|
+
priority=15,
|
|
524
|
+
)
|
|
525
|
+
)
|
|
526
|
+
elif _outcome(sd.num_sc_conflicted) is ComparisonOutcome.IMPROVED:
|
|
527
|
+
recs.append(
|
|
528
|
+
_rec(
|
|
529
|
+
f"Fewer scenario conflicts/overlaps ({sd.num_sc_conflicted.before} → {sd.num_sc_conflicted.after}). "
|
|
530
|
+
"Proceed to coverage work—ensure each important trace has at least one matching scenario.",
|
|
531
|
+
kind="next",
|
|
532
|
+
priority=28,
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# 5) Trace coverage
|
|
537
|
+
if _outcome(sd.num_test_traces_logic_good) is ComparisonOutcome.DECLINED:
|
|
538
|
+
recs.append(
|
|
539
|
+
_rec(
|
|
540
|
+
f"More test traces are failing IML validity ({sd.num_test_traces_logic_good.before} → {sd.num_test_traces_logic_good.after}). "
|
|
541
|
+
"Fix the trace encodings (given/when/then) so matching is meaningful.",
|
|
542
|
+
kind="error",
|
|
543
|
+
priority=10,
|
|
544
|
+
)
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
if _outcome(sd.num_test_traces_matched) is ComparisonOutcome.IMPROVED:
|
|
548
|
+
recs.append(
|
|
549
|
+
_rec(
|
|
550
|
+
f"More test traces matched scenarios ({sd.num_test_traces_matched.before} → {sd.num_test_traces_matched.after}). "
|
|
551
|
+
"Summarize the matched scenarios as the system’s inferred behavior for those tests.",
|
|
552
|
+
kind="next",
|
|
553
|
+
priority=35,
|
|
554
|
+
)
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
if _outcome(sd.num_log_traces_matched) is ComparisonOutcome.IMPROVED:
|
|
558
|
+
recs.append(
|
|
559
|
+
_rec(
|
|
560
|
+
f"More log traces matched scenarios ({sd.num_log_traces_matched.before} → {sd.num_log_traces_matched.after}). "
|
|
561
|
+
"Treat these as in-the-wild confirmation and use them to justify model boundaries.",
|
|
562
|
+
kind="next",
|
|
563
|
+
priority=40,
|
|
564
|
+
)
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
if not recs:
|
|
568
|
+
recs.append(
|
|
569
|
+
_rec(
|
|
570
|
+
"No major metric movement detected in the last change. "
|
|
571
|
+
"Add a predicate/transition/scenario (or new traces) to drive measurable progress.",
|
|
572
|
+
kind="info",
|
|
573
|
+
priority=60,
|
|
574
|
+
)
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
return recs
|