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 +8 -0
- package/README.md +8 -0
- package/lib/engine.js +55 -19
- package/package.json +17 -12
- package/skills/logicprobe/references/logicprobe-engine.py +46 -19
- package/src/engine.ts +57 -15
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
|
|
1728
|
-
const
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
if (
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
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
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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"
|
|
@@ -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
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
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
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
if
|
|
1890
|
-
return {'path':
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
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
|
|
1830
|
-
const
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
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
|
|
1842
|
-
|
|
1843
|
-
|
|
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
|
-
|
|
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 {
|