dsh-logicprobe 0.5.2 → 0.5.3
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/LICENSE +21 -21
- package/lib/engine.js +77 -0
- package/lib/tool.js +1 -1
- package/lib/types/engine.d.ts +18 -0
- package/package.json +29 -14
- package/skills/logicprobe/SKILL.md +66 -15
- package/skills/logicprobe/references/__pycache__/verification-harness.cpython-312.pyc +0 -0
- package/skills/logicprobe/references/dsh-model-schema.md +59 -0
- package/skills/logicprobe/references/logic-verification-guide.md +36 -4
- package/skills/logicprobe-datamodel/references/__pycache__/data-model-harness.cpython-312.pyc +0 -0
- package/src/engine.ts +81 -0
- package/src/tool.ts +1 -1
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Amethyst Luna
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Amethyst Luna
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/lib/engine.js
CHANGED
|
@@ -266,6 +266,82 @@ export function validateModel(input) {
|
|
|
266
266
|
bad(path + '.failEvent', 'must be a string');
|
|
267
267
|
});
|
|
268
268
|
}
|
|
269
|
+
if (root.narrative !== undefined) {
|
|
270
|
+
const narrativePath = 'narrative';
|
|
271
|
+
if (typeof root.narrative !== 'object' || root.narrative === null || Array.isArray(root.narrative)) {
|
|
272
|
+
bad(narrativePath, 'must be an object');
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
const narrative = root.narrative;
|
|
276
|
+
const stateIds = new Set(Array.isArray(root.states) ? root.states.map((state) => state.id) : []);
|
|
277
|
+
const eventIds = new Set(Array.isArray(root.transitions) ? root.transitions.map((transition) => transition.event) : []);
|
|
278
|
+
const fromEventGroups = new Set(Array.isArray(root.transitions) ? root.transitions.map((transition) => transition.from + '|' + transition.event) : []);
|
|
279
|
+
if (typeof narrative.states !== 'object' || narrative.states === null || Array.isArray(narrative.states)) {
|
|
280
|
+
bad(narrativePath + '.states', 'must be an object mapping state id -> natural-language description');
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
for (const [id, description] of Object.entries(narrative.states)) {
|
|
284
|
+
if (!stateIds.has(id))
|
|
285
|
+
bad(narrativePath + '.states', 'references unknown state ' + id);
|
|
286
|
+
if (typeof description !== 'string' || description.length === 0)
|
|
287
|
+
bad(narrativePath + '.states.' + id, 'must be a non-empty string');
|
|
288
|
+
}
|
|
289
|
+
for (const id of stateIds) {
|
|
290
|
+
if (typeof narrative.states[id] !== 'string')
|
|
291
|
+
bad(narrativePath + '.states', 'missing description for state ' + id);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (typeof narrative.events !== 'object' || narrative.events === null || Array.isArray(narrative.events)) {
|
|
295
|
+
bad(narrativePath + '.events', 'must be an object mapping event id -> natural-language description');
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
for (const [id, description] of Object.entries(narrative.events)) {
|
|
299
|
+
if (!eventIds.has(id))
|
|
300
|
+
bad(narrativePath + '.events', 'references unknown event ' + id);
|
|
301
|
+
if (typeof description !== 'string' || description.length === 0)
|
|
302
|
+
bad(narrativePath + '.events.' + id, 'must be a non-empty string');
|
|
303
|
+
}
|
|
304
|
+
for (const id of eventIds) {
|
|
305
|
+
if (typeof narrative.events[id] !== 'string')
|
|
306
|
+
bad(narrativePath + '.events', 'missing description for event ' + id);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (!Array.isArray(narrative.scenarios)) {
|
|
310
|
+
bad(narrativePath + '.scenarios', 'must be an array of { from, event, scenario }');
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
const seen = new Set();
|
|
314
|
+
narrative.scenarios.forEach((entry, index) => {
|
|
315
|
+
const scenarioPath = narrativePath + '.scenarios[' + index + ']';
|
|
316
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
317
|
+
bad(scenarioPath, 'must be an object');
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const scenario = entry;
|
|
321
|
+
if (typeof scenario.from !== 'string' || scenario.from.length === 0)
|
|
322
|
+
bad(scenarioPath + '.from', 'must be a non-empty string');
|
|
323
|
+
else if (!stateIds.has(scenario.from))
|
|
324
|
+
bad(scenarioPath + '.from', 'unknown state ' + scenario.from);
|
|
325
|
+
if (typeof scenario.event !== 'string' || scenario.event.length === 0)
|
|
326
|
+
bad(scenarioPath + '.event', 'must be a non-empty string');
|
|
327
|
+
else if (!eventIds.has(scenario.event))
|
|
328
|
+
bad(scenarioPath + '.event', 'unknown event ' + scenario.event);
|
|
329
|
+
if (typeof scenario.scenario !== 'string' || scenario.scenario.length === 0)
|
|
330
|
+
bad(scenarioPath + '.scenario', 'must be a non-empty string');
|
|
331
|
+
const key = String(scenario.from) + '|' + String(scenario.event);
|
|
332
|
+
if (seen.has(key))
|
|
333
|
+
bad(scenarioPath, 'duplicate scenario for (' + scenario.from + ', ' + scenario.event + ')');
|
|
334
|
+
seen.add(key);
|
|
335
|
+
});
|
|
336
|
+
for (const key of fromEventGroups) {
|
|
337
|
+
if (!seen.has(key)) {
|
|
338
|
+
const sep = key.indexOf('|');
|
|
339
|
+
bad(narrativePath + '.scenarios', 'missing scenario for (' + key.slice(0, sep) + ', ' + key.slice(sep + 1) + ')');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
269
345
|
// Guard/update variable references were validated structurally; re-walk for references.
|
|
270
346
|
if (Array.isArray(root.transitions)) {
|
|
271
347
|
for (const entry of root.transitions) {
|
|
@@ -1617,6 +1693,7 @@ export function runVerification(input, options = {}) {
|
|
|
1617
1693
|
truncated: exploration.truncated,
|
|
1618
1694
|
},
|
|
1619
1695
|
checks,
|
|
1696
|
+
...(model.narrative === undefined ? {} : { narrative: model.narrative }),
|
|
1620
1697
|
...(comparison === undefined ? {} : { comparison }),
|
|
1621
1698
|
};
|
|
1622
1699
|
}
|
package/lib/tool.js
CHANGED
|
@@ -11,7 +11,7 @@ export const LOGICPROBE_VERIFY_TOOL_NAME = 'logicprobe_verify';
|
|
|
11
11
|
*/
|
|
12
12
|
export const logicProbeVerifyTool = defineTool({
|
|
13
13
|
name: LOGICPROBE_VERIFY_TOOL_NAME,
|
|
14
|
-
description: 'Run executable state-machine verification (logicprobe). Takes a LogicModelV1 object with schemaVersion=1, init, states ({id, terminal?}), transitions ({from, event, to, guard?, updates?}), variables?, invariants?, concurrentPairs?, boundaryChecks?, resourcePairs?, idempotentEvents?. Guards are structured ({variable, op, value} | {all} | {any} | {not}); invariants support never-states, var-in-range, event-before-state, leads-to, sequence, and atomicity; variables support monotonic inc/dec. Returns a report with S1-
|
|
14
|
+
description: 'Run executable state-machine verification (logicprobe). Takes a LogicModelV1 object with schemaVersion=1, init, states ({id, terminal?}), transitions ({from, event, to, guard?, updates?}), variables?, invariants?, concurrentPairs?, boundaryChecks?, resourcePairs?, idempotentEvents?, narrative?. Guards are structured ({variable, op, value} | {all} | {any} | {not}); invariants support never-states, var-in-range, event-before-state, leads-to, sequence, and atomicity; 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-A11 adversarial 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
15
|
parameters: {
|
|
16
16
|
model: {
|
|
17
17
|
type: 'json',
|
package/lib/types/engine.d.ts
CHANGED
|
@@ -33,6 +33,20 @@ export interface TransitionSpec {
|
|
|
33
33
|
guard?: GuardNode;
|
|
34
34
|
updates?: UpdateSpec[];
|
|
35
35
|
}
|
|
36
|
+
export interface TransitionScenarioSpec {
|
|
37
|
+
from: string;
|
|
38
|
+
event: string;
|
|
39
|
+
/** Natural language: what this (state, event) combination represents in the real scenario. */
|
|
40
|
+
scenario: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ModelNarrative {
|
|
43
|
+
/** Natural-language meaning of each state id. */
|
|
44
|
+
states?: Record<string, string>;
|
|
45
|
+
/** Natural-language meaning of each event id. */
|
|
46
|
+
events?: Record<string, string>;
|
|
47
|
+
/** Natural-language scenario for each distinct (from, event) combination. */
|
|
48
|
+
scenarios?: TransitionScenarioSpec[];
|
|
49
|
+
}
|
|
36
50
|
export interface VariableSpec {
|
|
37
51
|
name: string;
|
|
38
52
|
kind: 'integer' | 'boolean';
|
|
@@ -99,6 +113,8 @@ export interface LogicModelV1 {
|
|
|
99
113
|
boundaryChecks?: BoundaryCheckSpec[];
|
|
100
114
|
resourcePairs?: ResourcePairSpec[];
|
|
101
115
|
idempotentEvents?: string[];
|
|
116
|
+
/** Natural-language descriptions of states, events, and (state, event) scenarios. */
|
|
117
|
+
narrative?: ModelNarrative;
|
|
102
118
|
}
|
|
103
119
|
export interface VerificationOptions {
|
|
104
120
|
maxStates?: number;
|
|
@@ -129,6 +145,8 @@ export interface VerificationReport {
|
|
|
129
145
|
ok: boolean;
|
|
130
146
|
schemaVersion: 1;
|
|
131
147
|
modelHash: string;
|
|
148
|
+
/** Echo of the model's natural-language narrative, when present. */
|
|
149
|
+
narrative?: ModelNarrative;
|
|
132
150
|
summary: {
|
|
133
151
|
states: number;
|
|
134
152
|
transitions: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-logicprobe",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Design document & plan claim verification — enumerate claims, verify against codebase facts, then escalate to state-machine verification (S1-S8/A1-A11) 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",
|
|
@@ -21,46 +21,58 @@
|
|
|
21
21
|
],
|
|
22
22
|
"dsh": {
|
|
23
23
|
"engines": {
|
|
24
|
-
"dsh": ">=0.1.
|
|
24
|
+
"dsh": ">=0.1.0-rc.7"
|
|
25
25
|
},
|
|
26
26
|
"category": "skill",
|
|
27
27
|
"displayName": "逻辑探针",
|
|
28
28
|
"bundle": {
|
|
29
29
|
"patch": "./cordis.patch.yml"
|
|
30
|
+
},
|
|
31
|
+
"compatibility": {
|
|
32
|
+
"dsh": "^0.1.0-rc.7 || ^0.1.1-rc.1",
|
|
33
|
+
"dshReleases": {
|
|
34
|
+
"0.1.0-rc.7": "compatible",
|
|
35
|
+
"0.1.0-rc.8": "compatible",
|
|
36
|
+
"0.1.1-rc.1": "compatible",
|
|
37
|
+
"0.1.1-rc.2": "compatible"
|
|
38
|
+
},
|
|
39
|
+
"profiles": [
|
|
40
|
+
"headless"
|
|
41
|
+
]
|
|
30
42
|
}
|
|
31
43
|
},
|
|
32
44
|
"scripts": {
|
|
33
45
|
"build": "tsc -p tsconfig.json",
|
|
34
46
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
47
|
+
"bump": "node scripts/bump-version.mjs",
|
|
35
48
|
"test:engine": "npm run build && node tests/engine/run.mjs && node tests/data-engine/run.mjs && node tests/concurrency/run.mjs && node tests/apply-smoke.mjs",
|
|
36
49
|
"test:full": "npm run build && node tests/full-suite.mjs"
|
|
37
50
|
},
|
|
38
|
-
"dependencies": {},
|
|
39
51
|
"peerDependencies": {
|
|
40
52
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
41
53
|
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
|
42
54
|
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
43
55
|
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
44
|
-
"@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.
|
|
45
|
-
"@deepseek-ai/
|
|
46
|
-
"@deepseek-ai/
|
|
56
|
+
"@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.8",
|
|
57
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
58
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
47
59
|
},
|
|
48
60
|
"devDependencies": {
|
|
49
61
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
50
62
|
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
|
51
|
-
"@deepseek-ai/dsh-cordis-host-runner": "^0.1.0-rc.
|
|
52
|
-
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.
|
|
63
|
+
"@deepseek-ai/dsh-cordis-host-runner": "^0.1.0-rc.8",
|
|
64
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.8",
|
|
53
65
|
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
54
66
|
"@deepseek-ai/dsh-scope": "^0.1.0-rc.6",
|
|
55
67
|
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
56
68
|
"@deepseek-ai/dsh-skill": "^0.1.0-rc.6",
|
|
57
|
-
"@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.
|
|
58
|
-
"@deepseek-ai/
|
|
69
|
+
"@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.8",
|
|
70
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
59
71
|
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.6",
|
|
60
|
-
"@types/node": "^20.0.0",
|
|
61
|
-
"typescript": "^5.0.0",
|
|
62
72
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
63
|
-
"@deepseek-ai/
|
|
73
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
74
|
+
"@types/node": "^26.3.0",
|
|
75
|
+
"typescript": "^7.0.2"
|
|
64
76
|
},
|
|
65
77
|
"author": {
|
|
66
78
|
"name": "Amethyst Luna",
|
|
@@ -81,5 +93,8 @@
|
|
|
81
93
|
"agentskills",
|
|
82
94
|
"plugin",
|
|
83
95
|
"dsh"
|
|
84
|
-
]
|
|
96
|
+
],
|
|
97
|
+
"engines": {
|
|
98
|
+
"node": ">=20"
|
|
99
|
+
}
|
|
85
100
|
}
|
|
@@ -147,7 +147,7 @@ When the document under review is a refactoring plan (modifying existing state m
|
|
|
147
147
|
|
|
148
148
|
1. **Extract the BEFORE model** from the existing codebase (not the plan — verify what the code actually does, not what the plan claims it does)
|
|
149
149
|
2. **Extract the AFTER model** from the refactoring plan
|
|
150
|
-
3. **Show both tables** to the user side by side and confirm the delta is intentional
|
|
150
|
+
3. **Show both tables AND their model narratives** to the user side by side and confirm the delta is intentional
|
|
151
151
|
4. **Run Phase 2a + 2b on the AFTER model** — same 14 checks as new design
|
|
152
152
|
5. **Compare BEFORE vs AFTER**:
|
|
153
153
|
|
|
@@ -217,25 +217,76 @@ For every probe failure:
|
|
|
217
217
|
|
|
218
218
|
### Extraction Rule
|
|
219
219
|
|
|
220
|
-
Before writing any verification code, output
|
|
220
|
+
Before writing any verification code, output the model in one of the rendering
|
|
221
|
+
forms below. Default is Form A; switch forms when the machine is large or the
|
|
222
|
+
display area is narrow — never let a table row wrap.
|
|
223
|
+
|
|
224
|
+
**Form A — integrated transition table (default)**: formal symbols with their
|
|
225
|
+
natural-language meaning inlined; the scenario each (state, event) combination
|
|
226
|
+
represents is the last column:
|
|
221
227
|
|
|
222
228
|
```text
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
INIT
|
|
226
|
-
IDLE
|
|
227
|
-
|
|
228
|
-
STARTING
|
|
229
|
-
STARTING
|
|
230
|
-
|
|
231
|
-
ACTIVE | done | IDLE | -
|
|
232
|
-
ERROR | cooldown_elapsed | RECOVERING | -
|
|
233
|
-
RECOVERING | reinit_complete | IDLE | -
|
|
229
|
+
状态(含义) | 事件(含义) | 下一状态(含义) | Guard? | 场景(状态+事件)
|
|
230
|
+
------------------------|-----------------------|----------------------|---------|--------------------
|
|
231
|
+
INIT(上电待就绪) | power_ready(电源就绪)| IDLE(空闲待命) | - | 就绪后进入待命
|
|
232
|
+
IDLE(空闲待命) | start_cmd(启动命令) | STARTING(启动中) | - | 收到命令开始启动
|
|
233
|
+
STARTING(启动中) | ack_received(收到ACK)| ACTIVE(运行中) | - | 启动成功进入运行
|
|
234
|
+
STARTING(启动中) | timeout(等待超时) | ERROR(出错待恢复) | retry==0 | 首次超时进入重试
|
|
235
|
+
STARTING(启动中) | timeout(等待超时) | FATAL(不可恢复) | retry>=1 | 重试耗尽转致命
|
|
236
|
+
ERROR(出错待恢复) | cooldown_elapsed(冷却结束)| RECOVERING(重初始化)| - | 冷却结束开始恢复
|
|
234
237
|
```
|
|
235
238
|
|
|
236
|
-
|
|
239
|
+
Width discipline (Form A): keep parenthetical meanings short (state/event ≤ 6
|
|
240
|
+
characters, scenario ≤ 10) and estimate every row's display width (CJK counts as
|
|
241
|
+
2) to fit the available area. If any row would wrap, shorten the meanings; if it
|
|
242
|
+
still will not fit, switch to Form C. A wrapped table row loses column alignment
|
|
243
|
+
and is harder to read than no table at all.
|
|
244
|
+
|
|
245
|
+
**Form B — sentence blocks (reading-accessible)**: one transition per block,
|
|
246
|
+
scenario sentence first, fixed three-line frame. Use for detailed confirmation,
|
|
247
|
+
users with reading difficulties, or machines with ≤ 10 transitions:
|
|
248
|
+
|
|
249
|
+
```text
|
|
250
|
+
第 1 步:就绪后进入待命
|
|
251
|
+
状态 INIT(上电待就绪)
|
|
252
|
+
发生 power_ready(电源就绪)
|
|
253
|
+
进入 IDLE(空闲待命)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Form C — grouped by source state (large machines / narrow panes)**: one
|
|
257
|
+
section per state, each rendered as its own small 3-column table
|
|
258
|
+
(事件(含义)| 下一状态(含义)| 场景); no cross-group column alignment to track:
|
|
259
|
+
|
|
260
|
+
```text
|
|
261
|
+
DEGRADED_LOADING(降级加载)
|
|
262
|
+
事件(含义) | 下一状态(含义) | 场景
|
|
263
|
+
first_data(首帧数据)| OK_READY(正常就绪) | 收到首帧转就绪
|
|
264
|
+
fault(故障) | FAULT(故障锁存) | 故障锁存
|
|
265
|
+
power_off(下电) | POWER_OFF(下电) | 下电
|
|
266
|
+
```
|
|
237
267
|
|
|
238
|
-
|
|
268
|
+
Each group table is narrow (3 columns, roughly ≤ 60 display columns with short
|
|
269
|
+
meanings). If one group's table would still wrap, fall back to a one-line-per-event
|
|
270
|
+
bullet list for that group only.
|
|
271
|
+
|
|
272
|
+
Reading rule: every row/block is one sentence — "在【状态(含义)】下发生【事件(含义)】
|
|
273
|
+
→ 进入【下一状态(含义)】,即【实际场景】"。Meanings repeat on purpose: each entry is
|
|
274
|
+
self-contained. The machine-readable model keeps the same information in its
|
|
275
|
+
`narrative` block (`narrative.states`, `narrative.events`, `narrative.scenarios`).
|
|
276
|
+
|
|
277
|
+
**CRITICAL**: Show the chosen form to the user and ask for confirmation before
|
|
278
|
+
generating the harness. Because the natural language is inlined, any entry that
|
|
279
|
+
reads wrong to the user — a state/event meaning that is off, or a scenario that
|
|
280
|
+
does not match reality — means the model is wrong even if the symbols are
|
|
281
|
+
consistent. The #1 failure mode of verification is extracting the wrong model. If
|
|
282
|
+
the plan is ambiguous, flag it as a finding first — don't guess.
|
|
283
|
+
|
|
284
|
+
**Exception**: If the runtime reports `logicprobe interaction=auto`, do NOT call
|
|
285
|
+
`ask_user_question`. Instead: (a) cite evidence for every cell/entry — each
|
|
286
|
+
state/event meaning and each scenario must trace to a source sentence, (b)
|
|
287
|
+
round-trip the filled model (including its `narrative` block) back into the SAME
|
|
288
|
+
rendering form and compare it with the extraction, and (c) mark the report
|
|
289
|
+
`UNCONFIRMED`.
|
|
239
290
|
|
|
240
291
|
### Code-Level Behavioral Suggestion
|
|
241
292
|
|
|
Binary file
|
|
@@ -33,6 +33,65 @@ The dsh-native `logicprobe_verify` tool accepts a structured JSON model. The eng
|
|
|
33
33
|
| `boundaryChecks` | no | `{ variable, values: number[] }` for A5 |
|
|
34
34
|
| `resourcePairs` | no | `{ resource, acquireEvent, releaseEvent, failEvent? }` for A4/A6 |
|
|
35
35
|
| `idempotentEvents` | no | Events that must be replay-safe; verified by A8 |
|
|
36
|
+
| `narrative` | no | Natural-language descriptions of states, events, and (state, event) scenarios — echoed in the report |
|
|
37
|
+
|
|
38
|
+
## Model narrative (natural-language context)
|
|
39
|
+
|
|
40
|
+
The model may carry a `narrative` block explaining, in natural language, what
|
|
41
|
+
every symbol means in the real scenario. It is what gets shown to the user when
|
|
42
|
+
the extracted model is presented for confirmation, and the report echoes it back
|
|
43
|
+
so findings can be read against real scenarios instead of bare ids.
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
"narrative": {
|
|
47
|
+
"states": {
|
|
48
|
+
"NEW": "订单已创建,等待支付",
|
|
49
|
+
"PAID": "已支付,等待发货",
|
|
50
|
+
"SHIPPED": "已发货,等待签收",
|
|
51
|
+
"DONE": "已完成(终态)",
|
|
52
|
+
"CANCELLED": "已取消(终态)"
|
|
53
|
+
},
|
|
54
|
+
"events": {
|
|
55
|
+
"pay": "买家完成支付",
|
|
56
|
+
"ship": "仓库发货",
|
|
57
|
+
"deliver": "买家签收",
|
|
58
|
+
"cancel": "取消订单"
|
|
59
|
+
},
|
|
60
|
+
"scenarios": [
|
|
61
|
+
{ "from": "NEW", "event": "pay", "scenario": "下单后支付成功,订单进入待发货" },
|
|
62
|
+
{ "from": "PAID", "event": "ship", "scenario": "已支付订单发货,进入运输中" },
|
|
63
|
+
{ "from": "SHIPPED", "event": "deliver", "scenario": "签收完成,订单结束" },
|
|
64
|
+
{ "from": "NEW", "event": "cancel", "scenario": "未支付订单被取消" },
|
|
65
|
+
{ "from": "PAID", "event": "cancel", "scenario": "已支付订单取消并退款" }
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Completeness contract**: when `narrative` is present, all three parts are
|
|
71
|
+
required and must fully cover the model — every declared state needs a
|
|
72
|
+
`narrative.states` entry, every event used in `transitions` needs a
|
|
73
|
+
`narrative.events` entry, and every distinct `(from, event)` group needs a
|
|
74
|
+
`narrative.scenarios` entry. Keys must reference declared ids; unknown
|
|
75
|
+
references, missing coverage, and duplicate scenario keys are model validation
|
|
76
|
+
errors. The report's `narrative` field echoes the block unchanged.
|
|
77
|
+
|
|
78
|
+
**Presenting the model**: when showing the extracted model for confirmation, render
|
|
79
|
+
the natural language INLINE in the model presentation, not as a separate block.
|
|
80
|
+
Three rendering forms (all derive from the same `narrative` data):
|
|
81
|
+
|
|
82
|
+
- **Form A — integrated transition table (default)**: one row per transition with
|
|
83
|
+
state/event meanings in parentheses and the scenario as the last column. Keep
|
|
84
|
+
meanings short (state/event ≤ 6 characters, scenario ≤ 10) and estimate row
|
|
85
|
+
width (CJK counts as 2) so rows fit the display area — a wrapped row loses
|
|
86
|
+
column alignment and readability collapses.
|
|
87
|
+
- **Form B — sentence blocks (reading-accessible)**: scenario sentence first, then
|
|
88
|
+
a fixed three-line frame (状态…/发生…/进入…). Use for detailed confirmation,
|
|
89
|
+
users with reading difficulties, or ≤ 10 transitions.
|
|
90
|
+
- **Form C — grouped by source state (large machines / narrow panes)**: one
|
|
91
|
+
section per state, each rendered as its own small 3-column table
|
|
92
|
+
(`event(含义)| NEXT(含义)| 场景`); no cross-group column alignment to
|
|
93
|
+
track. Use for ≥ 15 transitions or narrow display areas; if a group table would
|
|
94
|
+
still wrap, fall back to a one-line-per-event bullet list for that group.
|
|
36
95
|
|
|
37
96
|
## Guards
|
|
38
97
|
|
|
@@ -45,16 +45,48 @@ Extract every claim that uses absolute language:
|
|
|
45
45
|
| "cannot ..." | "cannot deadlock" | No absorbing cycle exists |
|
|
46
46
|
| "all paths ..." | "all paths lead to ERROR on failure" | Every failure event reaches ERROR (not FATAL, not stuck) |
|
|
47
47
|
|
|
48
|
+
### Step 4.5: Write the Natural Language and Pick a Rendering Form
|
|
49
|
+
|
|
50
|
+
Do NOT keep the natural-language explanations in a separate block — integrate them
|
|
51
|
+
into the model presentation so every entry reads like a sentence in the real
|
|
52
|
+
scenario. Write meanings for every state, every event, and every distinct
|
|
53
|
+
(state, event) combination (carry them in the model's `narrative` block:
|
|
54
|
+
`narrative.states`, `narrative.events`, `narrative.scenarios`; the report
|
|
55
|
+
echoes them back). Then render one of three forms:
|
|
56
|
+
|
|
57
|
+
**Form A — integrated transition table (default)**: `STATE(含义)` /
|
|
58
|
+
`event(含义)` / `NEXT(含义)` columns plus a final scenario column, so each
|
|
59
|
+
row is one self-contained sentence. Width discipline: keep meanings short
|
|
60
|
+
(state/event ≤ 6 characters, scenario ≤ 10) and estimate row width (CJK counts
|
|
61
|
+
as 2) to fit the display area — a wrapped row loses column alignment and
|
|
62
|
+
readability collapses. If a row would wrap, shorten meanings; if it still will
|
|
63
|
+
not fit, use Form C.
|
|
64
|
+
|
|
65
|
+
**Form B — sentence blocks (reading-accessible)**: one transition per block,
|
|
66
|
+
scenario sentence first, then a fixed three-line frame (状态…/发生…/进入…). Use
|
|
67
|
+
for detailed confirmation, users with reading difficulties, or ≤ 10 transitions.
|
|
68
|
+
|
|
69
|
+
**Form C — grouped by source state (large machines / narrow panes)**: one
|
|
70
|
+
section per state, each rendered as its own small 3-column table
|
|
71
|
+
(`event(含义)| NEXT(含义)| 场景`); no cross-group column alignment to track.
|
|
72
|
+
Use for ≥ 15 transitions or when the display area is too narrow for Form A. If one
|
|
73
|
+
group's table would still wrap, fall back to a one-line-per-event bullet list for
|
|
74
|
+
that group only.
|
|
75
|
+
|
|
48
76
|
### Step 5: Confirm with User
|
|
49
77
|
|
|
50
|
-
Show the
|
|
78
|
+
Show the rendered model (the chosen form, symbols + inlined natural language)
|
|
79
|
+
BEFORE writing the harness. Ask:
|
|
51
80
|
|
|
52
81
|
1. Are all states captured?
|
|
53
82
|
2. Are all transitions and guards correct?
|
|
54
83
|
3. Are there undocumented transitions not in the plan?
|
|
55
84
|
4. Is the initial state correct?
|
|
85
|
+
5. Does every entry read correctly — state/event meanings and the (state, event)
|
|
86
|
+
scenario all match the real situation?
|
|
56
87
|
|
|
57
|
-
Only proceed after confirmation. A wrong model produces wrong counter-examples,
|
|
88
|
+
Only proceed after confirmation. A wrong model produces wrong counter-examples,
|
|
89
|
+
which wastes more time than no verification at all.
|
|
58
90
|
|
|
59
91
|
## Probe Design Patterns
|
|
60
92
|
|
|
@@ -348,7 +380,7 @@ The BEFORE model comes from **code, not the plan**. The plan may describe the cu
|
|
|
348
380
|
1. Read the relevant source files (state machine dispatch, state enum, handler functions)
|
|
349
381
|
2. Extract the ACTUAL transition logic from code, not from the plan's description of "current behavior"
|
|
350
382
|
3. Extract the AFTER model from the plan as usual
|
|
351
|
-
4. Display both tables side by side
|
|
383
|
+
4. Display both tables AND their model narratives side by side
|
|
352
384
|
|
|
353
385
|
### Comparison Methodology
|
|
354
386
|
|
|
@@ -389,7 +421,7 @@ Manual verification is reliable for state machines with **≤ 10 states and ≤
|
|
|
389
421
|
|
|
390
422
|
### Prerequisites
|
|
391
423
|
|
|
392
|
-
- Transition table
|
|
424
|
+
- Transition table and model narrative have been extracted and confirmed with the user
|
|
393
425
|
- You have the full table in context (from Phase 2 extraction step)
|
|
394
426
|
- **Harness validation** (Python mode only): After filling in `verification-harness.py`, translate the Python `STATES` dict BACK into a transition table and compare it against the confirmed extraction table. If they differ, fix the harness. This catches typo and whitespace errors in manual dict construction.
|
|
395
427
|
|
|
Binary file
|
package/src/engine.ts
CHANGED
|
@@ -46,6 +46,22 @@ export interface TransitionSpec {
|
|
|
46
46
|
updates?: UpdateSpec[]
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export interface TransitionScenarioSpec {
|
|
50
|
+
from: string
|
|
51
|
+
event: string
|
|
52
|
+
/** Natural language: what this (state, event) combination represents in the real scenario. */
|
|
53
|
+
scenario: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ModelNarrative {
|
|
57
|
+
/** Natural-language meaning of each state id. */
|
|
58
|
+
states?: Record<string, string>
|
|
59
|
+
/** Natural-language meaning of each event id. */
|
|
60
|
+
events?: Record<string, string>
|
|
61
|
+
/** Natural-language scenario for each distinct (from, event) combination. */
|
|
62
|
+
scenarios?: TransitionScenarioSpec[]
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
export interface VariableSpec {
|
|
50
66
|
name: string
|
|
51
67
|
kind: 'integer' | 'boolean'
|
|
@@ -86,6 +102,8 @@ export interface LogicModelV1 {
|
|
|
86
102
|
boundaryChecks?: BoundaryCheckSpec[]
|
|
87
103
|
resourcePairs?: ResourcePairSpec[]
|
|
88
104
|
idempotentEvents?: string[]
|
|
105
|
+
/** Natural-language descriptions of states, events, and (state, event) scenarios. */
|
|
106
|
+
narrative?: ModelNarrative
|
|
89
107
|
}
|
|
90
108
|
|
|
91
109
|
export interface VerificationOptions {
|
|
@@ -121,6 +139,8 @@ export interface VerificationReport {
|
|
|
121
139
|
ok: boolean
|
|
122
140
|
schemaVersion: 1
|
|
123
141
|
modelHash: string
|
|
142
|
+
/** Echo of the model's natural-language narrative, when present. */
|
|
143
|
+
narrative?: ModelNarrative
|
|
124
144
|
summary: {
|
|
125
145
|
states: number
|
|
126
146
|
transitions: number
|
|
@@ -347,6 +367,66 @@ export function validateModel(input: unknown): { ok: true; model: LogicModelV1 }
|
|
|
347
367
|
if (pair.failEvent !== undefined && typeof pair.failEvent !== 'string') bad(path + '.failEvent', 'must be a string')
|
|
348
368
|
})
|
|
349
369
|
}
|
|
370
|
+
if (root.narrative !== undefined) {
|
|
371
|
+
const narrativePath = 'narrative'
|
|
372
|
+
if (typeof root.narrative !== 'object' || root.narrative === null || Array.isArray(root.narrative)) {
|
|
373
|
+
bad(narrativePath, 'must be an object')
|
|
374
|
+
} else {
|
|
375
|
+
const narrative = root.narrative as Record<string, unknown>
|
|
376
|
+
const stateIds = new Set<string>(Array.isArray(root.states) ? (root.states as StateSpec[]).map((state) => state.id) : [])
|
|
377
|
+
const eventIds = new Set<string>(Array.isArray(root.transitions) ? (root.transitions as TransitionSpec[]).map((transition) => transition.event) : [])
|
|
378
|
+
const fromEventGroups = new Set<string>(Array.isArray(root.transitions) ? (root.transitions as TransitionSpec[]).map((transition) => transition.from + '|' + transition.event) : [])
|
|
379
|
+
if (typeof narrative.states !== 'object' || narrative.states === null || Array.isArray(narrative.states)) {
|
|
380
|
+
bad(narrativePath + '.states', 'must be an object mapping state id -> natural-language description')
|
|
381
|
+
} else {
|
|
382
|
+
for (const [id, description] of Object.entries(narrative.states as Record<string, unknown>)) {
|
|
383
|
+
if (!stateIds.has(id)) bad(narrativePath + '.states', 'references unknown state ' + id)
|
|
384
|
+
if (typeof description !== 'string' || description.length === 0) bad(narrativePath + '.states.' + id, 'must be a non-empty string')
|
|
385
|
+
}
|
|
386
|
+
for (const id of stateIds) {
|
|
387
|
+
if (typeof (narrative.states as Record<string, unknown>)[id] !== 'string') bad(narrativePath + '.states', 'missing description for state ' + id)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (typeof narrative.events !== 'object' || narrative.events === null || Array.isArray(narrative.events)) {
|
|
391
|
+
bad(narrativePath + '.events', 'must be an object mapping event id -> natural-language description')
|
|
392
|
+
} else {
|
|
393
|
+
for (const [id, description] of Object.entries(narrative.events as Record<string, unknown>)) {
|
|
394
|
+
if (!eventIds.has(id)) bad(narrativePath + '.events', 'references unknown event ' + id)
|
|
395
|
+
if (typeof description !== 'string' || description.length === 0) bad(narrativePath + '.events.' + id, 'must be a non-empty string')
|
|
396
|
+
}
|
|
397
|
+
for (const id of eventIds) {
|
|
398
|
+
if (typeof (narrative.events as Record<string, unknown>)[id] !== 'string') bad(narrativePath + '.events', 'missing description for event ' + id)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (!Array.isArray(narrative.scenarios)) {
|
|
402
|
+
bad(narrativePath + '.scenarios', 'must be an array of { from, event, scenario }')
|
|
403
|
+
} else {
|
|
404
|
+
const seen = new Set<string>()
|
|
405
|
+
narrative.scenarios.forEach((entry, index) => {
|
|
406
|
+
const scenarioPath = narrativePath + '.scenarios[' + index + ']'
|
|
407
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
408
|
+
bad(scenarioPath, 'must be an object')
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
const scenario = entry as Record<string, unknown>
|
|
412
|
+
if (typeof scenario.from !== 'string' || scenario.from.length === 0) bad(scenarioPath + '.from', 'must be a non-empty string')
|
|
413
|
+
else if (!stateIds.has(scenario.from)) bad(scenarioPath + '.from', 'unknown state ' + scenario.from)
|
|
414
|
+
if (typeof scenario.event !== 'string' || scenario.event.length === 0) bad(scenarioPath + '.event', 'must be a non-empty string')
|
|
415
|
+
else if (!eventIds.has(scenario.event)) bad(scenarioPath + '.event', 'unknown event ' + scenario.event)
|
|
416
|
+
if (typeof scenario.scenario !== 'string' || scenario.scenario.length === 0) bad(scenarioPath + '.scenario', 'must be a non-empty string')
|
|
417
|
+
const key = String(scenario.from) + '|' + String(scenario.event)
|
|
418
|
+
if (seen.has(key)) bad(scenarioPath, 'duplicate scenario for (' + scenario.from + ', ' + scenario.event + ')')
|
|
419
|
+
seen.add(key)
|
|
420
|
+
})
|
|
421
|
+
for (const key of fromEventGroups) {
|
|
422
|
+
if (!seen.has(key)) {
|
|
423
|
+
const sep = key.indexOf('|')
|
|
424
|
+
bad(narrativePath + '.scenarios', 'missing scenario for (' + key.slice(0, sep) + ', ' + key.slice(sep + 1) + ')')
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
350
430
|
// Guard/update variable references were validated structurally; re-walk for references.
|
|
351
431
|
if (Array.isArray(root.transitions)) {
|
|
352
432
|
for (const entry of root.transitions as TransitionSpec[]) {
|
|
@@ -1692,6 +1772,7 @@ export function runVerification(input: unknown, options: VerificationOptions = {
|
|
|
1692
1772
|
truncated: exploration.truncated,
|
|
1693
1773
|
},
|
|
1694
1774
|
checks,
|
|
1775
|
+
...(model.narrative === undefined ? {} : { narrative: model.narrative }),
|
|
1695
1776
|
...(comparison === undefined ? {} : { comparison }),
|
|
1696
1777
|
}
|
|
1697
1778
|
}
|
package/src/tool.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const LOGICPROBE_VERIFY_TOOL_NAME = 'logicprobe_verify'
|
|
|
14
14
|
export const logicProbeVerifyTool = defineTool({
|
|
15
15
|
name: LOGICPROBE_VERIFY_TOOL_NAME,
|
|
16
16
|
description:
|
|
17
|
-
'Run executable state-machine verification (logicprobe). Takes a LogicModelV1 object with schemaVersion=1, init, states ({id, terminal?}), transitions ({from, event, to, guard?, updates?}), variables?, invariants?, concurrentPairs?, boundaryChecks?, resourcePairs?, idempotentEvents?. Guards are structured ({variable, op, value} | {all} | {any} | {not}); invariants support never-states, var-in-range, event-before-state, leads-to, sequence, and atomicity; variables support monotonic inc/dec. Returns a report with S1-
|
|
17
|
+
'Run executable state-machine verification (logicprobe). Takes a LogicModelV1 object with schemaVersion=1, init, states ({id, terminal?}), transitions ({from, event, to, guard?, updates?}), variables?, invariants?, concurrentPairs?, boundaryChecks?, resourcePairs?, idempotentEvents?, narrative?. Guards are structured ({variable, op, value} | {all} | {any} | {not}); invariants support never-states, var-in-range, event-before-state, leads-to, sequence, and atomicity; 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-A11 adversarial 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.',
|
|
18
18
|
parameters: {
|
|
19
19
|
model: {
|
|
20
20
|
type: 'json',
|