dsh-logicprobe 0.6.8 → 0.7.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.
@@ -1,266 +1,322 @@
1
- # DSH Model Schema v1 — logicprobe_verify
2
-
3
- The dsh-native `logicprobe_verify` tool accepts a structured JSON model. The engine runs 22 checks (S1-S8 structural, A1-A14) and returns a JSON report. When `beforeModel` is supplied, it also runs D1-D4 before/after regression checks. Guards and updates are structured data — no code strings, no arbitrary execution.
4
-
5
- ## Top-level model
6
-
7
- ```json
8
- {
9
- "schemaVersion": 1,
10
- "init": "INIT",
11
- "states": [{ "id": "INIT" }, { "id": "ACTIVE", "terminal": true }],
12
- "transitions": [
13
- { "from": "INIT", "event": "go", "to": "ACTIVE" }
14
- ],
15
- "variables": [],
16
- "invariants": [],
17
- "concurrentPairs": [],
18
- "boundaryChecks": [],
19
- "resourcePairs": [],
20
- "idempotentEvents": []
21
- }
22
- ```
23
-
24
- | Field | Required | Meaning |
25
- |---|---|---|
26
- | `schemaVersion` | yes | Must be `1` |
27
- | `init` | yes | Initial state id |
28
- | `states` | yes | `{ id, terminal?, onEntry?, onExit?, maxTicks? }`; `terminal` exempts S2/S3/S5/A1; `onEntry`/`onExit` are action-name lists fired on entry/exit and treated by A4 as implicit acquire/release; `maxTicks` is an A14 deadline since entry (needs top-level `tickEvents`) |
29
- | `transitions` | yes | `{ from, event, to, guard?, updates?, cost?, weight? }`; `cost` (non-negative, absent = 1) is checked by A12 against `budget` invariants; `weight` (non-negative, absent = 1) makes the machine a DTMC under A13 `probability` invariants |
30
- | `variables` | no | `{ name, kind: integer\|boolean, init, min?, max?, monotonic? }` |
31
- | `invariants` | no | See invariant kinds below |
32
- | `concurrentPairs` | no | `["eventA", "eventB"]` pairs for A2 |
33
- | `boundaryChecks` | no | `{ variable, values: number[] }` for A5 |
34
- | `resourcePairs` | no | `{ resource, acquireEvent, releaseEvent, failEvent? }` for A4/A6 |
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.
95
-
96
- ## Guards
97
-
98
- A guard is exactly one of:
99
-
100
- ```json
101
- { "variable": "retry", "op": "<", "value": 3 }
102
- { "all": [ { "variable": "armed", "op": "==", "value": true }, { "variable": "retry", "op": ">", "value": 0 } ] }
103
- { "any": [ { "variable": "mode", "op": "==", "value": 1 }, { "variable": "mode", "op": "==", "value": 2 } ] }
104
- { "not": { "variable": "locked", "op": "==", "value": true } }
105
- ```
106
-
107
- - Boolean variables only support `==` / `!=`.
108
- - A transition with **no guard** is the default/else branch for its `(from, event)` group. It fires only when no guarded branch in that group is true.
109
- - Multiple true guards in one `(from, event)` group are reported as S4 nondeterminism; a group with guards and no default must be exhaustive or S6 flags the missing branch.
110
-
111
- ## Updates
112
-
113
- ```json
114
- { "variable": "retry", "op": "inc", "value": 1 }
115
- { "variable": "retry", "op": "set", "value": 0 }
116
- { "variable": "retry", "op": "dec", "value": 1 }
117
- ```
118
-
119
- `inc`/`dec` default to 1 when `value` is omitted. `set` defaults to 0.
120
-
121
- ## Invariants
122
-
123
- | Kind | Shape | Checks |
124
- |---|---|---|
125
- | `never-states` | `{ states: ["ERROR"] }` | No reachable runtime state may be in the forbidden set |
126
- | `var-in-range` | `{ variable, min?, max? }` | Every reachable runtime state keeps the variable in range |
127
- | `event-before-state` | `{ event: "power_ready", state: "ACTIVE" }` | Every path entering `state` must have passed through `event` first |
128
- | `leads-to` | `{ from: "MIGRATING", to: "DONE" }` | Every path from `from` must eventually reach `to` |
129
- | `sequence` | `{ events: ["backup", "modify", "commit"] }` | Events must occur in the given order |
130
- | `atomicity` | `{ events: ["write"], commit: "commit", rollback?: "rollback" }` | Atomic group must end with commit/rollback before leaving scope |
131
- | `budget` | `{ budget: n }` | No reachable path may accumulate transition cost greater than n (A12). Costs are non-negative; a transition without `cost` counts 1, so legacy machines keep step-count semantics |
132
- | `probability` | `{ target, op: one of >= <= > <, p }` | P(ever hitting target) must satisfy the bound (A13, DTMC from transition `weight`, default 1; value iteration) |
133
-
134
- A7 reports the shortest violating path for each failed invariant. An empty path means the initial state already violates it.
135
-
136
- ## Permission presets and interaction mode
137
-
138
- | Preset | sandbox | approval | logicprobe behavior |
139
- |---|---|---|---|
140
- | `workspace-write` | workspace-write | ask | Evidence stays in workspace; model confirmation defaults to ask |
141
- | `danger-full-access` | danger-full-access | never | Full file access; interaction resolves to auto; never request sandbox escalation |
142
- | custom | any | any | The session folds the last `sandbox/mode` and `approval/policy` events; interaction follows approval only |
143
-
144
- In `interaction=auto`, do NOT call `ask_user_question` for model confirmation. Round-trip the extracted model into a transition table, compare it against the source extraction, and mark the report `UNCONFIRMED`.
145
-
146
- ## Cost and budget (A12)
147
-
148
- Transitions may carry a non-negative execution cost (`cost`, default 1 per transition — e.g. cycles or microseconds spent in the handler). A `budget` invariant bounds the worst-case accumulated cost over every reachable path:
149
-
150
- ```json
151
- {
152
- "id": "dispatch-budget",
153
- "description": "worst-case dispatch path stays within 100 cycles",
154
- "kind": "budget",
155
- "budget": 100
156
- }
157
- ```
158
-
159
- A12 reports the shortest over-budget counterexample path. A reachable cycle whose cost is positive is reported as unbounded — under model event semantics it can repeat indefinitely, so no finite budget holds. Budgets on machines with repeatable loops must bound those loops with variables (e.g. a retry counter guard). If transitions declare `cost` but no `budget` invariant exists, A12 emits the advisory `A12_COST_WITHOUT_BUDGET`.
160
-
161
- ## Probability reachability (A13)
162
-
163
- Declare `weight` on transitions (default 1) to interpret the machine as a DTMC, then add a `probability` invariant:
164
-
165
- ```json
166
- { "id": "reliability", "description": "at least 90% of runs reach SAFE", "kind": "probability", "target": "SAFE", "op": ">=", "p": 0.9 }
167
- ```
168
-
169
- A13 computes P(ever hitting `target`) from the initial state by value iteration over the absorbing chain (transitions with `weight` 0 never fire; terminals other than the target are absorbing failures) and reports a violation when the bound fails. Converges to the least fixed point; an iteration cap protects against non-convergent models.
170
-
171
- ## Deadline check (A14)
172
-
173
- Declare which events advance the discrete clock (`tickEvents`) and set `maxTicks` on a state that must be left within that many ticks of entering it:
174
-
175
- ```json
176
- { "schemaVersion": 1, "init": "LISTEN", "states": [{ "id": "LISTEN" }, { "id": "CRITICAL", "maxTicks": 2 }, { "id": "SAFE", "terminal": true }], "transitions": [ { "from": "LISTEN", "event": "fault", "to": "CRITICAL" }, { "from": "CRITICAL", "event": "recover", "to": "SAFE" } ], "tickEvents": ["tick"] }
177
- ```
178
-
179
- A14 explores residency with a per-entry tick counter: a `tickEvents` step that keeps the machine resident past `maxTicks` reports `A14_DEADLINE_MISS` with the over-residency path. Time advances only when a tick event fires — real-time passage (auto-advancing clocks) is not modeled; dense-time claims still route to timed model checkers. States declaring `maxTicks` without any `tickEvents` yield the advisory `A14_NO_TICK_EVENTS`.
180
-
181
- ## State entry/exit actions (onEntry / onExit)
182
-
183
- States may declare ordered action-name lists that fire automatically:
184
-
185
- ```json
186
- { "id": "ACTIVE", "onEntry": ["sync_lock"], "onExit": ["sync_unlock"] }
187
- ```
188
-
189
- Actions never change state or variables. Checks that care about resource discipline see them as implicit events: A4 Pair Symmetry treats an action equal to a pair acquireEvent/releaseEvent as an acquire/release that fires on every entry (resp. exit) of the state, so lock/unlock hidden inside entry/exit actions is verified without hand-written ENTER_x/EXIT_x pseudo-events.
190
-
191
- ## Composition verification
192
-
193
- Two or more machines can be checked together with `runCompositionVerification` (DSH tool `logicprobe_compose_verify`):
194
-
195
- - non-rendezvous events advance exactly one firing machine;
196
- - a rendezvous (handshake) event fires only when at least two machines declare it and every such non-terminal machine has it jointly enabled (guards held); participants advance simultaneously;
197
- - a terminal machine is stopped and does not participate.
198
-
199
- Checks: `C1_COMPOSITION_DEADLOCK` (a reachable composite state with no move while at least one machine is not terminal) and `C2_RENDEZVOUS_NEVER_FIRES`. This is a product-space BFS, so composite state count is the product of the machines; keep `maxStates` in mind.
200
-
201
- ## Minimal example
202
-
203
- ```json
204
- {
205
- "schemaVersion": 1,
206
- "init": "INIT",
207
- "states": [
208
- { "id": "INIT" },
209
- { "id": "RETRY" },
210
- { "id": "FATAL", "terminal": true }
211
- ],
212
- "transitions": [
213
- { "from": "INIT", "event": "timeout", "guard": { "variable": "retry", "op": "<", "value": 3 }, "to": "RETRY", "updates": [{ "variable": "retry", "op": "inc" }] },
214
- { "from": "INIT", "event": "timeout", "guard": { "variable": "retry", "op": ">=", "value": 3 }, "to": "FATAL" }
215
- ],
216
- "variables": [{ "name": "retry", "kind": "integer", "init": 0, "min": 0, "max": 3 }],
217
- "boundaryChecks": [{ "variable": "retry", "values": [0, 1, 2, 3] }]
218
- }
219
- ```
220
-
221
- ## Before/after comparison (D1-D4)
222
-
223
- When `beforeModel` is passed to `logicprobe_verify`, the engine treats `model` as AFTER and runs four extra checks after S1-A11:
224
-
225
- | Check | Purpose |
226
- |---|---|
227
- | D1 Behavioral Preservation | Every BEFORE (state, event) that could fire must still be fireable from the mapped AFTER state |
228
- | D2 Invariant Continuity | Every BEFORE invariant (mapped through `stateMapping`) must still hold in AFTER |
229
- | D3 Regression Delta | Lists added/removed states, events, and transitions |
230
- | D4 Deadlock/Liveness Regression | New deadlock states or closed SCCs not present in BEFORE |
231
-
232
- `stateMapping` maps BEFORE state ids to AFTER state ids. Omit it when state names are unchanged.
233
-
234
- Example tool call shape:
235
-
236
- ```json
237
- {
238
- "model": { "...": "AFTER LogicModelV1" },
239
- "beforeModel": { "...": "BEFORE LogicModelV1" },
240
- "stateMapping": { "OLD_INIT": "INIT", "OLD_ACTIVE": "ACTIVE" }
241
- }
242
- ```
243
-
244
- The report's `comparison` object includes both model hashes, state/transition counts, and delta arrays.
245
-
246
- ## Idempotent replay (A8)
247
-
248
- List events that must be idempotent in `idempotentEvents`. For every reachable state, applying the event twice must produce the same state as applying it once. This is useful for retries, webhook redelivery, and migration replay.
249
-
250
- ## Advanced constraints (S8, A9-A14)
251
-
252
- - **S8 Monotonic Variables**: declare `monotonic: "inc"|"dec"` on a variable; updates must not move in the opposite direction.
253
- - **A9 Leads-To**: `{ kind: "leads-to", from, to }` — every path from `from` must eventually reach `to`.
254
- - **A10 Sequence**: `{ kind: "sequence", events }` — events must appear in order.
255
- - **A11 Atomicity**: `{ kind: "atomicity", events, commit, rollback? }` — once an atomic event starts, the machine must reach commit/rollback before leaving the atomic scope or terminating.
256
- - **A12 Budget**: `{ kind: "budget", budget }` — no reachable path may accumulate transition cost above the budget; reports the shortest over-budget path and flags reachable positive-cost cycles as unbounded.
257
- - **A13 Probability**: `{ kind: "probability", target, op, p }` — P(ever hitting target) must satisfy the bound under the DTMC induced by transition `weight` (default 1).
258
- - **A14 Deadline**: state `maxTicks` + top-level `tickEvents` — a tick step may not keep a state resident past its deadline; reports the over-residency path.
259
-
260
- ## Limits
261
-
262
- - State-space exploration caps at `maxStates` (default 10000); larger guards/domains may report truncation instead of a false pass.
263
- - A3 samples the first `maxPermutationEvents` events (default 5).
264
- - The engine is a finite-state model checker. It cannot prove properties of the real implementation; follow with code-level review.
265
- - `cost` values are modeler-provided static labels — A12 verifies against them; real execution time/WCET needs binary-level timing analysis.
266
- - When the model state/event/action names reference semantics the engine does not verify (timing, preemption, hybrid control, probability), the report carries informational `coverageNotes` that route such claims to dedicated tools. These notes are vocabulary-based heuristics, never a substitute for the checks.
1
+ # DSH Model Schema v1 — logicprobe_verify
2
+
3
+ The dsh-native `logicprobe_verify` tool accepts a structured JSON model. The engine runs 22 checks (S1-S8 structural, A1-A14) and returns a JSON report. When `beforeModel` is supplied, it also runs D1-D4 before/after regression checks. Guards and updates are structured data — no code strings, no arbitrary execution.
4
+
5
+ ## Top-level model
6
+
7
+ ```json
8
+ {
9
+ "schemaVersion": 1,
10
+ "init": "INIT",
11
+ "states": [{ "id": "INIT" }, { "id": "ACTIVE", "terminal": true }],
12
+ "transitions": [
13
+ { "from": "INIT", "event": "go", "to": "ACTIVE" }
14
+ ],
15
+ "variables": [],
16
+ "invariants": [],
17
+ "concurrentPairs": [],
18
+ "boundaryChecks": [],
19
+ "resourcePairs": [],
20
+ "idempotentEvents": []
21
+ }
22
+ ```
23
+
24
+ | Field | Required | Meaning |
25
+ |---|---|---|
26
+ | `schemaVersion` | yes | Must be `1` |
27
+ | `init` | yes | Initial state id |
28
+ | `states` | yes | `{ id, terminal?, onEntry?, onExit?, maxTicks? }`; `terminal` exempts S2/S3/S5/A1; `onEntry`/`onExit` are action-name lists fired on entry/exit and treated by A4 as implicit acquire/release; `maxTicks` is an A14 deadline since entry (needs top-level `tickEvents`) |
29
+ | `transitions` | yes | `{ from, event, to, guard?, updates?, cost?, weight? }`; `cost` (non-negative, absent = 1) is checked by A12 against `budget` invariants; `weight` (non-negative, absent = 1) makes the machine a DTMC under A13 `probability` invariants |
30
+ | `variables` | no | `{ name, kind: integer\|boolean, init, min?, max?, monotonic? }` |
31
+ | `invariants` | no | See invariant kinds below |
32
+ | `concurrentPairs` | no | `["eventA", "eventB"]` pairs for A2 |
33
+ | `boundaryChecks` | no | `{ variable, values: number[] }` for A5 |
34
+ | `resourcePairs` | no | `{ resource, acquireEvent, releaseEvent, failEvent? }` for A4/A6 |
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
+ ### The schema is closed
39
+
40
+ Every object in the model declares a fixed key set, and **an undeclared key is a validation error, never silently ignored**. This is a correctness requirement, not strictness for its own sake: a mistyped field name used to be dropped while the reported result still looked authoritative. The concrete regression this prevents — `{"kind": "var-in-range", "variable": "c", "maximum": 0}` (note `maximum` for `max`) validated cleanly and reported `errors: 0, S7: pass`, turning a range that must fail into a vacuously true invariant. The same class of typo applies to `states` vs `state` on a `never-states` invariant, `guard` vs `gaurd` on a transition, `updates` vs `update`, and so on.
41
+
42
+ Declared keys per part:
43
+
44
+ | Part | Allowed keys |
45
+ |---|---|
46
+ | model | `schemaVersion, init, states, transitions, variables, invariants, concurrentPairs, boundaryChecks, resourcePairs, idempotentEvents, tickEvents, narrative` |
47
+ | state | `id, terminal, onEntry, onExit, maxTicks` |
48
+ | transition | `from, event, to, guard, updates, cost, weight` |
49
+ | update | `variable, op, value` |
50
+ | variable | `name, kind, init, min, max, monotonic` |
51
+ | boundaryCheck | `variable, values` |
52
+ | resourcePair | `resource, acquireEvent, releaseEvent, failEvent` |
53
+ | narrative | `states, events, scenarios` |
54
+ | scenario | `from, event, scenario` |
55
+ | invariant | per kind — see [Invariants](#invariants) |
56
+
57
+ `boundaryChecks` is the one place where two schemas share a field name: `logicprobe_verify` uses `{ variable, values }` (a variable's boundary values for A5), while `logicprobe_datamodel_verify` uses `{ entity, field, values }` (a field's boundary values for DA2). Each validator enforces its own shape, so an `(entity, field)` check passed to the state-machine engine is rejected rather than half-understood.
58
+
59
+ ### References must resolve
60
+
61
+ The same reasoning applies to every id a check names. A reference to a state, event, or variable that the model does not declare cannot be satisfied, and because the engine copies each check's target into its findings (`"target": "DONE"`), an unresolvable id reads as authoritative in the report:
62
+
63
+ | Reference | Must name |
64
+ |---|---|
65
+ | `states[].id`, `init` | a declared state (unique) |
66
+ | `transitions[].from` / `.to` | a declared state |
67
+ | `transitions[].updates[].variable`, `boundaryChecks[].variable` | a declared variable |
68
+ | `transitions[].guard` variables | a declared variable |
69
+ | `resourcePairs[].acquireEvent` / `.releaseEvent` / `.failEvent` | a declared event |
70
+ | `invariants[].event` (`event-before-state`) | a declared event |
71
+ | `invariants[].state` / `.states[]` / `.from` / `.to` / `.target` / `.when.state` | a declared state |
72
+
73
+ A vacuous check is worse than a rejected one: a `leads-to` invariant whose `to` state is misspelled can never be violated, and a `never-states` list holding a nonexistent id can never be reached, so both report "pass" for a model that was never actually checked.
74
+
75
+ ## Model narrative (natural-language context)
76
+
77
+ The model may carry a `narrative` block explaining, in natural language, what
78
+ every symbol means in the real scenario. It is what gets shown to the user when
79
+ the extracted model is presented for confirmation, and the report echoes it back
80
+ so findings can be read against real scenarios instead of bare ids.
81
+
82
+ ```json
83
+ "narrative": {
84
+ "states": {
85
+ "NEW": "订单已创建,等待支付",
86
+ "PAID": "已支付,等待发货",
87
+ "SHIPPED": "已发货,等待签收",
88
+ "DONE": "已完成(终态)",
89
+ "CANCELLED": "已取消(终态)"
90
+ },
91
+ "events": {
92
+ "pay": "买家完成支付",
93
+ "ship": "仓库发货",
94
+ "deliver": "买家签收",
95
+ "cancel": "取消订单"
96
+ },
97
+ "scenarios": [
98
+ { "from": "NEW", "event": "pay", "scenario": "下单后支付成功,订单进入待发货" },
99
+ { "from": "PAID", "event": "ship", "scenario": "已支付订单发货,进入运输中" },
100
+ { "from": "SHIPPED", "event": "deliver", "scenario": "签收完成,订单结束" },
101
+ { "from": "NEW", "event": "cancel", "scenario": "未支付订单被取消" },
102
+ { "from": "PAID", "event": "cancel", "scenario": "已支付订单取消并退款" }
103
+ ]
104
+ }
105
+ ```
106
+
107
+ **Completeness contract**: when `narrative` is present, all three parts are
108
+ required and must fully cover the model — every declared state needs a
109
+ `narrative.states` entry, every event used in `transitions` needs a
110
+ `narrative.events` entry, and every distinct `(from, event)` group needs a
111
+ `narrative.scenarios` entry. Keys must reference declared ids; unknown
112
+ references, missing coverage, and duplicate scenario keys are model validation
113
+ errors. The report's `narrative` field echoes the block unchanged.
114
+
115
+ **Presenting the model**: when showing the extracted model for confirmation, render
116
+ the natural language INLINE in the model presentation, not as a separate block.
117
+ Three rendering forms (all derive from the same `narrative` data):
118
+
119
+ - **Form A — integrated transition table (default)**: one row per transition with
120
+ state/event meanings in parentheses and the scenario as the last column. Keep
121
+ meanings short (state/event ≤ 6 characters, scenario ≤ 10) and estimate row
122
+ width (CJK counts as 2) so rows fit the display area — a wrapped row loses
123
+ column alignment and readability collapses.
124
+ - **Form B — sentence blocks (reading-accessible)**: scenario sentence first, then
125
+ a fixed three-line frame (状态…/发生…/进入…). Use for detailed confirmation,
126
+ users with reading difficulties, or ≤ 10 transitions.
127
+ - **Form C — grouped by source state (large machines / narrow panes)**: one
128
+ section per state, each rendered as its own small 3-column table
129
+ (`event(含义)| NEXT(含义)| 场景`); no cross-group column alignment to
130
+ track. Use for ≥ 15 transitions or narrow display areas; if a group table would
131
+ still wrap, fall back to a one-line-per-event bullet list for that group.
132
+
133
+ ## Guards
134
+
135
+ A guard is exactly one of:
136
+
137
+ ```json
138
+ { "variable": "retry", "op": "<", "value": 3 }
139
+ { "all": [ { "variable": "armed", "op": "==", "value": true }, { "variable": "retry", "op": ">", "value": 0 } ] }
140
+ { "any": [ { "variable": "mode", "op": "==", "value": 1 }, { "variable": "mode", "op": "==", "value": 2 } ] }
141
+ { "not": { "variable": "locked", "op": "==", "value": true } }
142
+ ```
143
+
144
+ - Boolean variables only support `==` / `!=`.
145
+ - A transition with **no guard** is the default/else branch for its `(from, event)` group. It fires only when no guarded branch in that group is true.
146
+ - Multiple true guards in one `(from, event)` group are reported as S4 nondeterminism; a group with guards and no default must be exhaustive or S6 flags the missing branch.
147
+
148
+ ## Updates
149
+
150
+ ```json
151
+ { "variable": "retry", "op": "inc", "value": 1 }
152
+ { "variable": "retry", "op": "set", "value": 0 }
153
+ { "variable": "retry", "op": "dec", "value": 1 }
154
+ ```
155
+
156
+ `inc`/`dec` default to 1 when `value` is omitted. `set` defaults to 0.
157
+
158
+ ## Invariants
159
+
160
+ | Kind | Shape | Checks |
161
+ |---|---|---|
162
+ | `never-states` | `{ states: ["ERROR"] }` | No reachable runtime state may be in the forbidden set |
163
+ | `var-in-range` | `{ variable, min?, max?, when? }` | Every reachable runtime state selected by `when` keeps the variable in range (at least one of `min`/`max` is required) |
164
+ | `event-before-state` | `{ event: "power_ready", state: "ACTIVE" }` | Every path entering `state` must have passed through `event` first |
165
+ | `leads-to` | `{ from: "MIGRATING", to: "DONE" }` | Every path from `from` must eventually reach `to` |
166
+ | `sequence` | `{ events: ["backup", "modify", "commit"] }` | Events must occur in the given order |
167
+ | `atomicity` | `{ events: ["write"], commit: "commit", rollback?: "rollback" }` | Atomic group must end with commit/rollback before leaving scope |
168
+ | `budget` | `{ budget: n }` | No reachable path may accumulate transition cost greater than n (A12). Costs are non-negative; a transition without `cost` counts 1, so legacy machines keep step-count semantics |
169
+ | `probability` | `{ target, op: one of >= <= > <, p }` | P(ever hitting target) must satisfy the bound (A13, DTMC from transition `weight`, default 1; value iteration) |
170
+
171
+ A7 reports the shortest violating path for each failed invariant. An empty path means the initial state already violates it.
172
+
173
+ ### Scoping a range to a state (`when`)
174
+
175
+ `var-in-range` is the only kind that accepts `when`, and only it needs to: the others either already constrain a state set (`never-states`) or assert a property of a whole path (`leads-to`, `sequence`, `atomicity`, `budget`, `probability`), where "the scope applies at which step of the path" has no single answer. A `when` on any other kind is a validation error rather than a silently ignored field.
176
+
177
+ ```json
178
+ { "id": "depth-in-probe", "description": "probe depth stays bounded while in PROBE",
179
+ "kind": "var-in-range", "variable": "depth", "min": 0, "max": 4,
180
+ "when": { "state": "PROBE" } }
181
+ ```
182
+
183
+ Two scope shapes are accepted:
184
+
185
+ - `{ "state": "PROBE" }` — the state-scoped form, for "this range applies only here".
186
+ - a guard node (`{ variable, op, value }` / `{ all }` / `{ any }` / `{ not }`) — for "this range applies only while a mode variable says so". It must not reference the constrained variable itself: a scope that depends on the value it constrains can switch itself off exactly when the value drifts out of range, which masks its own violation.
187
+
188
+ Semantics — `when` is evaluated against the **post-state** of every transition, and the **initial state is checked unconditionally** whatever the scope says. So a `{ state }` scope is exhaustive over that state: for every reachable runtime state whose id is in the scope, the variable is in range, including the case where the machine starts there. This is why scoping away from the state that actually violates the range is not a loophole — it simply produces a machine in which no reachable in-scope state is out of range, i.e. an invariant that holds.
189
+
190
+ `when` participates in the model hash, so two models differing only in scope are never treated as the same model in before/after comparison; a `{ state }` scope also follows `stateMapping` during D2 continuity checks.
191
+
192
+ ## Permission presets and interaction mode
193
+
194
+ | Preset | sandbox | approval | logicprobe behavior |
195
+ |---|---|---|---|
196
+ | `workspace-write` | workspace-write | ask | Evidence stays in workspace; model confirmation defaults to ask |
197
+ | `danger-full-access` | danger-full-access | never | Full file access; interaction resolves to auto; never request sandbox escalation |
198
+ | custom | any | any | The session folds the last `sandbox/mode` and `approval/policy` events; interaction follows approval only |
199
+
200
+ In `interaction=auto`, do NOT call `ask_user_question` for model confirmation. Round-trip the extracted model into a transition table, compare it against the source extraction, and mark the report `UNCONFIRMED`.
201
+
202
+ ## Cost and budget (A12)
203
+
204
+ Transitions may carry a non-negative execution cost (`cost`, default 1 per transition — e.g. cycles or microseconds spent in the handler). A `budget` invariant bounds the worst-case accumulated cost over every reachable path:
205
+
206
+ ```json
207
+ {
208
+ "id": "dispatch-budget",
209
+ "description": "worst-case dispatch path stays within 100 cycles",
210
+ "kind": "budget",
211
+ "budget": 100
212
+ }
213
+ ```
214
+
215
+ A12 reports the shortest over-budget counterexample path. A reachable cycle whose cost is positive is reported as unbounded — under model event semantics it can repeat indefinitely, so no finite budget holds. Budgets on machines with repeatable loops must bound those loops with variables (e.g. a retry counter guard). If transitions declare `cost` but no `budget` invariant exists, A12 emits the advisory `A12_COST_WITHOUT_BUDGET`.
216
+
217
+ ## Probability reachability (A13)
218
+
219
+ Declare `weight` on transitions (default 1) to interpret the machine as a DTMC, then add a `probability` invariant:
220
+
221
+ ```json
222
+ { "id": "reliability", "description": "at least 90% of runs reach SAFE", "kind": "probability", "target": "SAFE", "op": ">=", "p": 0.9 }
223
+ ```
224
+
225
+ A13 computes P(ever hitting `target`) from the initial state by value iteration over the absorbing chain (transitions with `weight` 0 never fire; terminals other than the target are absorbing failures) and reports a violation when the bound fails. Converges to the least fixed point; an iteration cap protects against non-convergent models.
226
+
227
+ ## Deadline check (A14)
228
+
229
+ Declare which events advance the discrete clock (`tickEvents`) and set `maxTicks` on a state that must be left within that many ticks of entering it:
230
+
231
+ ```json
232
+ { "schemaVersion": 1, "init": "LISTEN", "states": [{ "id": "LISTEN" }, { "id": "CRITICAL", "maxTicks": 2 }, { "id": "SAFE", "terminal": true }], "transitions": [ { "from": "LISTEN", "event": "fault", "to": "CRITICAL" }, { "from": "CRITICAL", "event": "recover", "to": "SAFE" } ], "tickEvents": ["tick"] }
233
+ ```
234
+
235
+ A14 explores residency with a per-entry tick counter: a `tickEvents` step that keeps the machine resident past `maxTicks` reports `A14_DEADLINE_MISS` with the over-residency path. Time advances only when a tick event fires — real-time passage (auto-advancing clocks) is not modeled; dense-time claims still route to timed model checkers. States declaring `maxTicks` without any `tickEvents` yield the advisory `A14_NO_TICK_EVENTS`.
236
+
237
+ ## State entry/exit actions (onEntry / onExit)
238
+
239
+ States may declare ordered action-name lists that fire automatically:
240
+
241
+ ```json
242
+ { "id": "ACTIVE", "onEntry": ["sync_lock"], "onExit": ["sync_unlock"] }
243
+ ```
244
+
245
+ Actions never change state or variables. Checks that care about resource discipline see them as implicit events: A4 Pair Symmetry treats an action equal to a pair acquireEvent/releaseEvent as an acquire/release that fires on every entry (resp. exit) of the state, so lock/unlock hidden inside entry/exit actions is verified without hand-written ENTER_x/EXIT_x pseudo-events.
246
+
247
+ ## Composition verification
248
+
249
+ Two or more machines can be checked together with `runCompositionVerification` (DSH tool `logicprobe_compose_verify`):
250
+
251
+ - non-rendezvous events advance exactly one firing machine;
252
+ - a rendezvous (handshake) event fires only when at least two machines declare it and every such non-terminal machine has it jointly enabled (guards held); participants advance simultaneously;
253
+ - a terminal machine is stopped and does not participate.
254
+
255
+ Checks: `C1_COMPOSITION_DEADLOCK` (a reachable composite state with no move while at least one machine is not terminal) and `C2_RENDEZVOUS_NEVER_FIRES`. This is a product-space BFS, so composite state count is the product of the machines; keep `maxStates` in mind.
256
+
257
+ ## Minimal example
258
+
259
+ ```json
260
+ {
261
+ "schemaVersion": 1,
262
+ "init": "INIT",
263
+ "states": [
264
+ { "id": "INIT" },
265
+ { "id": "RETRY" },
266
+ { "id": "FATAL", "terminal": true }
267
+ ],
268
+ "transitions": [
269
+ { "from": "INIT", "event": "timeout", "guard": { "variable": "retry", "op": "<", "value": 3 }, "to": "RETRY", "updates": [{ "variable": "retry", "op": "inc" }] },
270
+ { "from": "INIT", "event": "timeout", "guard": { "variable": "retry", "op": ">=", "value": 3 }, "to": "FATAL" }
271
+ ],
272
+ "variables": [{ "name": "retry", "kind": "integer", "init": 0, "min": 0, "max": 3 }],
273
+ "boundaryChecks": [{ "variable": "retry", "values": [0, 1, 2, 3] }]
274
+ }
275
+ ```
276
+
277
+ ## Before/after comparison (D1-D4)
278
+
279
+ When `beforeModel` is passed to `logicprobe_verify`, the engine treats `model` as AFTER and runs four extra checks after S1-A11:
280
+
281
+ | Check | Purpose |
282
+ |---|---|
283
+ | D1 Behavioral Preservation | Every BEFORE (state, event) that could fire must still be fireable from the mapped AFTER state |
284
+ | D2 Invariant Continuity | Every BEFORE invariant (mapped through `stateMapping`) must still hold in AFTER |
285
+ | D3 Regression Delta | Lists added/removed states, events, and transitions |
286
+ | D4 Deadlock/Liveness Regression | New deadlock states or closed SCCs not present in BEFORE |
287
+
288
+ `stateMapping` maps BEFORE state ids to AFTER state ids. Omit it when state names are unchanged.
289
+
290
+ Example tool call shape:
291
+
292
+ ```json
293
+ {
294
+ "model": { "...": "AFTER LogicModelV1" },
295
+ "beforeModel": { "...": "BEFORE LogicModelV1" },
296
+ "stateMapping": { "OLD_INIT": "INIT", "OLD_ACTIVE": "ACTIVE" }
297
+ }
298
+ ```
299
+
300
+ The report's `comparison` object includes both model hashes, state/transition counts, and delta arrays.
301
+
302
+ ## Idempotent replay (A8)
303
+
304
+ List events that must be idempotent in `idempotentEvents`. For every reachable state, applying the event twice must produce the same state as applying it once. This is useful for retries, webhook redelivery, and migration replay.
305
+
306
+ ## Advanced constraints (S8, A9-A14)
307
+
308
+ - **S8 Monotonic Variables**: declare `monotonic: "inc"|"dec"` on a variable; updates must not move in the opposite direction.
309
+ - **A9 Leads-To**: `{ kind: "leads-to", from, to }` — every path from `from` must eventually reach `to`.
310
+ - **A10 Sequence**: `{ kind: "sequence", events }` — events must appear in order.
311
+ - **A11 Atomicity**: `{ kind: "atomicity", events, commit, rollback? }` — once an atomic event starts, the machine must reach commit/rollback before leaving the atomic scope or terminating.
312
+ - **A12 Budget**: `{ kind: "budget", budget }` — no reachable path may accumulate transition cost above the budget; reports the shortest over-budget path and flags reachable positive-cost cycles as unbounded.
313
+ - **A13 Probability**: `{ kind: "probability", target, op, p }` — P(ever hitting target) must satisfy the bound under the DTMC induced by transition `weight` (default 1).
314
+ - **A14 Deadline**: state `maxTicks` + top-level `tickEvents` — a tick step may not keep a state resident past its deadline; reports the over-residency path.
315
+
316
+ ## Limits
317
+
318
+ - State-space exploration caps at `maxStates` (default 10000); larger guards/domains may report truncation instead of a false pass.
319
+ - A3 samples the first `maxPermutationEvents` events (default 5).
320
+ - The engine is a finite-state model checker. It cannot prove properties of the real implementation; follow with code-level review.
321
+ - `cost` values are modeler-provided static labels — A12 verifies against them; real execution time/WCET needs binary-level timing analysis.
322
+ - When the model state/event/action names reference semantics the engine does not verify (timing, preemption, hybrid control, probability), the report carries informational `coverageNotes` that route such claims to dedicated tools. These notes are vocabulary-based heuristics, never a substitute for the checks.