dsh-logicprobe 0.8.0 → 0.9.0
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.
- package/README.en-US.md +8 -0
- package/README.md +8 -0
- package/lib/concurrency-tool.js +1 -1
- package/lib/engine.js +275 -48
- package/lib/index.js +12 -3
- package/lib/tool.js +1 -1
- package/lib/types/engine.d.ts +1 -1
- package/package.json +17 -12
- package/skills/logicprobe/SKILL.md +16 -3
- package/skills/logicprobe/references/dsh-model-schema.md +6 -5
- package/skills/logicprobe/references/logic-verification-guide.md +7 -3
- package/skills/logicprobe/references/logicprobe-engine.py +232 -42
- package/skills/logicprobe-datamodel/SKILL.md +1 -1
- package/src/concurrency-tool.ts +2 -2
- package/src/engine.ts +262 -39
- package/src/index.ts +12 -3
- package/src/tool.ts +2 -2
|
@@ -68,7 +68,8 @@ The same reasoning applies to every id a check names. A reference to a state, ev
|
|
|
68
68
|
| `transitions[].guard` variables | a declared variable |
|
|
69
69
|
| `resourcePairs[].acquireEvent` / `.releaseEvent` / `.failEvent` | a declared event |
|
|
70
70
|
| `invariants[].event` (`event-before-state`) | a declared event |
|
|
71
|
-
| `invariants[].state` / `.states[]` / `.from` / `.
|
|
71
|
+
| `invariants[].state` / `.states[]` / `.from` / `.target` / `.when.state` | a declared state |
|
|
72
|
+
| `invariants[].to` and every member of `invariants[].to[]` (`leads-to`) | a declared state |
|
|
72
73
|
|
|
73
74
|
A vacuous check is worse than a rejected one: a `leads-to` invariant whose `to` state is misspelled can never be violated, and a `never-states` list holding a nonexistent id can never be reached, so both report "pass" for a model that was never actually checked.
|
|
74
75
|
|
|
@@ -162,13 +163,13 @@ A guard is exactly one of:
|
|
|
162
163
|
| `never-states` | `{ states: ["ERROR"] }` | No reachable runtime state may be in the forbidden set |
|
|
163
164
|
| `var-in-range` | `{ variable, min?, max?, when? }` | Every reachable runtime state selected by `when` keeps the variable in range (at least one of `min`/`max` is required) |
|
|
164
165
|
| `event-before-state` | `{ event: "power_ready", state: "ACTIVE" }` | Every path entering `state` must have passed through `event` first |
|
|
165
|
-
| `leads-to` | `{ from: "MIGRATING", to: "DONE" }` | Every path from `from` must eventually reach `to
|
|
166
|
+
| `leads-to` | `{ from: "MIGRATING", to: "DONE" }`, or `to: ["DONE", "FAILED"]` | Every path from `from` must eventually reach `to`. With a target set, every path must reach at least one member. The set must be non-empty and free of duplicates |
|
|
166
167
|
| `sequence` | `{ events: ["backup", "modify", "commit"] }` | Events must occur in the given order |
|
|
167
168
|
| `atomicity` | `{ events: ["write"], commit: "commit", rollback?: "rollback" }` | Atomic group must end with commit/rollback before leaving scope |
|
|
168
169
|
| `budget` | `{ budget: n }` | No reachable path may accumulate transition cost greater than n (A12). Costs are non-negative; a transition without `cost` counts 1, so legacy machines keep step-count semantics |
|
|
169
170
|
| `probability` | `{ target, op: one of >= <= > <, p }` | P(ever hitting target) must satisfy the bound (A13, DTMC from transition `weight`, default 1; value iteration) |
|
|
170
171
|
|
|
171
|
-
A7 reports
|
|
172
|
+
A7 reports a violating path for each failed invariant, and an empty path means the initial state already violates it. The state-predicate kinds, `sequence`, `atomicity` and `budget` are breadth-first, so their witness is the shortest one; `leads-to` and `probability` report a witness without claiming minimality.
|
|
172
173
|
|
|
173
174
|
### Scoping a range to a state (`when`)
|
|
174
175
|
|
|
@@ -281,7 +282,7 @@ When `beforeModel` is passed to `logicprobe_verify`, the engine treats `model` a
|
|
|
281
282
|
| Check | Purpose |
|
|
282
283
|
|---|---|
|
|
283
284
|
| D1 Behavioral Preservation | Every BEFORE (state, event) that could fire must still be fireable from the mapped AFTER state |
|
|
284
|
-
| D2 Invariant Continuity | Every BEFORE invariant (mapped through `stateMapping`) must still hold in AFTER |
|
|
285
|
+
| D2 Invariant Continuity | Every BEFORE invariant (mapped through `stateMapping`) must still hold in AFTER. State predicates go through the same reachability search as S7; `leads-to`, `sequence`, `atomicity`, `budget` and `probability` are re-decided by the searches their own probes (A9-A13) use |
|
|
285
286
|
| D3 Regression Delta | Lists added/removed states, events, and transitions |
|
|
286
287
|
| D4 Deadlock/Liveness Regression | New deadlock states or closed SCCs not present in BEFORE |
|
|
287
288
|
|
|
@@ -306,7 +307,7 @@ List events that must be idempotent in `idempotentEvents`. For every reachable s
|
|
|
306
307
|
## Advanced constraints (S8, A9-A14)
|
|
307
308
|
|
|
308
309
|
- **S8 Monotonic Variables**: declare `monotonic: "inc"|"dec"` on a variable; updates must not move in the opposite direction.
|
|
309
|
-
- **A9 Leads-To**: `{ kind: "leads-to", from, to }` — every path from `from` must eventually reach `to`.
|
|
310
|
+
- **A9 Leads-To**: `{ kind: "leads-to", from, to }` — every path from `from` must eventually reach `to`. `to` is one state id or a non-empty array of state ids for "reach any one of these"; a duplicated member is a validation error rather than a shorthand. Either way the property stays universal: one branch that loops forever, or that stops before every target, refutes it.
|
|
310
311
|
- **A10 Sequence**: `{ kind: "sequence", events }` — events must appear in order.
|
|
311
312
|
- **A11 Atomicity**: `{ kind: "atomicity", events, commit, rollback? }` — once an atomic event starts, the machine must reach commit/rollback before leaving the atomic scope or terminating.
|
|
312
313
|
- **A12 Budget**: `{ kind: "budget", budget }` — no reachable path may accumulate transition cost above the budget; reports the shortest over-budget path and flags reachable positive-cost cycles as unbounded.
|
|
@@ -271,6 +271,8 @@ def shortest_violating_path(states, init, invariant_check):
|
|
|
271
271
|
return None # Invariant holds for all reachable states
|
|
272
272
|
```
|
|
273
273
|
|
|
274
|
+
That template decides one runtime state at a time, so it only fits the state-predicate kinds. A `leads-to`, `sequence`, `atomicity`, `budget` or `probability` invariant asserts a property of a whole run: reuse the search in its own section (A9-A13) as the witness finder, or A7 will report "all invariants hold" for a model where one of them fails.
|
|
275
|
+
|
|
274
276
|
### S8: Monotonic Variables
|
|
275
277
|
|
|
276
278
|
For counters or progress variables that must only move in one direction, list the events that increase/decrease them. Any event that moves against the declared direction is a finding.
|
|
@@ -291,11 +293,13 @@ IDEMPOTENT_EVENTS = {"retry", "sync", "webhook_delivery"}
|
|
|
291
293
|
|
|
292
294
|
### A9: Leads-To
|
|
293
295
|
|
|
294
|
-
For progress claims ("from MIGRATING, eventually DONE"), check every path from the source state. If a path dead-ends or loops before reaching the target, flag it.
|
|
296
|
+
For progress claims ("from MIGRATING, eventually DONE"), check every path from the source state. If a path dead-ends or loops before reaching the target, flag it. When the code has several acceptable outcomes, give the invariant a target set and check every path reaches at least one member; that is still a universal claim, so a branch that reaches none of them fails.
|
|
295
297
|
|
|
296
298
|
```python
|
|
297
299
|
LEADS_TO = [
|
|
298
300
|
("MIGRATING", "DONE"),
|
|
301
|
+
# or a set of acceptable outcomes
|
|
302
|
+
("DRAINING", ["IDLE", "FAULT"]),
|
|
299
303
|
]
|
|
300
304
|
```
|
|
301
305
|
|
|
@@ -438,7 +442,7 @@ The BEFORE model comes from **code, not the plan**. The plan may describe the cu
|
|
|
438
442
|
|
|
439
443
|
**Behavioral preservation**: For each event sequence accepted by BEFORE, trace the same sequence in AFTER. If AFTER ends in a different state (or rejects the sequence), flag as BEHAVIORAL DELTA. The plan must explicitly document this delta — if it doesn't, it's a regression.
|
|
440
444
|
|
|
441
|
-
**Invariant continuity**: Re-
|
|
445
|
+
**Invariant continuity**: Re-decide every invariant on AFTER, mapped through `stateMapping`. Any invariant that holds on BEFORE but fails on AFTER is a regression — the refactoring broke an existing guarantee. State predicates go through the same reachability search as S7; `leads-to`, `sequence`, `atomicity`, `budget` and `probability` are re-decided by the searches their own probes (A9-A13) use, so continuity agrees with the standalone verdict. A state reference that the mapping drops is reported as a mapped-state warning instead of silently changing the property.
|
|
442
446
|
|
|
443
447
|
**Complexity delta**: Count objectively:
|
|
444
448
|
|
|
@@ -507,7 +511,7 @@ Manual verification is reliable for state machines with **≤ 10 states and ≤
|
|
|
507
511
|
|
|
508
512
|
**A6 Resource Injection**: For each state that appears to allocate (names containing "alloc", "init", "start", "open", "connect", "begin"), check if there is a recovery/error path if the allocation fails. If no recovery exists, flag as RESOURCE VULNERABLE.
|
|
509
513
|
|
|
510
|
-
**A7 Minimal Counter-Example**: For each invariant that failed
|
|
514
|
+
**A7 Minimal Counter-Example**: For each invariant that failed, find the event sequence that violates it. This is the most useful output — it gives the plan author an exact repro. The searches for state predicates, `sequence`, `atomicity` and `budget` are breadth-first, so their witness is the shortest one; the `leads-to` walk and the `probability` bound report what they found without claiming minimality.
|
|
511
515
|
|
|
512
516
|
### Manual Mode Output Format
|
|
513
517
|
|
|
@@ -392,11 +392,32 @@ def validate_model(input_value):
|
|
|
392
392
|
bad(p + '.from', 'must be a non-empty string')
|
|
393
393
|
elif lfrom not in invariant_state_ids:
|
|
394
394
|
bad(p + '.from', 'references unknown state ' + lfrom)
|
|
395
|
+
# `to` is one state id or a target set. A set with a duplicate
|
|
396
|
+
# member is a model-authoring slip that would otherwise give one
|
|
397
|
+
# meaning two hashes, so it is rejected rather than deduplicated.
|
|
395
398
|
lto = entry.get('to')
|
|
396
|
-
if
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
399
|
+
if isinstance(lto, str):
|
|
400
|
+
if len(lto) == 0:
|
|
401
|
+
bad(p + '.to', 'must be a non-empty string')
|
|
402
|
+
elif lto not in invariant_state_ids:
|
|
403
|
+
bad(p + '.to', 'references unknown state ' + lto)
|
|
404
|
+
elif isinstance(lto, list):
|
|
405
|
+
if len(lto) == 0:
|
|
406
|
+
bad(p + '.to', 'must be a non-empty array of state ids')
|
|
407
|
+
else:
|
|
408
|
+
seen_targets = set()
|
|
409
|
+
for target_index, target in enumerate(lto):
|
|
410
|
+
at = p + '.to[' + str(target_index) + ']'
|
|
411
|
+
if not isinstance(target, str) or len(target) == 0:
|
|
412
|
+
bad(at, 'must be a non-empty string')
|
|
413
|
+
elif target not in invariant_state_ids:
|
|
414
|
+
bad(at, 'references unknown state ' + target)
|
|
415
|
+
elif target in seen_targets:
|
|
416
|
+
bad(at, 'duplicates state ' + target + ' in the target set')
|
|
417
|
+
else:
|
|
418
|
+
seen_targets.add(target)
|
|
419
|
+
else:
|
|
420
|
+
bad(p + '.to', 'must be a state id or a non-empty array of state ids')
|
|
400
421
|
elif kind == 'sequence':
|
|
401
422
|
evs = entry.get('events')
|
|
402
423
|
if not isinstance(evs, list) or len(evs) == 0:
|
|
@@ -1190,10 +1211,98 @@ def _shortest_violation_for_invariant(model, max_states, invariant):
|
|
|
1190
1211
|
return None
|
|
1191
1212
|
|
|
1192
1213
|
|
|
1193
|
-
def
|
|
1214
|
+
def _is_path_property_invariant(invariant):
|
|
1215
|
+
# Whether an invariant asserts a property of whole runs rather than of a single
|
|
1216
|
+
# runtime state. These kinds cannot be decided by a state predicate, so they are
|
|
1217
|
+
# not part of _shortest_violation_for_invariant (and therefore not of S7), which
|
|
1218
|
+
# owns the states; A9-A13 own the verdicts, and _path_property_violation below
|
|
1219
|
+
# makes the same verdicts available to A7 and D2.
|
|
1220
|
+
return invariant['kind'] in ('leads-to', 'sequence', 'atomicity', 'budget', 'probability')
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
def _probability_violated(invariant, probability):
|
|
1224
|
+
# Whether a probability invariant's bound fails for the computed hit probability.
|
|
1225
|
+
eps = 1e-9
|
|
1226
|
+
op = invariant['op']
|
|
1227
|
+
if op == '>=':
|
|
1228
|
+
return probability < invariant['p'] - eps
|
|
1229
|
+
if op == '>':
|
|
1230
|
+
return probability <= invariant['p'] + eps
|
|
1231
|
+
if op == '<=':
|
|
1232
|
+
return probability > invariant['p'] + eps
|
|
1233
|
+
return probability >= invariant['p'] - eps
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
def _reachable_runtime_states(model, max_states):
|
|
1237
|
+
# Every runtime state reachable from init, capped like the other searches.
|
|
1238
|
+
init = _initial_state(model)
|
|
1239
|
+
visited = set([_runtime_key(init)])
|
|
1240
|
+
reached = [init]
|
|
1241
|
+
queue = deque([init])
|
|
1242
|
+
steps = 0
|
|
1243
|
+
while queue:
|
|
1244
|
+
steps += 1
|
|
1245
|
+
if steps > max_states:
|
|
1246
|
+
break
|
|
1247
|
+
current = queue.popleft()
|
|
1248
|
+
for event in _all_events(model):
|
|
1249
|
+
for nxt in _step_runtime(model, current, event):
|
|
1250
|
+
k = _runtime_key(nxt)
|
|
1251
|
+
if k in visited:
|
|
1252
|
+
continue
|
|
1253
|
+
visited.add(k)
|
|
1254
|
+
reached.append(nxt)
|
|
1255
|
+
queue.append(nxt)
|
|
1256
|
+
return reached
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
def _path_property_violation(model, max_states, invariant):
|
|
1260
|
+
# Violation of a path-property invariant, or None when it holds. Each kind
|
|
1261
|
+
# delegates to the search its dedicated probe uses (A9 leads-to, A10 sequence, A11
|
|
1262
|
+
# atomicity, A12 budget, A13 probability), so a verdict reported here can never
|
|
1263
|
+
# disagree with the probe that owns the kind.
|
|
1264
|
+
kind = invariant['kind']
|
|
1265
|
+
if kind == 'leads-to':
|
|
1266
|
+
targets = set(_leads_to_targets(invariant))
|
|
1267
|
+
label = _leads_to_target_label(invariant)
|
|
1268
|
+
for runtime in _reachable_runtime_states(model, max_states):
|
|
1269
|
+
if runtime['state'] != invariant['from']:
|
|
1270
|
+
continue
|
|
1271
|
+
bad = _find_leads_to_bad_path(model, runtime, targets, label)
|
|
1272
|
+
if bad is not None:
|
|
1273
|
+
return {'invariant': invariant, 'path': bad['path'], 'reason': bad['reason']}
|
|
1274
|
+
return None
|
|
1275
|
+
if kind == 'sequence':
|
|
1276
|
+
return _find_sequence_violation(model, max_states, invariant)
|
|
1277
|
+
if kind == 'atomicity':
|
|
1278
|
+
return _find_atomicity_violation(model, max_states, invariant)
|
|
1279
|
+
if kind == 'budget':
|
|
1280
|
+
violation = _find_budget_violation(model, max_states, invariant)
|
|
1281
|
+
if violation is None:
|
|
1282
|
+
return None
|
|
1283
|
+
reason = ('a reachable cycle keeps accumulating cost, so no finite budget holds'
|
|
1284
|
+
if violation['unbounded']
|
|
1285
|
+
else 'a run accumulates cost ' + str(violation['cost']) + ', above the declared budget ' + str(invariant['budget']))
|
|
1286
|
+
return {'invariant': invariant, 'path': violation['path'], 'reason': reason}
|
|
1287
|
+
if kind == 'probability':
|
|
1288
|
+
computed = _compute_hit_probability(model, max_states, invariant['target'])
|
|
1289
|
+
probability = computed['probability']
|
|
1290
|
+
if not _probability_violated(invariant, probability):
|
|
1291
|
+
return None
|
|
1292
|
+
reason = ('P(hit ' + invariant['target'] + ') = ' + '{:.6f}'.format(probability)
|
|
1293
|
+
+ ' does not satisfy ' + invariant['op'] + ' ' + str(invariant['p'])
|
|
1294
|
+
+ ('' if computed['converged'] else ' (value iteration did not converge)'))
|
|
1295
|
+
return {'invariant': invariant, 'path': [], 'reason': reason}
|
|
1296
|
+
return None
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
def _run_invariants(model, max_states, include_path_properties=False):
|
|
1194
1300
|
violations = []
|
|
1195
1301
|
for invariant in model.get('invariants') or []:
|
|
1196
|
-
|
|
1302
|
+
if include_path_properties and _is_path_property_invariant(invariant):
|
|
1303
|
+
violation = _path_property_violation(model, max_states, invariant)
|
|
1304
|
+
else:
|
|
1305
|
+
violation = _shortest_violation_for_invariant(model, max_states, invariant)
|
|
1197
1306
|
if violation is not None:
|
|
1198
1307
|
violations.append(violation)
|
|
1199
1308
|
return violations
|
|
@@ -1575,14 +1684,27 @@ def A6_resource_injection(model):
|
|
|
1575
1684
|
|
|
1576
1685
|
|
|
1577
1686
|
def A7_shortest_violations(model, max_states):
|
|
1578
|
-
|
|
1687
|
+
# Path-property invariants are included: "for any invariant that fails" is this
|
|
1688
|
+
# check's contract, and a check that silently skips five of the eight kinds cannot
|
|
1689
|
+
# honour it. The searches for the state kinds, for sequence, atomicity and budget
|
|
1690
|
+
# are breadth-first, so their first witness is the shortest one; the leads-to walk
|
|
1691
|
+
# and the probability bound have no such guarantee, and their message says what it
|
|
1692
|
+
# found instead of claiming minimality.
|
|
1693
|
+
violations = _run_invariants(model, max_states, True)
|
|
1579
1694
|
findings = []
|
|
1580
1695
|
for violation in violations:
|
|
1581
1696
|
invariant = violation['invariant']
|
|
1697
|
+
path_length = len(violation['path'])
|
|
1698
|
+
if _is_path_property_invariant(invariant):
|
|
1699
|
+
message = ('Invariant "' + invariant['id'] + '" violated: ' + violation['reason']
|
|
1700
|
+
+ ('' if path_length == 0 else ' (witness path length: ' + str(path_length) + ')'))
|
|
1701
|
+
else:
|
|
1702
|
+
message = ('Invariant "' + invariant['id'] + '" shortest violating path length: ' + str(path_length)
|
|
1703
|
+
+ (' (initial state)' if path_length == 0 else ''))
|
|
1582
1704
|
findings.append({
|
|
1583
1705
|
'code': 'A7_SHORTEST_COUNTEREXAMPLE',
|
|
1584
1706
|
'severity': 'warning',
|
|
1585
|
-
'message':
|
|
1707
|
+
'message': message,
|
|
1586
1708
|
'path': violation['path'],
|
|
1587
1709
|
'evidence': {'invariant': invariant['id']},
|
|
1588
1710
|
})
|
|
@@ -1648,6 +1770,14 @@ def _map_invariant_for_comparison(invariant, mapping):
|
|
|
1648
1770
|
when = invariant.get('when')
|
|
1649
1771
|
if when is not None and 'state' in when:
|
|
1650
1772
|
out['when'] = {'state': _map_state_id(mapping, when['state'])}
|
|
1773
|
+
elif invariant['kind'] == 'leads-to':
|
|
1774
|
+
# A target set maps member by member; a rename that drops a state is reported by
|
|
1775
|
+
# D2's own mapped-state check rather than silently changing the property.
|
|
1776
|
+
out['from'] = _map_state_id(mapping, invariant['from'])
|
|
1777
|
+
to = invariant['to']
|
|
1778
|
+
out['to'] = _map_state_id(mapping, to) if isinstance(to, str) else [_map_state_id(mapping, s) for s in to]
|
|
1779
|
+
elif invariant['kind'] == 'probability':
|
|
1780
|
+
out['target'] = _map_state_id(mapping, invariant['target'])
|
|
1651
1781
|
return out
|
|
1652
1782
|
|
|
1653
1783
|
|
|
@@ -1683,7 +1813,33 @@ def D2_invariant_continuity(before, after, max_states, mapping):
|
|
|
1683
1813
|
'evidence': {'invariant': invariant['id'], 'variable': mapped['variable']},
|
|
1684
1814
|
})
|
|
1685
1815
|
continue
|
|
1686
|
-
|
|
1816
|
+
elif mapped['kind'] == 'leads-to':
|
|
1817
|
+
# The state references of a path-property invariant must follow stateMapping
|
|
1818
|
+
# too, otherwise a pure rename searches for a state AFTER does not declare.
|
|
1819
|
+
for state in [mapped['from']] + _leads_to_targets(mapped):
|
|
1820
|
+
if state not in after_states:
|
|
1821
|
+
findings.append({
|
|
1822
|
+
'code': 'D2_MAPPED_STATE_MISSING',
|
|
1823
|
+
'severity': 'warning',
|
|
1824
|
+
'message': 'BEFORE invariant "' + invariant['id'] + '" maps to state ' + state + ', which is not declared in AFTER.',
|
|
1825
|
+
'evidence': {'invariant': invariant['id'], 'state': state},
|
|
1826
|
+
})
|
|
1827
|
+
elif mapped['kind'] == 'probability':
|
|
1828
|
+
if mapped['target'] not in after_states:
|
|
1829
|
+
findings.append({
|
|
1830
|
+
'code': 'D2_MAPPED_STATE_MISSING',
|
|
1831
|
+
'severity': 'warning',
|
|
1832
|
+
'message': 'BEFORE invariant "' + invariant['id'] + '" maps to state ' + mapped['target'] + ', which is not declared in AFTER.',
|
|
1833
|
+
'evidence': {'invariant': invariant['id'], 'state': mapped['target']},
|
|
1834
|
+
})
|
|
1835
|
+
# Every invariant the engine can decide is re-decided against AFTER.
|
|
1836
|
+
# Path-property kinds go through the same searches their dedicated probes use,
|
|
1837
|
+
# so D2 can no longer report "all BEFORE invariants continue to hold" while
|
|
1838
|
+
# A9-A13 fail on the very same model.
|
|
1839
|
+
if _is_path_property_invariant(mapped):
|
|
1840
|
+
violation = _path_property_violation(after, max_states, mapped)
|
|
1841
|
+
else:
|
|
1842
|
+
violation = _shortest_violation_for_invariant(after, max_states, mapped)
|
|
1687
1843
|
if violation is not None:
|
|
1688
1844
|
findings.append({
|
|
1689
1845
|
'code': 'D2_INVARIANT_REGRESSION',
|
|
@@ -1869,29 +2025,71 @@ def S8_monotonic_variables(model):
|
|
|
1869
2025
|
'Monotonic variables are respected' if not findings else 'Monotonic findings: ' + str(len(findings)))
|
|
1870
2026
|
|
|
1871
2027
|
|
|
1872
|
-
def
|
|
1873
|
-
|
|
2028
|
+
def _leads_to_targets(invariant):
|
|
2029
|
+
# Target states of a leads-to invariant. The schema accepts one state id or a
|
|
2030
|
+
# non-empty array of them, so "reach any of these" needs no separate kind.
|
|
2031
|
+
to = invariant['to']
|
|
2032
|
+
return [to] if isinstance(to, str) else list(to)
|
|
2033
|
+
|
|
2034
|
+
|
|
2035
|
+
def _leads_to_target_label(invariant):
|
|
2036
|
+
# Target list as findings print it. A single target renders as its bare id, so
|
|
2037
|
+
# existing messages stay byte-identical.
|
|
2038
|
+
return ', '.join(_leads_to_targets(invariant))
|
|
2039
|
+
|
|
2040
|
+
|
|
2041
|
+
def _find_leads_to_bad_path(model, start, targets, label):
|
|
2042
|
+
# Find a run from start that reaches none of targets, or None when every run
|
|
2043
|
+
# reaches at least one of them. The property is universal (see the A9 row in
|
|
2044
|
+
# SKILL.md): one branch that loops forever, or that stops before any target,
|
|
2045
|
+
# refutes it.
|
|
2046
|
+
#
|
|
2047
|
+
# Depth-first walk of the run graph with three colours; runs at a target are
|
|
2048
|
+
# success leaves that are never expanded. A node reached while it is on the
|
|
2049
|
+
# current walk (GRAY) closes a cycle that avoids every target. A node with no
|
|
2050
|
+
# outgoing step at all stops the machine where it stands, which is the same
|
|
2051
|
+
# violation for a different reason. A node whose walk completed without a
|
|
2052
|
+
# violation is BLACK, and reaching it again from another branch is a shared
|
|
2053
|
+
# sub-graph, not a cycle -- that is why the colour map cannot be a plain visited
|
|
2054
|
+
# set: a diamond (two branches rejoining) is acyclic and must pass, while a
|
|
2055
|
+
# genuine cycle must not. A run is identified by its state plus its variable
|
|
2056
|
+
# values.
|
|
2057
|
+
if start['state'] in targets:
|
|
1874
2058
|
return None
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2059
|
+
GRAY = 1
|
|
2060
|
+
BLACK = 2
|
|
2061
|
+
color = {}
|
|
2062
|
+
stack = [{'runtime': start, 'path': [], 'nexts': None, 'index': 0}]
|
|
2063
|
+
color[_runtime_key(start)] = GRAY
|
|
2064
|
+
while stack:
|
|
2065
|
+
frame = stack[-1]
|
|
2066
|
+
if frame['nexts'] is None:
|
|
2067
|
+
nexts = []
|
|
2068
|
+
for event in _all_events(model):
|
|
2069
|
+
for nxt in _step_runtime(model, frame['runtime'], event):
|
|
2070
|
+
nexts.append({'next': nxt, 'event': event})
|
|
2071
|
+
frame['nexts'] = nexts
|
|
2072
|
+
if not nexts:
|
|
2073
|
+
return {'path': frame['path'], 'reason': 'Dead end before target ' + label}
|
|
2074
|
+
if frame['index'] >= len(frame['nexts']):
|
|
2075
|
+
color[_runtime_key(frame['runtime'])] = BLACK
|
|
2076
|
+
stack.pop()
|
|
1881
2077
|
continue
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
if
|
|
1890
|
-
return {'path':
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
2078
|
+
item = frame['nexts'][frame['index']]
|
|
2079
|
+
frame['index'] += 1
|
|
2080
|
+
step = {'from': frame['runtime']['state'], 'event': item['event'], 'to': item['next']['state']}
|
|
2081
|
+
if item['next']['state'] in targets:
|
|
2082
|
+
continue
|
|
2083
|
+
key = _runtime_key(item['next'])
|
|
2084
|
+
seen = color.get(key)
|
|
2085
|
+
if seen == GRAY:
|
|
2086
|
+
return {'path': frame['path'] + [step], 'reason': 'Cycle avoids target ' + label}
|
|
2087
|
+
if seen == BLACK:
|
|
2088
|
+
continue
|
|
2089
|
+
color[key] = GRAY
|
|
2090
|
+
stack.append({'runtime': item['next'], 'path': frame['path'] + [step], 'nexts': None, 'index': 0})
|
|
2091
|
+
# Every branch either reached the target or rejoined a branch that did.
|
|
2092
|
+
return None
|
|
1895
2093
|
|
|
1896
2094
|
|
|
1897
2095
|
def A9_leads_to(model, exploration):
|
|
@@ -1899,10 +2097,12 @@ def A9_leads_to(model, exploration):
|
|
|
1899
2097
|
for invariant in model.get('invariants') or []:
|
|
1900
2098
|
if invariant['kind'] != 'leads-to':
|
|
1901
2099
|
continue
|
|
2100
|
+
targets = set(_leads_to_targets(invariant))
|
|
2101
|
+
label = _leads_to_target_label(invariant)
|
|
1902
2102
|
for runtime in exploration['reachable']:
|
|
1903
2103
|
if runtime['state'] != invariant['from']:
|
|
1904
2104
|
continue
|
|
1905
|
-
bad = _find_leads_to_bad_path(model, runtime,
|
|
2105
|
+
bad = _find_leads_to_bad_path(model, runtime, targets, label)
|
|
1906
2106
|
if bad is not None:
|
|
1907
2107
|
findings.append({
|
|
1908
2108
|
'code': 'A9_LEADS_TO_VIOLATION',
|
|
@@ -2282,7 +2482,6 @@ def _compute_hit_probability(model, max_states, target_state):
|
|
|
2282
2482
|
|
|
2283
2483
|
def A13_probability(model, max_states):
|
|
2284
2484
|
findings = []
|
|
2285
|
-
eps = 1e-9
|
|
2286
2485
|
for invariant in model.get('invariants') or []:
|
|
2287
2486
|
if invariant['kind'] != 'probability':
|
|
2288
2487
|
continue
|
|
@@ -2291,16 +2490,7 @@ def A13_probability(model, max_states):
|
|
|
2291
2490
|
converged = result['converged']
|
|
2292
2491
|
op = invariant['op']
|
|
2293
2492
|
p_bound = invariant['p']
|
|
2294
|
-
|
|
2295
|
-
if op == '>=':
|
|
2296
|
-
violated = probability < p_bound - eps
|
|
2297
|
-
elif op == '>':
|
|
2298
|
-
violated = probability <= p_bound + eps
|
|
2299
|
-
elif op == '<=':
|
|
2300
|
-
violated = probability > p_bound + eps
|
|
2301
|
-
else:
|
|
2302
|
-
violated = probability >= p_bound - eps
|
|
2303
|
-
if violated:
|
|
2493
|
+
if _probability_violated(invariant, probability):
|
|
2304
2494
|
findings.append({
|
|
2305
2495
|
'code': 'A13_PROBABILITY_VIOLATION',
|
|
2306
2496
|
'severity': 'error',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: logicprobe-datamodel
|
|
3
|
-
description: "Use when reviewing design documents, architecture specs, technical proposals, schema changes, data contracts, or refactoring plans that make claims about entities, fields, constraints, relationships, data invariants, migration coverage, or before/after data-model equivalence. When the document contains schema changes, data migration logic, or data-
|
|
3
|
+
description: "Use when reviewing design documents, architecture specs, technical proposals, schema changes, data contracts, or refactoring plans that make claims about entities, fields, constraints, relationships, data invariants, migration coverage, copy consistency, rollback symmetry, or before/after data-model equivalence. When the document contains schema changes, data migration logic, or data-behavioural assertions ('all records must have X', 'target count equals source count', 'no orphan rows', 'ids never move backwards', 'exactly one row per event', 'rollback restores the old shape', 'migration is non-breaking'), escalate into data-model verification — generate and run executable checks for structural consistency (DS1-DS4), data invariants and adversarial probes (DA1-DA12: null/boundary injection, uniqueness, referential integrity, migration coverage, copy consistency, rollback symmetry, idempotency, monotonicity, ordering, leads-to, atomicity), and before/after data regression (DD1-DD4) before trusting any claim. Behavioural, state-machine, protocol and concurrency claims belong to the sibling logicprobe skill. Also proactively SUGGEST this skill for code-level data/schema behavioural questions."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Logic Probe Data
|
package/src/concurrency-tool.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
2
|
import type { JsonValue } from './json-value.js'
|
|
3
3
|
import { runConcurrencyScan } from './concurrency.js'
|
|
4
4
|
|
|
@@ -13,7 +13,7 @@ export const LOGICPROBE_CONCURRENCY_SCAN_TOOL_NAME = 'logicprobe_concurrency_sca
|
|
|
13
13
|
export const logicProbeConcurrencyScanTool = defineTool({
|
|
14
14
|
name: LOGICPROBE_CONCURRENCY_SCAN_TOOL_NAME,
|
|
15
15
|
description:
|
|
16
|
-
'Scan a document or plan text for concurrency risk points. Use ONLY after confirming the target actually has concurrency requirements or behavior (threads, async tasks, interrupts, shared state, parallel execution). If the target is purely sequential, do not call this tool. Returns findings for keywords like thread-safe, lock-free, data race, race condition, atomic, synchronized, mutex, semaphore, shared variable, reentrant, interrupt-safe. Absolute claims (thread-safe, lock-free, no data race) are flagged as errors requiring dedicated verification.',
|
|
16
|
+
'Scan a document or plan text for concurrency risk points. Use ONLY after confirming the target actually has concurrency requirements or behavior (threads, async tasks, interrupts, shared state, parallel execution). If the target is purely sequential, do not call this tool. Returns findings for keywords like thread-safe, lock-free, wait-free, data race, race condition, atomic, synchronized, mutex, semaphore, spinlock, shared variable, shared memory, reentrant, interrupt-safe, ISR, IRQ, NMI, critical section, disable_irq/enable_irq. Absolute claims (thread-safe, lock-free, no data race, interrupt-safe, ISR-safe) are flagged as errors requiring dedicated verification, and each carries suggestions naming the analysis to run instead (TSan/Helgrind, CBMC, TLA+, or RTOS-aware interrupt analysis). logicprobe does not prove concurrency safety.',
|
|
17
17
|
parameters: {
|
|
18
18
|
text: {
|
|
19
19
|
type: 'string',
|