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
package/lib/concurrency-tool.js
CHANGED
|
@@ -9,7 +9,7 @@ export const LOGICPROBE_CONCURRENCY_SCAN_TOOL_NAME = 'logicprobe_concurrency_sca
|
|
|
9
9
|
*/
|
|
10
10
|
export const logicProbeConcurrencyScanTool = defineTool({
|
|
11
11
|
name: LOGICPROBE_CONCURRENCY_SCAN_TOOL_NAME,
|
|
12
|
-
description: '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.',
|
|
12
|
+
description: '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.',
|
|
13
13
|
parameters: {
|
|
14
14
|
text: {
|
|
15
15
|
type: 'string',
|
package/lib/engine.js
CHANGED
|
@@ -308,10 +308,36 @@ export function validateModel(input) {
|
|
|
308
308
|
bad(path + '.from', 'must be a non-empty string');
|
|
309
309
|
else if (!invariantStateIds.has(invariant.from))
|
|
310
310
|
bad(path + '.from', 'references unknown state ' + invariant.from);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
311
|
+
// `to` is one state id or a target set. A set with a duplicate member is a
|
|
312
|
+
// model-authoring slip that would otherwise give one meaning two hashes, so it
|
|
313
|
+
// is rejected rather than silently deduplicated.
|
|
314
|
+
if (typeof invariant.to === 'string') {
|
|
315
|
+
if (invariant.to.length === 0)
|
|
316
|
+
bad(path + '.to', 'must be a non-empty string');
|
|
317
|
+
else if (!invariantStateIds.has(invariant.to))
|
|
318
|
+
bad(path + '.to', 'references unknown state ' + invariant.to);
|
|
319
|
+
}
|
|
320
|
+
else if (Array.isArray(invariant.to)) {
|
|
321
|
+
if (invariant.to.length === 0)
|
|
322
|
+
bad(path + '.to', 'must be a non-empty array of state ids');
|
|
323
|
+
else {
|
|
324
|
+
const targets = new Set();
|
|
325
|
+
invariant.to.forEach((target, targetIndex) => {
|
|
326
|
+
const at = path + '.to[' + targetIndex + ']';
|
|
327
|
+
if (typeof target !== 'string' || target.length === 0)
|
|
328
|
+
bad(at, 'must be a non-empty string');
|
|
329
|
+
else if (!invariantStateIds.has(target))
|
|
330
|
+
bad(at, 'references unknown state ' + target);
|
|
331
|
+
else if (targets.has(target))
|
|
332
|
+
bad(at, 'duplicates state ' + target + ' in the target set');
|
|
333
|
+
else
|
|
334
|
+
targets.add(target);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
bad(path + '.to', 'must be a state id or a non-empty array of state ids');
|
|
340
|
+
}
|
|
315
341
|
}
|
|
316
342
|
else if (invariant.kind === 'sequence') {
|
|
317
343
|
if (!Array.isArray(invariant.events) || invariant.events.length === 0)
|
|
@@ -1007,6 +1033,103 @@ function shortestViolationForInvariant(model, options, invariant) {
|
|
|
1007
1033
|
}
|
|
1008
1034
|
return undefined;
|
|
1009
1035
|
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Whether an invariant asserts a property of whole runs rather than of a single
|
|
1038
|
+
* runtime state. These kinds cannot be decided by a state predicate, so they are not
|
|
1039
|
+
* part of `shortestViolationForInvariant` (and therefore not of S7), which owns the
|
|
1040
|
+
* states; A9-A13 own the verdicts, and `pathPropertyViolation` below makes the same
|
|
1041
|
+
* verdicts available to A7 and D2.
|
|
1042
|
+
*/
|
|
1043
|
+
function isPathPropertyInvariant(invariant) {
|
|
1044
|
+
return invariant.kind === 'leads-to'
|
|
1045
|
+
|| invariant.kind === 'sequence'
|
|
1046
|
+
|| invariant.kind === 'atomicity'
|
|
1047
|
+
|| invariant.kind === 'budget'
|
|
1048
|
+
|| invariant.kind === 'probability';
|
|
1049
|
+
}
|
|
1050
|
+
/** Whether a probability invariant's bound fails for the computed hit probability. */
|
|
1051
|
+
function probabilityViolated(invariant, probability) {
|
|
1052
|
+
const eps = 1e-9;
|
|
1053
|
+
if (invariant.op === '>=')
|
|
1054
|
+
return probability < invariant.p - eps;
|
|
1055
|
+
if (invariant.op === '>')
|
|
1056
|
+
return probability <= invariant.p + eps;
|
|
1057
|
+
if (invariant.op === '<=')
|
|
1058
|
+
return probability > invariant.p + eps;
|
|
1059
|
+
return probability >= invariant.p - eps;
|
|
1060
|
+
}
|
|
1061
|
+
/** Every runtime state reachable from init, capped like the other searches. */
|
|
1062
|
+
function reachableRuntimeStates(model, options) {
|
|
1063
|
+
const init = initialState(model);
|
|
1064
|
+
const visited = new Set([runtimeKey(init)]);
|
|
1065
|
+
const reached = [init];
|
|
1066
|
+
const queue = [init];
|
|
1067
|
+
let steps = 0;
|
|
1068
|
+
while (queue.length > 0) {
|
|
1069
|
+
if (++steps > options.maxStates)
|
|
1070
|
+
break;
|
|
1071
|
+
const current = queue.shift();
|
|
1072
|
+
for (const event of allEvents(model)) {
|
|
1073
|
+
for (const next of stepRuntime(model, current, event)) {
|
|
1074
|
+
const key = runtimeKey(next);
|
|
1075
|
+
if (visited.has(key))
|
|
1076
|
+
continue;
|
|
1077
|
+
visited.add(key);
|
|
1078
|
+
reached.push(next);
|
|
1079
|
+
queue.push(next);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return reached;
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Violation of a path-property invariant, or undefined when it holds. Each kind
|
|
1087
|
+
* delegates to the search its dedicated probe uses (A9 leads-to, A10 sequence, A11
|
|
1088
|
+
* atomicity, A12 budget, A13 probability), so a verdict reported here can never
|
|
1089
|
+
* disagree with the probe that owns the kind.
|
|
1090
|
+
*/
|
|
1091
|
+
function pathPropertyViolation(model, options, invariant) {
|
|
1092
|
+
if (invariant.kind === 'leads-to') {
|
|
1093
|
+
const targets = new Set(leadsToTargets(invariant));
|
|
1094
|
+
const label = leadsToTargetLabel(invariant);
|
|
1095
|
+
for (const runtime of reachableRuntimeStates(model, options)) {
|
|
1096
|
+
if (runtime.state !== invariant.from)
|
|
1097
|
+
continue;
|
|
1098
|
+
const bad = findLeadsToBadPath(model, runtime, targets, label);
|
|
1099
|
+
if (bad !== undefined)
|
|
1100
|
+
return { invariant, path: bad.path, reason: bad.reason };
|
|
1101
|
+
}
|
|
1102
|
+
return undefined;
|
|
1103
|
+
}
|
|
1104
|
+
if (invariant.kind === 'sequence')
|
|
1105
|
+
return findSequenceViolation(model, options, invariant);
|
|
1106
|
+
if (invariant.kind === 'atomicity')
|
|
1107
|
+
return findAtomicityViolation(model, options, invariant);
|
|
1108
|
+
if (invariant.kind === 'budget') {
|
|
1109
|
+
const violation = findBudgetViolation(model, options, invariant);
|
|
1110
|
+
if (violation === undefined)
|
|
1111
|
+
return undefined;
|
|
1112
|
+
return {
|
|
1113
|
+
invariant,
|
|
1114
|
+
path: violation.path,
|
|
1115
|
+
reason: violation.unbounded
|
|
1116
|
+
? 'a reachable cycle keeps accumulating cost, so no finite budget holds'
|
|
1117
|
+
: 'a run accumulates cost ' + String(violation.cost) + ', above the declared budget ' + String(invariant.budget),
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
if (invariant.kind === 'probability') {
|
|
1121
|
+
const { probability, converged } = computeHitProbability(model, options, invariant.target);
|
|
1122
|
+
if (!probabilityViolated(invariant, probability))
|
|
1123
|
+
return undefined;
|
|
1124
|
+
return {
|
|
1125
|
+
invariant,
|
|
1126
|
+
path: [],
|
|
1127
|
+
reason: 'P(hit ' + invariant.target + ') = ' + probability.toFixed(6) + ' does not satisfy ' + invariant.op + ' ' + String(invariant.p)
|
|
1128
|
+
+ (converged ? '' : ' (value iteration did not converge)'),
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
return undefined;
|
|
1132
|
+
}
|
|
1010
1133
|
function shortestEventBeforeStateViolation(model, options, invariant) {
|
|
1011
1134
|
const init = initialState(model);
|
|
1012
1135
|
const seenInitially = false;
|
|
@@ -1038,10 +1161,12 @@ function shortestEventBeforeStateViolation(model, options, invariant) {
|
|
|
1038
1161
|
}
|
|
1039
1162
|
return undefined;
|
|
1040
1163
|
}
|
|
1041
|
-
function runInvariants(model, options) {
|
|
1164
|
+
function runInvariants(model, options, includePathProperties = false) {
|
|
1042
1165
|
const violations = [];
|
|
1043
1166
|
for (const invariant of model.invariants ?? []) {
|
|
1044
|
-
const violation =
|
|
1167
|
+
const violation = includePathProperties && isPathPropertyInvariant(invariant)
|
|
1168
|
+
? pathPropertyViolation(model, options, invariant)
|
|
1169
|
+
: shortestViolationForInvariant(model, options, invariant);
|
|
1045
1170
|
if (violation !== undefined)
|
|
1046
1171
|
violations.push(violation);
|
|
1047
1172
|
}
|
|
@@ -1415,14 +1540,27 @@ function A6_resourceInjection(model) {
|
|
|
1415
1540
|
return checkResult('A6', 'Resource Injection', findings, findings.length === 0 ? 'No resource vulnerabilities detected' : 'Resource failure paths missing: ' + findings.length);
|
|
1416
1541
|
}
|
|
1417
1542
|
function A7_shortestViolations(model, options) {
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1543
|
+
// Path-property invariants are included: "for any invariant that fails" is this
|
|
1544
|
+
// check's contract, and a check that silently skips five of the eight kinds cannot
|
|
1545
|
+
// honour it. The searches for the state kinds, for sequence, atomicity and budget
|
|
1546
|
+
// are breadth-first, so their first witness is the shortest one; the leads-to walk
|
|
1547
|
+
// and the probability bound have no such guarantee, and their message says what it
|
|
1548
|
+
// found instead of claiming minimality.
|
|
1549
|
+
const violations = runInvariants(model, options, true);
|
|
1550
|
+
const findings = violations.map((violation) => {
|
|
1551
|
+
const pathLength = violation.path.length;
|
|
1552
|
+
const message = isPathPropertyInvariant(violation.invariant)
|
|
1553
|
+
? 'Invariant "' + violation.invariant.id + '" violated: ' + violation.reason
|
|
1554
|
+
+ (pathLength === 0 ? '' : ' (witness path length: ' + pathLength + ')')
|
|
1555
|
+
: 'Invariant "' + violation.invariant.id + '" shortest violating path length: ' + pathLength + (pathLength === 0 ? ' (initial state)' : '');
|
|
1556
|
+
return {
|
|
1557
|
+
code: 'A7_SHORTEST_COUNTEREXAMPLE',
|
|
1558
|
+
severity: 'warning',
|
|
1559
|
+
message,
|
|
1560
|
+
path: violation.path,
|
|
1561
|
+
evidence: { invariant: violation.invariant.id },
|
|
1562
|
+
};
|
|
1563
|
+
});
|
|
1426
1564
|
return checkResult('A7', 'Minimal Counter-Example', findings, violations.length === 0 ? 'All invariants hold for all reachable paths' : 'Violated invariants: ' + findings.length);
|
|
1427
1565
|
}
|
|
1428
1566
|
function mapStateId(mapping, state) {
|
|
@@ -1500,6 +1638,25 @@ function mapInvariantForComparison(invariant, mapping) {
|
|
|
1500
1638
|
}
|
|
1501
1639
|
return mapped;
|
|
1502
1640
|
}
|
|
1641
|
+
if (invariant.kind === 'leads-to') {
|
|
1642
|
+
// A target set maps member by member; a rename that drops a state is reported by
|
|
1643
|
+
// D2's own mapped-state check rather than silently changing the property.
|
|
1644
|
+
return {
|
|
1645
|
+
...invariant,
|
|
1646
|
+
id: invariant.id + ':before',
|
|
1647
|
+
description: invariant.description + ' (from BEFORE)',
|
|
1648
|
+
from: mapStateId(mapping, invariant.from),
|
|
1649
|
+
to: typeof invariant.to === 'string' ? mapStateId(mapping, invariant.to) : invariant.to.map((state) => mapStateId(mapping, state)),
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
if (invariant.kind === 'probability') {
|
|
1653
|
+
return {
|
|
1654
|
+
...invariant,
|
|
1655
|
+
id: invariant.id + ':before',
|
|
1656
|
+
description: invariant.description + ' (from BEFORE)',
|
|
1657
|
+
target: mapStateId(mapping, invariant.target),
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1503
1660
|
return { ...invariant, id: invariant.id + ':before', description: invariant.description + ' (from BEFORE)' };
|
|
1504
1661
|
}
|
|
1505
1662
|
function D2_invariantContinuity(before, after, options, mapping) {
|
|
@@ -1541,7 +1698,37 @@ function D2_invariantContinuity(before, after, options, mapping) {
|
|
|
1541
1698
|
continue;
|
|
1542
1699
|
}
|
|
1543
1700
|
}
|
|
1544
|
-
|
|
1701
|
+
else if (mapped.kind === 'leads-to') {
|
|
1702
|
+
// The state references of a path-property invariant must follow stateMapping too,
|
|
1703
|
+
// otherwise a pure rename searches for a state AFTER does not declare.
|
|
1704
|
+
for (const state of [mapped.from, ...leadsToTargets(mapped)]) {
|
|
1705
|
+
if (!afterStates.has(state)) {
|
|
1706
|
+
findings.push({
|
|
1707
|
+
code: 'D2_MAPPED_STATE_MISSING',
|
|
1708
|
+
severity: 'warning',
|
|
1709
|
+
message: 'BEFORE invariant "' + invariant.id + '" maps to state ' + state + ', which is not declared in AFTER.',
|
|
1710
|
+
evidence: { invariant: invariant.id, state },
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
else if (mapped.kind === 'probability') {
|
|
1716
|
+
if (!afterStates.has(mapped.target)) {
|
|
1717
|
+
findings.push({
|
|
1718
|
+
code: 'D2_MAPPED_STATE_MISSING',
|
|
1719
|
+
severity: 'warning',
|
|
1720
|
+
message: 'BEFORE invariant "' + invariant.id + '" maps to state ' + mapped.target + ', which is not declared in AFTER.',
|
|
1721
|
+
evidence: { invariant: invariant.id, state: mapped.target },
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
// Every invariant the engine can decide is re-decided against AFTER. Path-property
|
|
1726
|
+
// kinds go through the same searches their dedicated probes use, so D2 can no
|
|
1727
|
+
// longer report "all BEFORE invariants continue to hold" while A9-A13 fail on the
|
|
1728
|
+
// very same model.
|
|
1729
|
+
const violation = isPathPropertyInvariant(mapped)
|
|
1730
|
+
? pathPropertyViolation(after, options, mapped)
|
|
1731
|
+
: shortestViolationForInvariant(after, options, mapped);
|
|
1545
1732
|
if (violation !== undefined) {
|
|
1546
1733
|
findings.push({
|
|
1547
1734
|
code: 'D2_INVARIANT_REGRESSION',
|
|
@@ -1722,15 +1909,27 @@ function S8_monotonicVariables(model) {
|
|
|
1722
1909
|
return checkResult('S8', 'Monotonic Variables', findings, findings.length === 0 ? 'Monotonic variables are respected' : 'Monotonic findings: ' + findings.length);
|
|
1723
1910
|
}
|
|
1724
1911
|
/**
|
|
1725
|
-
*
|
|
1726
|
-
*
|
|
1727
|
-
|
|
1912
|
+
* Target states of a `leads-to` invariant. The schema accepts one state id or a
|
|
1913
|
+
* non-empty array of them, so "reach any of these" needs no separate invariant kind.
|
|
1914
|
+
*/
|
|
1915
|
+
function leadsToTargets(invariant) {
|
|
1916
|
+
return typeof invariant.to === 'string' ? [invariant.to] : invariant.to;
|
|
1917
|
+
}
|
|
1918
|
+
/** Target list as findings print it. A single target renders as its bare id, so existing messages stay byte-identical. */
|
|
1919
|
+
function leadsToTargetLabel(invariant) {
|
|
1920
|
+
return leadsToTargets(invariant).join(', ');
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Find a run from `start` that reaches none of `targets`, or return undefined when
|
|
1924
|
+
* every run reaches at least one of them. The property is universal (see the A9 row
|
|
1925
|
+
* in `SKILL.md`): one branch that loops forever, or that stops before any target,
|
|
1926
|
+
* refutes it.
|
|
1728
1927
|
*
|
|
1729
|
-
* The search is a depth-first walk of the run graph with three colours, and
|
|
1730
|
-
*
|
|
1928
|
+
* The search is a depth-first walk of the run graph with three colours, and runs at a
|
|
1929
|
+
* target are success leaves that are never expanded:
|
|
1731
1930
|
*
|
|
1732
1931
|
* - a node reached while it is on the current walk (GRAY) closes a cycle that avoids
|
|
1733
|
-
*
|
|
1932
|
+
* every target, so some run never reaches one;
|
|
1734
1933
|
* - a node with no outgoing step at all stops the machine where it stands, which is
|
|
1735
1934
|
* the same violation for a different reason;
|
|
1736
1935
|
* - a node whose walk completed without a violation is BLACK, and reaching it again
|
|
@@ -1741,8 +1940,8 @@ function S8_monotonicVariables(model) {
|
|
|
1741
1940
|
* A run is identified by its state plus its variable values, so the same state id
|
|
1742
1941
|
* with different values is a different node.
|
|
1743
1942
|
*/
|
|
1744
|
-
function findLeadsToBadPath(model, start,
|
|
1745
|
-
if (start.state
|
|
1943
|
+
function findLeadsToBadPath(model, start, targets, label) {
|
|
1944
|
+
if (targets.has(start.state))
|
|
1746
1945
|
return undefined;
|
|
1747
1946
|
const GRAY = 1;
|
|
1748
1947
|
const BLACK = 2;
|
|
@@ -1759,7 +1958,7 @@ function findLeadsToBadPath(model, start, target) {
|
|
|
1759
1958
|
}
|
|
1760
1959
|
frame.nexts = nexts;
|
|
1761
1960
|
if (nexts.length === 0)
|
|
1762
|
-
return { path: frame.path, reason: 'Dead end before target ' +
|
|
1961
|
+
return { path: frame.path, reason: 'Dead end before target ' + label };
|
|
1763
1962
|
}
|
|
1764
1963
|
if (frame.index >= frame.nexts.length) {
|
|
1765
1964
|
color.set(runtimeKey(frame.runtime), BLACK);
|
|
@@ -1769,18 +1968,18 @@ function findLeadsToBadPath(model, start, target) {
|
|
|
1769
1968
|
const { next, event } = frame.nexts[frame.index];
|
|
1770
1969
|
frame.index += 1;
|
|
1771
1970
|
const step = { from: frame.runtime.state, event, to: next.state };
|
|
1772
|
-
if (next.state
|
|
1971
|
+
if (targets.has(next.state))
|
|
1773
1972
|
continue;
|
|
1774
1973
|
const key = runtimeKey(next);
|
|
1775
1974
|
const seen = color.get(key);
|
|
1776
1975
|
if (seen === GRAY)
|
|
1777
|
-
return { path: [...frame.path, step], reason: 'Cycle avoids target ' +
|
|
1976
|
+
return { path: [...frame.path, step], reason: 'Cycle avoids target ' + label };
|
|
1778
1977
|
if (seen === BLACK)
|
|
1779
1978
|
continue;
|
|
1780
1979
|
color.set(key, GRAY);
|
|
1781
1980
|
stack.push({ runtime: next, path: [...frame.path, step], nexts: undefined, index: 0 });
|
|
1782
1981
|
}
|
|
1783
|
-
// Every branch either reached
|
|
1982
|
+
// Every branch either reached a target or rejoined a branch that did.
|
|
1784
1983
|
return undefined;
|
|
1785
1984
|
}
|
|
1786
1985
|
function A9_leadsTo(model, exploration) {
|
|
@@ -1788,10 +1987,12 @@ function A9_leadsTo(model, exploration) {
|
|
|
1788
1987
|
for (const invariant of model.invariants ?? []) {
|
|
1789
1988
|
if (invariant.kind !== 'leads-to')
|
|
1790
1989
|
continue;
|
|
1990
|
+
const targets = new Set(leadsToTargets(invariant));
|
|
1991
|
+
const label = leadsToTargetLabel(invariant);
|
|
1791
1992
|
for (const runtime of exploration.reachable) {
|
|
1792
1993
|
if (runtime.state !== invariant.from)
|
|
1793
1994
|
continue;
|
|
1794
|
-
const bad = findLeadsToBadPath(model, runtime,
|
|
1995
|
+
const bad = findLeadsToBadPath(model, runtime, targets, label);
|
|
1795
1996
|
if (bad !== undefined) {
|
|
1796
1997
|
findings.push({
|
|
1797
1998
|
code: 'A9_LEADS_TO_VIOLATION',
|
|
@@ -2199,21 +2400,11 @@ function computeHitProbability(model, options, targetState) {
|
|
|
2199
2400
|
}
|
|
2200
2401
|
function A13_probability(model, options) {
|
|
2201
2402
|
const findings = [];
|
|
2202
|
-
const eps = 1e-9;
|
|
2203
2403
|
for (const invariant of model.invariants ?? []) {
|
|
2204
2404
|
if (invariant.kind !== 'probability')
|
|
2205
2405
|
continue;
|
|
2206
2406
|
const { probability, converged } = computeHitProbability(model, options, invariant.target);
|
|
2207
|
-
|
|
2208
|
-
if (invariant.op === '>=')
|
|
2209
|
-
violated = probability < invariant.p - eps;
|
|
2210
|
-
else if (invariant.op === '>')
|
|
2211
|
-
violated = probability <= invariant.p + eps;
|
|
2212
|
-
else if (invariant.op === '<=')
|
|
2213
|
-
violated = probability > invariant.p + eps;
|
|
2214
|
-
else
|
|
2215
|
-
violated = probability >= invariant.p - eps;
|
|
2216
|
-
if (violated) {
|
|
2407
|
+
if (probabilityViolated(invariant, probability)) {
|
|
2217
2408
|
findings.push({
|
|
2218
2409
|
code: 'A13_PROBABILITY_VIOLATION',
|
|
2219
2410
|
severity: 'error',
|
package/lib/index.js
CHANGED
|
@@ -49,7 +49,7 @@ const GATE_SOURCE_KIND = 'plugin:logicprobe';
|
|
|
49
49
|
const DEFAULT_GATE_CONTENT = `<EXTREMELY_IMPORTANT>
|
|
50
50
|
Plugin logicprobe is active. Documents are not truth — code is. Verify every verifiable claim before accepting or acting on any design.
|
|
51
51
|
|
|
52
|
-
**1% Rule**: If there is even a 1% chance the logicprobe skill applies — reviewing design documents, architecture specs, technical proposals, or refactoring plans that make claims about API names, file locations, enum values, mechanism feasibility, state machines, protocol logic, data models, schema migrations, data invariants,
|
|
52
|
+
**1% Rule**: If there is even a 1% chance the logicprobe skill applies — reviewing design documents, architecture specs, technical proposals, or refactoring plans that make claims about API names, file locations, enum values, mechanism feasibility, state machines, protocol logic, data models, schema migrations, data invariants, behavioral guarantees ("always"/"never"/"guaranteed"), worst-case path cost or declared budgets, probabilistic reachability ("≥90% of runs reach SAFE"), deadlines ("must leave within 2 ticks"), cross-machine handshakes, or concurrency guarantees ("thread-safe"/"lock-free"/"ISR-safe") — load it with the skill tool before responding. The cost of loading is trivial compared to the cost of a false claim.
|
|
53
53
|
|
|
54
54
|
**Red Flags** — if you think any of these, STOP. You are rationalizing:
|
|
55
55
|
|
|
@@ -60,7 +60,16 @@ Plugin logicprobe is active. Documents are not truth — code is. Verify every v
|
|
|
60
60
|
| "I'll verify while implementing" | Verification happens before implementation, not during. |
|
|
61
61
|
| "I can check this with reasoning alone" | Behavioral claims are verified with code/models, not intuition. One counter-example refutes a universal claim. |
|
|
62
62
|
|
|
63
|
-
**Native verification path
|
|
63
|
+
**Native verification path** (dsh) — six model-invocable tools:
|
|
64
|
+
|
|
65
|
+
- \`logicprobe_verify\` — state machines: S1-S8 structural + A1-A14 adversarial, including A12 worst-case path cost against a declared \`budget\`, A13 probabilistic reachability over a DTMC, and A14 discrete-tick deadlines. Pass \`beforeModel\` + \`stateMapping\` to add the D1-D4 before/after regression.
|
|
66
|
+
- \`logicprobe_datamodel_verify\` — data models and schema migrations: DS/DA checks, migration coverage, copy consistency, rollback symmetry, DD1-DD4 data regression.
|
|
67
|
+
- \`logicprobe_compose_verify\` — two or more machines checked together (rendezvous handshake semantics): C1 composition deadlock, C2 rendezvous that never fires.
|
|
68
|
+
- \`logicprobe_concurrency_scan\` — mine concurrency claims (thread-safe, lock-free, race condition, mutex, ISR-safe) and route them to dedicated verification; logicprobe does not prove concurrency safety.
|
|
69
|
+
- \`logicprobe_export\` — emit external-checker input from a verified model: UPPAAL, TLA+, PRISM, SPIN.
|
|
70
|
+
- \`logicprobe_uml\` — model a code flow as UML (render), read a diagram back into a model (parse), or audit the modelling (review: UML001-UML019 structural defects, documentation gaps, diagram-versus-model round-trip fidelity).
|
|
71
|
+
|
|
72
|
+
Python harnesses in the skill references remain the fallback for non-dsh hosts.
|
|
64
73
|
|
|
65
74
|
**Proactive suggestion**: When a user asks code-level behavioral questions — "could this state machine deadlock", "is this retry limit safe", "check this timing sequence for bugs", "is this migration non-breaking", "does this copy cover all required fields" — suggest logicprobe as an optional verification pass (do not auto-escalate).
|
|
66
75
|
</EXTREMELY_IMPORTANT>`;
|
|
@@ -146,7 +155,7 @@ function resolveInteraction(config, session) {
|
|
|
146
155
|
function modeContextText(config, session) {
|
|
147
156
|
const interaction = resolveInteraction(config, session);
|
|
148
157
|
const lines = [
|
|
149
|
-
'logicprobe:
|
|
158
|
+
'logicprobe: native tools — `logicprobe_verify` (state machines: S1-S8 + A1-A14, incl. A12 budget/worst-case path cost, A13 probabilistic reachability, A14 deadlines; `beforeModel` adds D1-D4 regression), `logicprobe_datamodel_verify` (data models and schema migrations: DS/DA, migration coverage, copy consistency, rollback symmetry, DD1-DD4), `logicprobe_compose_verify` (two or more machines: C1 composition deadlock, C2 rendezvous never fires), `logicprobe_concurrency_scan` (mine concurrency claims — it does not prove concurrency safety), `logicprobe_export` (UPPAAL, TLA+, PRISM, SPIN input from a verified model).',
|
|
150
159
|
'logicprobe: use `logicprobe_uml` to model a code flow as UML (render), to read a UML diagram back into a model (parse), or to audit the modelling (review: structural defects, documentation gaps, diagram-vs-model round-trip fidelity).',
|
|
151
160
|
interaction === 'auto'
|
|
152
161
|
? 'logicprobe interaction=auto: do NOT call ask_user_question for model confirmation; run round-trip validation of the extracted transition table and mark the result UNCONFIRMED.'
|
package/lib/tool.js
CHANGED
|
@@ -12,7 +12,7 @@ export const LOGICPROBE_VERIFY_TOOL_NAME = 'logicprobe_verify';
|
|
|
12
12
|
*/
|
|
13
13
|
export const logicProbeVerifyTool = defineTool({
|
|
14
14
|
name: LOGICPROBE_VERIFY_TOOL_NAME,
|
|
15
|
-
description: 'Run executable state-machine verification (logicprobe). Takes a LogicModelV1 object with schemaVersion=1, init, states ({id, terminal?}), transitions ({from, event, to, guard?, updates?, cost?}), variables?, invariants?, concurrentPairs?, boundaryChecks?, resourcePairs?, idempotentEvents?, narrative?. Guards are structured ({variable, op, value} | {all} | {any} | {not}); invariants support never-states, var-in-range ({variable, min?, max?, when?} — when scopes the range to a state ({state}) or a guard node, is evaluated on the post-state of every transition, leaves the initial state checked unconditionally, and is rejected on any other invariant kind), event-before-state, leads-to, sequence, atomicity, budget (transition cost defaults to 1; A12 checks every reachable path stays within the declared budget and reports the shortest over-budget counterexample), and probability (transition weight, default 1, makes the model a DTMC; A13 checks P(hit target) against the bound by value iteration). The schema is closed: an undeclared key on any model part (model, state, transition, update, variable, invariant, boundaryCheck, resourcePair, narrative, scenario) is a validation error, never ignored. Variables support monotonic inc/dec. The optional narrative block carries natural-language descriptions of states (narrative.states), events (narrative.events), and (state, event) scenarios (narrative.scenarios: [{from, event, scenario}]); when present it must fully cover the model and is echoed in the report. Returns a report with S1-S8 structural checks and A1-
|
|
15
|
+
description: 'Run executable state-machine verification (logicprobe). Takes a LogicModelV1 object with schemaVersion=1, init, states ({id, terminal?}), transitions ({from, event, to, guard?, updates?, cost?}), variables?, invariants?, concurrentPairs?, boundaryChecks?, resourcePairs?, idempotentEvents?, narrative?. Guards are structured ({variable, op, value} | {all} | {any} | {not}); invariants support never-states, var-in-range ({variable, min?, max?, when?} — when scopes the range to a state ({state}) or a guard node, is evaluated on the post-state of every transition, leaves the initial state checked unconditionally, and is rejected on any other invariant kind), event-before-state, leads-to (`to` is one state id or a non-empty array of them: every path must reach at least one member), sequence, atomicity, budget (transition cost defaults to 1; A12 checks every reachable path stays within the declared budget and reports the shortest over-budget counterexample), and probability (transition weight, default 1, makes the model a DTMC; A13 checks P(hit target) against the bound by value iteration), and deadline (declaring maxTicks on a state is how many tick steps it may stay resident and the top-level tickEvents list names those steps; A14 reports the over-residency path, and A14_NO_TICK_EVENTS when maxTicks is declared without tickEvents). The schema is closed: an undeclared key on any model part (model, state, transition, update, variable, invariant, boundaryCheck, resourcePair, narrative, scenario) is a validation error, never ignored. Variables support monotonic inc/dec. The optional narrative block carries natural-language descriptions of states (narrative.states), events (narrative.events), and (state, event) scenarios (narrative.scenarios: [{from, event, scenario}]); when present it must fully cover the model and is echoed in the report. Returns a report with S1-S8 structural checks and A1-A14 adversarial, cost, probability and deadline probes, including counterexample paths (shortest where the search is breadth-first). If beforeModel is provided, also runs D1-D4 before/after regression checks. See skills/logicprobe/references/dsh-model-schema.md.',
|
|
16
16
|
parameters: {
|
|
17
17
|
model: {
|
|
18
18
|
type: 'json',
|
package/lib/types/engine.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-logicprobe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Design document & plan claim verification — enumerate claims, verify against codebase facts, then escalate to state-machine verification (S1-S8/A1-A12, including budget/worst-case path-cost checks) and data-model verification (DS/DA/DD) for behavioral claims. Supports before/after regression, idempotency/monotonic/sequence/leads-to/atomicity constraints, and concurrency risk mining. Ships a native DeepSeek Harness (dsh) bundle that injects the claim-verification gate into the first model step.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: logicprobe
|
|
3
|
-
description: "Use when reviewing design documents, architecture specs, technical proposals, or refactoring plans that make claims about API names, file locations, enum values, or mechanism feasibility.
|
|
3
|
+
description: "Use when reviewing design documents, architecture specs, technical proposals, or refactoring plans that make claims about API names, file locations, enum values, or mechanism feasibility. Escalate into logic-primitive verification — generate and run an executable model before trusting the claim — when the document asserts state-machine or protocol behaviour (≥3 states, ACK/NACK/retry sequences, guards, lock/unlock pairing), a quantitative or temporal guarantee (worst-case path cost ≤ budget, P(reach SAFE) ≥ p, 'must leave within 2 ticks'), a cross-machine handshake (req/ack, power-up sequencing between two components), concurrency guarantees ('thread-safe', 'lock-free', 'ISR-safe' — mined and routed, never proven here), or a refactoring that changes state topology or guard conditions, where the pipeline compares before/after models for behavioral preservation and regression freedom. ALSO use when the task is to model a code flow as UML, or to audit a diagram somebody drew: the skill renders the model as a state, activity or sequence diagram, reads a hand-drawn diagram back into a model, and reviews the modelling — unreachable states, dead ends, ambiguous branches, documentation gaps, diagram-versus-model round-trip fidelity. Data-model, schema and migration claims belong to the sibling logicprobe-datamodel skill. ALSO proactively SUGGEST this skill (do not require) when a user asks code-level behavioral questions — 'check this timing for bugs', 'could this state machine deadlock', 'is this retry limit safe' — since plan-level verification has usually already been done."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Logic Probe
|
|
@@ -147,6 +147,19 @@ UML modelling variant (the task is to draw a flow, not to check a claim):
|
|
|
147
147
|
→ logicprobe_verify → the behavioural checks (S1-S8 / A1-A14)
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
### Native dsh Tools
|
|
151
|
+
|
|
152
|
+
| Tool | Covers |
|
|
153
|
+
|------|--------|
|
|
154
|
+
| `logicprobe_verify` | S1-S8 structural + A1-A14 adversarial, including A12 budget / worst-case path cost, A13 probabilistic reachability over a DTMC, and A14 discrete-tick deadlines. `beforeModel` + `stateMapping` add the D1-D4 before/after regression. |
|
|
155
|
+
| `logicprobe_datamodel_verify` | Data models and schema migrations: DS/DA checks plus the DD1-DD4 data regression. See the `logicprobe-datamodel` skill. |
|
|
156
|
+
| `logicprobe_compose_verify` | Two or more machines checked together (rendezvous handshake semantics): C1 composition deadlock, C2 rendezvous never fires. |
|
|
157
|
+
| `logicprobe_concurrency_scan` | Mines concurrency claims (thread-safe, lock-free, race condition, mutex, ISR-safe) and routes them to dedicated verification. It does not prove concurrency safety. |
|
|
158
|
+
| `logicprobe_export` | Emits external-checker input from a verified model: UPPAAL, TLA+, PRISM, SPIN. |
|
|
159
|
+
| `logicprobe_uml` | render / parse / review for UML modelling — see [UML Modelling and Modelling Review](#uml-modelling-and-modelling-review). |
|
|
160
|
+
|
|
161
|
+
`references/logicprobe-engine.py` mirrors the same checks and carries `compose` and `export` subcommands for hosts without the native tools.
|
|
162
|
+
|
|
150
163
|
### Refactoring Verification Mode
|
|
151
164
|
|
|
152
165
|
When the document under review is a refactoring plan (modifying existing state machine logic, not designing from scratch), adapt the pipeline:
|
|
@@ -206,9 +219,9 @@ Run these SECOND. Each probe actively tries to BREAK the model. If any probe suc
|
|
|
206
219
|
| A4 | **Pair symmetry** | Match every `start/stop`, `lock/unlock`, `alloc/free` pair. Flag if any state allows a path where a pair is unbalanced (start without stop, lock without unlock). State `onEntry`/`onExit` actions are treated as implicit acquire/release. | "Resources are always released" |
|
|
207
220
|
| A5 | **Boundary blast** | Probe counters at 0, 1, max-1, max, max+1. Probe timestamps at 0, tick_wraparound. Flag overflow, underflow, or undefined behavior. | "Handles all counter/timer values" |
|
|
208
221
|
| A6 | **Resource injection** | Simulate `malloc→NULL`, `queue→full`, `semaphore→timeout` at each state that calls them. Flag if any state has no recovery path. | "Graceful degradation under resource pressure" |
|
|
209
|
-
| A7 | **
|
|
222
|
+
| A7 | **Counter-example witness** | For any invariant that fails, find the event sequence that violates it and output the exact path. Breadth-first searches (state predicates, sequence, atomicity, budget) yield the shortest witness; the leads-to walk and the probability bound report the witness they found without claiming minimality. | "This invariant holds" → refuted by a concrete path |
|
|
210
223
|
| A8 | **Idempotent replay** | For each event declared in `idempotentEvents`, apply it twice from every reachable state. Flag if the second application changes state or is not replayable. | "This event is safe to retry/replay" |
|
|
211
|
-
| A9 | **Leads-to** | From a declared source state, every path must eventually reach the target state. | "This state always progresses to completion" |
|
|
224
|
+
| A9 | **Leads-to** | From a declared source state, every path must eventually reach the target state, or reach one member of a declared target set. | "This state always progresses to completion" |
|
|
212
225
|
| A10 | **Sequence order** | Events declared in a sequence must occur in the specified order. | "backup before modify before commit" |
|
|
213
226
|
| A11 | **Atomicity** | Once an atomic event starts, the machine must commit or roll back before leaving the scope or terminating. | "all-or-nothing transaction" |
|
|
214
227
|
| A12 | **Budget (worst-case path cost)** | With `cost` on transitions (absent = 1) and a `budget` invariant, every reachable path must stay within budget; reports the shortest over-budget counterexample and flags reachable positive-cost cycles as unbounded. | "Worst-case path cost ≤ budget" |
|
|
@@ -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
|
|