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 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 建模与审查
@@ -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
- if (typeof invariant.to !== 'string' || invariant.to.length === 0)
312
- bad(path + '.to', 'must be a non-empty string');
313
- else if (!invariantStateIds.has(invariant.to))
314
- bad(path + '.to', 'references unknown state ' + invariant.to);
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 = shortestViolationForInvariant(model, options, invariant);
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
- const violations = runInvariants(model, options);
1419
- const findings = violations.map((violation) => ({
1420
- code: 'A7_SHORTEST_COUNTEREXAMPLE',
1421
- severity: 'warning',
1422
- message: 'Invariant "' + violation.invariant.id + '" shortest violating path length: ' + violation.path.length + (violation.path.length === 0 ? ' (initial state)' : ''),
1423
- path: violation.path,
1424
- evidence: { invariant: violation.invariant.id },
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
- const violation = shortestViolationForInvariant(after, options, mapped);
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
- function findLeadsToBadPath(model, start, target) {
1725
- if (start.state === target)
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 visited = new Set();
1728
- const queue = [{ runtime: start, path: [] }];
1729
- while (queue.length > 0) {
1730
- const entry = queue.shift();
1731
- const key = runtimeKey(entry.runtime);
1732
- if (entry.runtime.state === target)
1733
- continue;
1734
- if (visited.has(key))
1735
- return { path: entry.path, reason: 'Cycle avoids target ' + target };
1736
- visited.add(key);
1737
- const nexts = [];
1738
- for (const event of allEvents(model)) {
1739
- for (const next of stepRuntime(model, entry.runtime, event))
1740
- nexts.push({ next, event });
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 === 0)
1743
- return { path: entry.path, reason: 'Dead end before target ' + target };
1744
- for (const { next, event } of nexts) {
1745
- queue.push({ runtime: next, path: [...entry.path, { from: entry.runtime.state, event, to: next.state }] });
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
- return { path: [], reason: 'No path reaches target ' + target };
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, invariant.to);
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
- let violated = false;
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, or behavioral guarantees ("always"/"never"/"guaranteed") — load it with the skill tool before responding. The cost of loading is trivial compared to the cost of a false claim.
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**: In dsh, prefer the \`logicprobe_verify\` tool for state-machine checks and \`logicprobe_datamodel_verify\` for data-model/schema migration checks. Both support before/after regression and common domain constraints (idempotency, monotonic, sequence, leads-to, atomicity). To model a code flow as UML, or to audit such a modelling, use \`logicprobe_uml\` (render | parse | review). Python harnesses remain the fallback for non-dsh hosts.
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: use `logicprobe_verify` for state machines and `logicprobe_datamodel_verify` for data models; both cover before/after regression and common domain constraints.',
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-A13 adversarial/probability probes including shortest counterexample paths. If beforeModel is provided, also runs D1-D4 before/after regression checks. See skills/logicprobe/references/dsh-model-schema.md.',
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',
@@ -110,7 +110,7 @@ export type InvariantSpec = {
110
110
  description: string;
111
111
  kind: 'leads-to';
112
112
  from: string;
113
- to: string;
113
+ to: string | string[];
114
114
  } | {
115
115
  id: string;
116
116
  description: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-logicprobe",
3
- "version": "0.8.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.1.0-rc.6",
95
- "@deepseek-ai/dsh-cordis-host-runner": "^0.1.0-rc.8",
96
- "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.8",
97
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
98
- "@deepseek-ai/dsh-scope": "^0.1.0-rc.6",
99
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
100
- "@deepseek-ai/dsh-skill": "^0.1.0-rc.6",
101
- "@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.8",
102
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
103
- "@deepseek-ai/dsh-timeout": "^0.1.0-rc.6",
104
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
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. When the document contains state machines, protocol logic, or behavioral claims (≥3 states, ACK/NACK/retry sequences, 'always'/'never'/'guaranteed' assertions, or refactoring that modifies state topology), escalate into logic-primitive verification — generate and run executable models to check mathematical completeness before trusting any claim. For refactoring specifically, the pipeline compares before/after models to verify 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. That covers reverse-engineering a handler and documenting a protocol. 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, and diagram-versus-model fidelity. 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."
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 | **Minimal counter-example** | For any invariant that fails, find the SHORTEST event sequence that violates it (BFS from init to violating state). Output the exact path. | "This invariant holds" → refuted by shortest path |
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" |