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
package/README.en-US.md
CHANGED
|
@@ -97,6 +97,14 @@ npx -p @deepseek-ai/dsh dsh plugin --profile web add dsh-logicprobe
|
|
|
97
97
|
|
|
98
98
|
Restart the profile afterwards. `dsh --profile web --dump-config` must show the `id: logicprobe` row with `enabled: true`. More options (plain skill copy, project-level install) are in [`.dsh/INSTALL.md`](.dsh/INSTALL.md).
|
|
99
99
|
|
|
100
|
+
pnpm 11 has a release-age gate. It holds back versions published less than a day ago (`minimumReleaseAge`, default 1440 minutes), and its default is non-strict, so a bare-name install **silently resolves to the previous version**. The profile then looks like the release never happened. To get the newest version within about 24 hours of a release, pin it:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
dsh plugin --profile web add dsh-logicprobe@<version>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
pnpm records that version in a `minimumReleaseAgeExclude` entry in the profile's `pnpm-workspace.yaml`. That entry is pnpm's documented escape hatch.
|
|
107
|
+
|
|
100
108
|
> Package name note: the npm package is `dsh-logicprobe`, with no scope. In the web profile's `package.json`, both the dependency key and the `dsh.profile.bundles` entry must use that name. On a mismatch the dsh loader cannot find `node_modules/dsh-logicprobe` and the boot fails.
|
|
101
109
|
|
|
102
110
|
## UML Modelling and Review
|
package/README.md
CHANGED
|
@@ -95,6 +95,14 @@ npx -p @deepseek-ai/dsh dsh plugin --profile web add dsh-logicprobe
|
|
|
95
95
|
|
|
96
96
|
装完重启 profile。运行 `dsh --profile web --dump-config` 应看到 `id: logicprobe` 且 `enabled: true`。更多方式(纯技能拷贝、项目级等)见 [`.dsh/INSTALL.md`](.dsh/INSTALL.md)。
|
|
97
97
|
|
|
98
|
+
pnpm 11 有一个发布年龄闸门。它挡住发布不满一天的新版本(`minimumReleaseAge`,默认 1440 分钟),而且默认是非严格模式,所以裸名安装会**静默装到上一个版本**,profile 看起来像这次发版没发生。发版后约 24 小时内要装最新版,请带上版本号:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
dsh plugin --profile web add dsh-logicprobe@<version>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
pnpm 会把该版本写进 profile 的 `pnpm-workspace.yaml` 的 `minimumReleaseAgeExclude` 条目。那是它官方的豁免方式。
|
|
105
|
+
|
|
98
106
|
> 注意包名。npm 包名是 `dsh-logicprobe`,没有 scope。在 web profile 的 `package.json` 中,依赖键与 `dsh.profile.bundles` 必须都写 `dsh-logicprobe`。写错时 dsh 加载器找不到 `node_modules/dsh-logicprobe`,启动会失败。
|
|
99
107
|
|
|
100
108
|
## UML 建模与审查
|
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',
|
|
@@ -1721,41 +1908,91 @@ function S8_monotonicVariables(model) {
|
|
|
1721
1908
|
}
|
|
1722
1909
|
return checkResult('S8', 'Monotonic Variables', findings, findings.length === 0 ? 'Monotonic variables are respected' : 'Monotonic findings: ' + findings.length);
|
|
1723
1910
|
}
|
|
1724
|
-
|
|
1725
|
-
|
|
1911
|
+
/**
|
|
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.
|
|
1927
|
+
*
|
|
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:
|
|
1930
|
+
*
|
|
1931
|
+
* - a node reached while it is on the current walk (GRAY) closes a cycle that avoids
|
|
1932
|
+
* every target, so some run never reaches one;
|
|
1933
|
+
* - a node with no outgoing step at all stops the machine where it stands, which is
|
|
1934
|
+
* the same violation for a different reason;
|
|
1935
|
+
* - a node whose walk completed without a violation is BLACK, and reaching it again
|
|
1936
|
+
* from another branch is a shared sub-graph, not a cycle.
|
|
1937
|
+
*
|
|
1938
|
+
* That last point is why the colour map cannot be a plain visited set: a diamond
|
|
1939
|
+
* (two branches rejoining) is acyclic and must pass, while a genuine cycle must not.
|
|
1940
|
+
* A run is identified by its state plus its variable values, so the same state id
|
|
1941
|
+
* with different values is a different node.
|
|
1942
|
+
*/
|
|
1943
|
+
function findLeadsToBadPath(model, start, targets, label) {
|
|
1944
|
+
if (targets.has(start.state))
|
|
1726
1945
|
return undefined;
|
|
1727
|
-
const
|
|
1728
|
-
const
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
if (
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1946
|
+
const GRAY = 1;
|
|
1947
|
+
const BLACK = 2;
|
|
1948
|
+
const color = new Map();
|
|
1949
|
+
const stack = [{ runtime: start, path: [], nexts: undefined, index: 0 }];
|
|
1950
|
+
color.set(runtimeKey(start), GRAY);
|
|
1951
|
+
while (stack.length > 0) {
|
|
1952
|
+
const frame = stack[stack.length - 1];
|
|
1953
|
+
if (frame.nexts === undefined) {
|
|
1954
|
+
const nexts = [];
|
|
1955
|
+
for (const event of allEvents(model)) {
|
|
1956
|
+
for (const next of stepRuntime(model, frame.runtime, event))
|
|
1957
|
+
nexts.push({ next, event });
|
|
1958
|
+
}
|
|
1959
|
+
frame.nexts = nexts;
|
|
1960
|
+
if (nexts.length === 0)
|
|
1961
|
+
return { path: frame.path, reason: 'Dead end before target ' + label };
|
|
1741
1962
|
}
|
|
1742
|
-
if (nexts.length
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1963
|
+
if (frame.index >= frame.nexts.length) {
|
|
1964
|
+
color.set(runtimeKey(frame.runtime), BLACK);
|
|
1965
|
+
stack.pop();
|
|
1966
|
+
continue;
|
|
1746
1967
|
}
|
|
1968
|
+
const { next, event } = frame.nexts[frame.index];
|
|
1969
|
+
frame.index += 1;
|
|
1970
|
+
const step = { from: frame.runtime.state, event, to: next.state };
|
|
1971
|
+
if (targets.has(next.state))
|
|
1972
|
+
continue;
|
|
1973
|
+
const key = runtimeKey(next);
|
|
1974
|
+
const seen = color.get(key);
|
|
1975
|
+
if (seen === GRAY)
|
|
1976
|
+
return { path: [...frame.path, step], reason: 'Cycle avoids target ' + label };
|
|
1977
|
+
if (seen === BLACK)
|
|
1978
|
+
continue;
|
|
1979
|
+
color.set(key, GRAY);
|
|
1980
|
+
stack.push({ runtime: next, path: [...frame.path, step], nexts: undefined, index: 0 });
|
|
1747
1981
|
}
|
|
1748
|
-
|
|
1982
|
+
// Every branch either reached a target or rejoined a branch that did.
|
|
1983
|
+
return undefined;
|
|
1749
1984
|
}
|
|
1750
1985
|
function A9_leadsTo(model, exploration) {
|
|
1751
1986
|
const findings = [];
|
|
1752
1987
|
for (const invariant of model.invariants ?? []) {
|
|
1753
1988
|
if (invariant.kind !== 'leads-to')
|
|
1754
1989
|
continue;
|
|
1990
|
+
const targets = new Set(leadsToTargets(invariant));
|
|
1991
|
+
const label = leadsToTargetLabel(invariant);
|
|
1755
1992
|
for (const runtime of exploration.reachable) {
|
|
1756
1993
|
if (runtime.state !== invariant.from)
|
|
1757
1994
|
continue;
|
|
1758
|
-
const bad = findLeadsToBadPath(model, runtime,
|
|
1995
|
+
const bad = findLeadsToBadPath(model, runtime, targets, label);
|
|
1759
1996
|
if (bad !== undefined) {
|
|
1760
1997
|
findings.push({
|
|
1761
1998
|
code: 'A9_LEADS_TO_VIOLATION',
|
|
@@ -2163,21 +2400,11 @@ function computeHitProbability(model, options, targetState) {
|
|
|
2163
2400
|
}
|
|
2164
2401
|
function A13_probability(model, options) {
|
|
2165
2402
|
const findings = [];
|
|
2166
|
-
const eps = 1e-9;
|
|
2167
2403
|
for (const invariant of model.invariants ?? []) {
|
|
2168
2404
|
if (invariant.kind !== 'probability')
|
|
2169
2405
|
continue;
|
|
2170
2406
|
const { probability, converged } = computeHitProbability(model, options, invariant.target);
|
|
2171
|
-
|
|
2172
|
-
if (invariant.op === '>=')
|
|
2173
|
-
violated = probability < invariant.p - eps;
|
|
2174
|
-
else if (invariant.op === '>')
|
|
2175
|
-
violated = probability <= invariant.p + eps;
|
|
2176
|
-
else if (invariant.op === '<=')
|
|
2177
|
-
violated = probability > invariant.p + eps;
|
|
2178
|
-
else
|
|
2179
|
-
violated = probability >= invariant.p - eps;
|
|
2180
|
-
if (violated) {
|
|
2407
|
+
if (probabilityViolated(invariant, probability)) {
|
|
2181
2408
|
findings.push({
|
|
2182
2409
|
code: 'A13_PROBABILITY_VIOLATION',
|
|
2183
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",
|
|
@@ -91,17 +91,22 @@
|
|
|
91
91
|
},
|
|
92
92
|
"devDependencies": {
|
|
93
93
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
94
|
-
"@deepseek-ai/dsh-agent": "^0.
|
|
95
|
-
"@deepseek-ai/dsh-cordis-host-runner": "^0.
|
|
96
|
-
"@deepseek-ai/dsh-home-paths": "^0.
|
|
97
|
-
"@deepseek-ai/dsh-
|
|
98
|
-
"@deepseek-ai/dsh-
|
|
99
|
-
"@deepseek-ai/dsh-
|
|
100
|
-
"@deepseek-ai/dsh-
|
|
101
|
-
"@deepseek-ai/dsh-
|
|
102
|
-
"@deepseek-ai/dsh-
|
|
103
|
-
"@deepseek-ai/dsh-
|
|
104
|
-
"@deepseek-ai/dsh-
|
|
94
|
+
"@deepseek-ai/dsh-agent": "^0.2.0-rc.2",
|
|
95
|
+
"@deepseek-ai/dsh-cordis-host-runner": "^0.2.0-rc.2",
|
|
96
|
+
"@deepseek-ai/dsh-home-paths": "^0.2.0-rc.2",
|
|
97
|
+
"@deepseek-ai/dsh-invariants": "^0.2.0-rc.2",
|
|
98
|
+
"@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
|
|
99
|
+
"@deepseek-ai/dsh-ptc-runtime": "^0.2.0-rc.2",
|
|
100
|
+
"@deepseek-ai/dsh-sandbox": "^0.2.0-rc.2",
|
|
101
|
+
"@deepseek-ai/dsh-sandbox-policy": "^0.2.0-rc.2",
|
|
102
|
+
"@deepseek-ai/dsh-scope": "^0.2.0-rc.2",
|
|
103
|
+
"@deepseek-ai/dsh-session": "^0.2.0-rc.2",
|
|
104
|
+
"@deepseek-ai/dsh-skill": "^0.2.0-rc.2",
|
|
105
|
+
"@deepseek-ai/dsh-skill-filesystem": "^0.2.0-rc.2",
|
|
106
|
+
"@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.2",
|
|
107
|
+
"@deepseek-ai/dsh-timeout": "^0.2.0-rc.2",
|
|
108
|
+
"@deepseek-ai/dsh-tools": "^0.2.0-rc.2",
|
|
109
|
+
"@deepseek-ai/dsh-user-approval": "^0.2.0-rc.2",
|
|
105
110
|
"@deepseek-ai/schemastery": "^3.18.4",
|
|
106
111
|
"@types/node": "^26.6.3",
|
|
107
112
|
"typescript": "^7.0.2"
|
|
@@ -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" |
|