dsh-logicprobe 0.8.0 → 0.8.1

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 建模与审查
package/lib/engine.js CHANGED
@@ -1721,31 +1721,67 @@ function S8_monotonicVariables(model) {
1721
1721
  }
1722
1722
  return checkResult('S8', 'Monotonic Variables', findings, findings.length === 0 ? 'Monotonic variables are respected' : 'Monotonic findings: ' + findings.length);
1723
1723
  }
1724
+ /**
1725
+ * Find a run from `start` that never reaches `target`, or return undefined when
1726
+ * every run does. The property is universal (see the A9 row in `SKILL.md`): one
1727
+ * branch that loops forever, or that stops before the target, refutes it.
1728
+ *
1729
+ * The search is a depth-first walk of the run graph with three colours, and target
1730
+ * runs are success leaves that are never expanded:
1731
+ *
1732
+ * - a node reached while it is on the current walk (GRAY) closes a cycle that avoids
1733
+ * the target, so some run never reaches it;
1734
+ * - a node with no outgoing step at all stops the machine where it stands, which is
1735
+ * the same violation for a different reason;
1736
+ * - a node whose walk completed without a violation is BLACK, and reaching it again
1737
+ * from another branch is a shared sub-graph, not a cycle.
1738
+ *
1739
+ * That last point is why the colour map cannot be a plain visited set: a diamond
1740
+ * (two branches rejoining) is acyclic and must pass, while a genuine cycle must not.
1741
+ * A run is identified by its state plus its variable values, so the same state id
1742
+ * with different values is a different node.
1743
+ */
1724
1744
  function findLeadsToBadPath(model, start, target) {
1725
1745
  if (start.state === target)
1726
1746
  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 });
1747
+ const GRAY = 1;
1748
+ const BLACK = 2;
1749
+ const color = new Map();
1750
+ const stack = [{ runtime: start, path: [], nexts: undefined, index: 0 }];
1751
+ color.set(runtimeKey(start), GRAY);
1752
+ while (stack.length > 0) {
1753
+ const frame = stack[stack.length - 1];
1754
+ if (frame.nexts === undefined) {
1755
+ const nexts = [];
1756
+ for (const event of allEvents(model)) {
1757
+ for (const next of stepRuntime(model, frame.runtime, event))
1758
+ nexts.push({ next, event });
1759
+ }
1760
+ frame.nexts = nexts;
1761
+ if (nexts.length === 0)
1762
+ return { path: frame.path, reason: 'Dead end before target ' + target };
1741
1763
  }
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 }] });
1764
+ if (frame.index >= frame.nexts.length) {
1765
+ color.set(runtimeKey(frame.runtime), BLACK);
1766
+ stack.pop();
1767
+ continue;
1746
1768
  }
1769
+ const { next, event } = frame.nexts[frame.index];
1770
+ frame.index += 1;
1771
+ const step = { from: frame.runtime.state, event, to: next.state };
1772
+ if (next.state === target)
1773
+ continue;
1774
+ const key = runtimeKey(next);
1775
+ const seen = color.get(key);
1776
+ if (seen === GRAY)
1777
+ return { path: [...frame.path, step], reason: 'Cycle avoids target ' + target };
1778
+ if (seen === BLACK)
1779
+ continue;
1780
+ color.set(key, GRAY);
1781
+ stack.push({ runtime: next, path: [...frame.path, step], nexts: undefined, index: 0 });
1747
1782
  }
1748
- return { path: [], reason: 'No path reaches target ' + target };
1783
+ // Every branch either reached the target or rejoined a branch that did.
1784
+ return undefined;
1749
1785
  }
