dsh-logicprobe 0.8.1 → 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/lib/concurrency-tool.js +1 -1
- package/lib/engine.js +230 -39
- package/lib/index.js +12 -3
- package/lib/tool.js +1 -1
- package/lib/types/engine.d.ts +1 -1
- package/package.json +1 -1
- 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 +201 -38
- package/skills/logicprobe-datamodel/SKILL.md +1 -1
- package/src/concurrency-tool.ts +2 -2
- package/src/engine.ts +215 -34
- package/src/index.ts +12 -3
- package/src/tool.ts +2 -2
|
@@ -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,21 +2025,36 @@ 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
|
-
#
|
|
1874
|
-
#
|
|
1875
|
-
|
|
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.
|
|
1876
2046
|
#
|
|
1877
|
-
# Depth-first walk of the run graph with three colours; target
|
|
1878
|
-
# leaves that are never expanded. A node reached while it is on the
|
|
1879
|
-
# (GRAY) closes a cycle that avoids
|
|
1880
|
-
# all stops the machine where it stands, which is the same
|
|
1881
|
-
# different reason. A node whose walk completed without a
|
|
1882
|
-
# reaching it again from another branch is a shared
|
|
1883
|
-
# is why the colour map cannot be a plain visited
|
|
1884
|
-
# rejoining) is acyclic and must pass, while a
|
|
1885
|
-
# identified by its state plus its variable
|
|
1886
|
-
|
|
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:
|
|
1887
2058
|
return None
|
|
1888
2059
|
GRAY = 1
|
|
1889
2060
|
BLACK = 2
|
|
@@ -1899,7 +2070,7 @@ def _find_leads_to_bad_path(model, start, target):
|
|
|
1899
2070
|
nexts.append({'next': nxt, 'event': event})
|
|
1900
2071
|
frame['nexts'] = nexts
|
|
1901
2072
|
if not nexts:
|
|
1902
|
-
return {'path': frame['path'], 'reason': 'Dead end before target ' +
|
|
2073
|
+
return {'path': frame['path'], 'reason': 'Dead end before target ' + label}
|
|
1903
2074
|
if frame['index'] >= len(frame['nexts']):
|
|
1904
2075
|
color[_runtime_key(frame['runtime'])] = BLACK
|
|
1905
2076
|
stack.pop()
|
|
@@ -1907,12 +2078,12 @@ def _find_leads_to_bad_path(model, start, target):
|
|
|
1907
2078
|
item = frame['nexts'][frame['index']]
|
|
1908
2079
|
frame['index'] += 1
|
|
1909
2080
|
step = {'from': frame['runtime']['state'], 'event': item['event'], 'to': item['next']['state']}
|
|
1910
|
-
if item['next']['state']
|
|
2081
|
+
if item['next']['state'] in targets:
|
|
1911
2082
|
continue
|
|
1912
2083
|
key = _runtime_key(item['next'])
|
|
1913
2084
|
seen = color.get(key)
|
|
1914
2085
|
if seen == GRAY:
|
|
1915
|
-
return {'path': frame['path'] + [step], 'reason': 'Cycle avoids target ' +
|
|
2086
|
+
return {'path': frame['path'] + [step], 'reason': 'Cycle avoids target ' + label}
|
|
1916
2087
|
if seen == BLACK:
|
|
1917
2088
|
continue
|
|
1918
2089
|
color[key] = GRAY
|
|
@@ -1926,10 +2097,12 @@ def A9_leads_to(model, exploration):
|
|
|
1926
2097
|
for invariant in model.get('invariants') or []:
|
|
1927
2098
|
if invariant['kind'] != 'leads-to':
|
|
1928
2099
|
continue
|
|
2100
|
+
targets = set(_leads_to_targets(invariant))
|
|
2101
|
+
label = _leads_to_target_label(invariant)
|
|
1929
2102
|
for runtime in exploration['reachable']:
|
|
1930
2103
|
if runtime['state'] != invariant['from']:
|
|
1931
2104
|
continue
|
|
1932
|
-
bad = _find_leads_to_bad_path(model, runtime,
|
|
2105
|
+
bad = _find_leads_to_bad_path(model, runtime, targets, label)
|
|
1933
2106
|
if bad is not None:
|
|
1934
2107
|
findings.append({
|
|
1935
2108
|
'code': 'A9_LEADS_TO_VIOLATION',
|
|
@@ -2309,7 +2482,6 @@ def _compute_hit_probability(model, max_states, target_state):
|
|
|
2309
2482
|
|
|
2310
2483
|
def A13_probability(model, max_states):
|
|
2311
2484
|
findings = []
|
|
2312
|
-
eps = 1e-9
|
|
2313
2485
|
for invariant in model.get('invariants') or []:
|
|
2314
2486
|
if invariant['kind'] != 'probability':
|
|
2315
2487
|
continue
|
|
@@ -2318,16 +2490,7 @@ def A13_probability(model, max_states):
|
|
|
2318
2490
|
converged = result['converged']
|
|
2319
2491
|
op = invariant['op']
|
|
2320
2492
|
p_bound = invariant['p']
|
|
2321
|
-
|
|
2322
|
-
if op == '>=':
|
|
2323
|
-
violated = probability < p_bound - eps
|
|
2324
|
-
elif op == '>':
|
|
2325
|
-
violated = probability <= p_bound + eps
|
|
2326
|
-
elif op == '<=':
|
|
2327
|
-
violated = probability > p_bound + eps
|
|
2328
|
-
else:
|
|
2329
|
-
violated = probability >= p_bound - eps
|
|
2330
|
-
if violated:
|
|
2493
|
+
if _probability_violated(invariant, probability):
|
|
2331
2494
|
findings.append({
|
|
2332
2495
|
'code': 'A13_PROBABILITY_VIOLATION',
|
|
2333
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',
|