dsh-logicprobe 0.5.2 → 0.5.4

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 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-S7 structural checks and A1-A7 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.',
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',
@@ -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.2",
3
+ "version": "0.5.4",
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,59 @@
21
21
  ],
22
22
  "dsh": {
23
23
  "engines": {
24
- "dsh": ">=0.1.1-rc.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 || ^0.1.2-alpha.2",
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
+ "0.1.2-alpha.2": "compatible"
39
+ },
40
+ "profiles": [
41
+ "headless"
42
+ ]
30
43
  }
31
44
  },
32
45
  "scripts": {
33
46
  "build": "tsc -p tsconfig.json",
34
47
  "typecheck": "tsc -p tsconfig.json --noEmit",
48
+ "bump": "node scripts/bump-version.mjs",
35
49
  "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
50
  "test:full": "npm run build && node tests/full-suite.mjs"
37
51
  },
38
- "dependencies": {},
39
52
  "peerDependencies": {
40
53
  "@deepseek-ai/cordis": "^4.0.1",
41
54
  "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
42
55
  "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
43
56
  "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
44
- "@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.6",
45
- "@deepseek-ai/schemastery": "^3.18.1",
46
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
57
+ "@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.8",
58
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
59
+ "@deepseek-ai/schemastery": "^3.18.1"
47
60
  },
48
61
  "devDependencies": {
49
62
  "@deepseek-ai/cordis": "^4.0.1",
50
63
  "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
51
- "@deepseek-ai/dsh-cordis-host-runner": "^0.1.0-rc.6",
52
- "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6",
64
+ "@deepseek-ai/dsh-cordis-host-runner": "^0.1.0-rc.8",
65
+ "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.8",
53
66
  "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
54
67
  "@deepseek-ai/dsh-scope": "^0.1.0-rc.6",
55
68
  "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
56
69
  "@deepseek-ai/dsh-skill": "^0.1.0-rc.6",
57
- "@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.6",
58
- "@deepseek-ai/schemastery": "^3.18.1",
70
+ "@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.8",
71
+ "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
59
72
  "@deepseek-ai/dsh-timeout": "^0.1.0-rc.6",
60
- "@types/node": "^20.0.0",
61
- "typescript": "^5.0.0",
62
73
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
63
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6"
74
+ "@deepseek-ai/schemastery": "^3.18.1",
75
+ "@types/node": "^26.3.0",
76
+ "typescript": "^7.0.2"
64
77
  },
65
78
  "author": {
66
79
  "name": "Amethyst Luna",
@@ -81,5 +94,8 @@
81
94
  "agentskills",
82
95
  "plugin",
83
96
  "dsh"
84
- ]
97
+ ],
98
+ "engines": {
99
+ "node": ">=20"
100
+ }
85
101
  }
@@ -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 a transition table:
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
- State | Event/Condition | Next State | Guard?
224
- ------------|----------------------------|---------------|-------
225
- INIT | power_ready | IDLE | -
226
- IDLE | start_cmd | STARTING | -
227
- IDLE | error_detected | ERROR | -
228
- STARTING | ack_received | ACTIVE | -
229
- STARTING | timeout | ERROR | retry==0
230
- STARTING | timeout | FATAL | retry>=1
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
- **CRITICAL**: Show this table to the user and ask for confirmation before generating the harness. The #1 failure mode of verification is extracting the wrong model. If the plan is ambiguous, flag it as a finding first — don't guess.
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
- **Exception**: If the runtime reports `logicprobe interaction=auto`, do NOT call `ask_user_question`. Instead: (a) cite evidence for every extracted state/transition/guard, (b) round-trip the filled model back into a transition table and compare it with the extraction table, and (c) mark the report `UNCONFIRMED`.
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
 
@@ -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 extracted transition table BEFORE writing the harness. Ask:
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, which wastes more time than no verification at all.
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 has been extracted and confirmed with the user
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
 
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-S7 structural checks and A1-A7 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.',
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',