1750
1786
  function A9_leadsTo(model, exploration) {
1751
1787
  const findings = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-logicprobe",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
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"
@@ -1870,28 +1870,55 @@ def S8_monotonic_variables(model):
1870
1870
 
1871
1871
 
1872
1872
  def _find_leads_to_bad_path(model, start, target):
1873
+ # Find a run from start that never reaches target, or None when every run does.
1874
+ # The property is universal (see the A9 row in SKILL.md): one branch that loops
1875
+ # forever, or that stops before the target, refutes it.
1876
+ #
1877
+ # Depth-first walk of the run graph with three colours; target runs are success
1878
+ # leaves that are never expanded. A node reached while it is on the current walk
1879
+ # (GRAY) closes a cycle that avoids the target. A node with no outgoing step at
1880
+ # all stops the machine where it stands, which is the same violation for a
1881
+ # different reason. A node whose walk completed without a violation is BLACK, and
1882
+ # reaching it again from another branch is a shared sub-graph, not a cycle -- that
1883
+ # is why the colour map cannot be a plain visited set: a diamond (two branches
1884
+ # rejoining) is acyclic and must pass, while a genuine cycle must not. A run is
1885
+ # identified by its state plus its variable values.
1873
1886
  if start['state'] == target:
1874
1887
  return None
1875
- visited = set()
1876
- queue = deque([{'runtime': start, 'path': []}])
1877
- while queue:
1878
- entry = queue.popleft()
1879
- k = _runtime_key(entry['runtime'])
1880
- if entry['runtime']['state'] == target:
1888
+ GRAY = 1
1889
+ BLACK = 2
1890
+ color = {}
1891
+ stack = [{'runtime': start, 'path': [], 'nexts': None, 'index': 0}]
1892
+ color[_runtime_key(start)] = GRAY
1893
+ while stack:
1894
+ frame = stack[-1]
1895
+ if frame['nexts'] is None:
1896
+ nexts = []
1897
+ for event in _all_events(model):
1898
+ for nxt in _step_runtime(model, frame['runtime'], event):
1899
+ nexts.append({'next': nxt, 'event': event})
1900
+ frame['nexts'] = nexts
1901
+ if not nexts:
1902
+ return {'path': frame['path'], 'reason': 'Dead end before target ' + target}
1903
+ if frame['index'] >= len(frame['nexts']):
1904
+ color[_runtime_key(frame['runtime'])] = BLACK
1905
+ stack.pop()
1881
1906
  continue
1882
- if k in visited:
1883
- return {'path': entry['path'], 'reason': 'Cycle avoids target ' + target}
1884
- visited.add(k)
1885
- nexts = []
1886
- for event in _all_events(model):
1887
- for nxt in _step_runtime(model, entry['runtime'], event):
1888
- nexts.append({'next': nxt, 'event': event})
1889
- if not nexts:
1890
- return {'path': entry['path'], 'reason': 'Dead end before target ' + target}
1891
- for item in nexts:
1892
- queue.append({'runtime': item['next'],
1893
- 'path': list(entry['path']) + [{'from': entry['runtime']['state'], 'event': item['event'], 'to': item['next']['state']}]})
1894
- return {'path': [], 'reason': 'No path reaches target ' + target}
1907
+ item = frame['nexts'][frame['index']]
1908
+ frame['index'] += 1
1909
+ step = {'from': frame['runtime']['state'], 'event': item['event'], 'to': item['next']['state']}
1910
+ if item['next']['state'] == target:
1911
+ continue
1912
+ key = _runtime_key(item['next'])
1913
+ seen = color.get(key)
1914
+ if seen == GRAY:
1915
+ return {'path': frame['path'] + [step], 'reason': 'Cycle avoids target ' + target}
1916
+ if seen == BLACK:
1917
+ continue
1918
+ color[key] = GRAY
1919
+ stack.append({'runtime': item['next'], 'path': frame['path'] + [step], 'nexts': None, 'index': 0})
1920
+ # Every branch either reached the target or rejoined a branch that did.
1921
+ return None
1895
1922
 
1896
1923
 
1897
1924
  def A9_leads_to(model, exploration):
package/src/engine.ts CHANGED
@@ -1824,26 +1824,68 @@ function S8_monotonicVariables(model: LogicModelV1): CheckResult {
1824
1824
  return checkResult('S8', 'Monotonic Variables', findings, findings.length === 0 ? 'Monotonic variables are respected' : 'Monotonic findings: ' + findings.length)
1825
1825
  }
1826
1826
 
1827
+ /**
1828
+ * Find a run from `start` that never reaches `target`, or return undefined when
1829
+ * every run does. The property is universal (see the A9 row in `SKILL.md`): one
1830
+ * branch that loops forever, or that stops before the target, refutes it.
1831
+ *
1832
+ * The search is a depth-first walk of the run graph with three colours, and target
1833
+ * runs are success leaves that are never expanded:
1834
+ *
1835
+ * - a node reached while it is on the current walk (GRAY) closes a cycle that avoids
1836
+ * the target, so some run never reaches it;
1837
+ * - a node with no outgoing step at all stops the machine where it stands, which is
1838
+ * the same violation for a different reason;
1839
+ * - a node whose walk completed without a violation is BLACK, and reaching it again
1840
+ * from another branch is a shared sub-graph, not a cycle.
1841
+ *
1842
+ * That last point is why the colour map cannot be a plain visited set: a diamond
1843
+ * (two branches rejoining) is acyclic and must pass, while a genuine cycle must not.
1844
+ * A run is identified by its state plus its variable values, so the same state id
1845
+ * with different values is a different node.
1846
+ */
1827
1847
  function findLeadsToBadPath(model: LogicModelV1, start: RuntimeState, target: string): { path: PathStep[]; reason: string } | undefined {
1828
1848
  if (start.state === target) return undefined
1829
- const visited = new Set<string>()
1830
- const queue: Array<{ runtime: RuntimeState; path: PathStep[] }> = [{ runtime: start, path: [] }]
1831
- while (queue.length > 0) {
1832
- const entry = queue.shift()!
1833
- const key = runtimeKey(entry.runtime)
1834
- if (entry.runtime.state === target) continue
1835
- if (visited.has(key)) return { path: entry.path, reason: 'Cycle avoids target ' + target }
1836
- visited.add(key)
1837
- const nexts: Array<{ next: RuntimeState; event: string }> = []
1838
- for (const event of allEvents(model)) {
1839
- for (const next of stepRuntime(model, entry.runtime, event)) nexts.push({ next, event })
1849
+ const GRAY = 1
1850
+ const BLACK = 2
1851
+ interface LeadFrame {
1852
+ runtime: RuntimeState
1853
+ path: PathStep[]
1854
+ /** Outgoing steps, filled on first expansion; `undefined` means "not expanded yet". */
1855
+ nexts: Array<{ next: RuntimeState; event: string }> | undefined
1856
+ index: number
1857
+ }
1858
+ const color = new Map<string, number>()
1859
+ const stack: LeadFrame[] = [{ runtime: start, path: [], nexts: undefined, index: 0 }]
1860
+ color.set(runtimeKey(start), GRAY)
1861
+ while (stack.length > 0) {
1862
+ const frame = stack[stack.length - 1]
1863
+ if (frame.nexts === undefined) {
1864
+ const nexts: Array<{ next: RuntimeState; event: string }> = []
1865
+ for (const event of allEvents(model)) {
1866
+ for (const next of stepRuntime(model, frame.runtime, event)) nexts.push({ next, event })
1867
+ }
1868
+ frame.nexts = nexts
1869
+ if (nexts.length === 0) return { path: frame.path, reason: 'Dead end before target ' + target }
1840
1870
  }
1841
- if (nexts.length === 0) return { path: entry.path, reason: 'Dead end before target ' + target }
1842
- for (const { next, event } of nexts) {
1843
- queue.push({ runtime: next, path: [...entry.path, { from: entry.runtime.state, event, to: next.state }] })
1871
+ if (frame.index >= frame.nexts.length) {
1872
+ color.set(runtimeKey(frame.runtime), BLACK)
1873
+ stack.pop()
1874
+ continue
1844
1875
  }
1876
+ const { next, event } = frame.nexts[frame.index]
1877
+ frame.index += 1
1878
+ const step: PathStep = { from: frame.runtime.state, event, to: next.state }
1879
+ if (next.state === target) continue
1880
+ const key = runtimeKey(next)
1881
+ const seen = color.get(key)
1882
+ if (seen === GRAY) return { path: [...frame.path, step], reason: 'Cycle avoids target ' + target }
1883
+ if (seen === BLACK) continue
1884
+ color.set(key, GRAY)
1885
+ stack.push({ runtime: next, path: [...frame.path, step], nexts: undefined, index: 0 })
1845
1886
  }
1846
- return { path: [], reason: 'No path reaches target ' + target }
1887
+ // Every branch either reached the target or rejoined a branch that did.
1888
+ return undefined
1847
1889
  }
1848
1890
 
1849
1891
  function A9_leadsTo(model: LogicModelV1, exploration: Exploration): CheckResult {