dsh-logicprobe 0.5.6 → 0.6.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,204 +1,266 @@
1
- # DSH Model Schema v1 — logicprobe_verify
2
-
3
- The dsh-native `logicprobe_verify` tool accepts a structured JSON model. The engine runs 19 checks (S1-S8 structural, A1-A11 adversarial) 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? }`; `terminal` exempts S2/S3/S5/A1 |
29
- | `transitions` | yes | `{ from, event, to, guard?, updates? }` |
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
-
132
- A7 reports the shortest violating path for each failed invariant. An empty path means the initial state already violates it.
133
-
134
- ## Permission presets and interaction mode
135
-
136
- | Preset | sandbox | approval | logicprobe behavior |
137
- |---|---|---|---|
138
- | `workspace-write` | workspace-write | ask | Evidence stays in workspace; model confirmation defaults to ask |
139
- | `danger-full-access` | danger-full-access | never | Full file access; interaction resolves to auto; never request sandbox escalation |
140
- | custom | any | any | The session folds the last `sandbox/mode` and `approval/policy` events; interaction follows approval only |
141
-
142
- 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`.
143
-
144
- ## Minimal example
145
-
146
- ```json
147
- {
148
- "schemaVersion": 1,
149
- "init": "INIT",
150
- "states": [
151
- { "id": "INIT" },
152
- { "id": "RETRY" },
153
- { "id": "FATAL", "terminal": true }
154
- ],
155
- "transitions": [
156
- { "from": "INIT", "event": "timeout", "guard": { "variable": "retry", "op": "<", "value": 3 }, "to": "RETRY", "updates": [{ "variable": "retry", "op": "inc" }] },
157
- { "from": "INIT", "event": "timeout", "guard": { "variable": "retry", "op": ">=", "value": 3 }, "to": "FATAL" }
158
- ],
159
- "variables": [{ "name": "retry", "kind": "integer", "init": 0, "min": 0, "max": 3 }],
160
- "boundaryChecks": [{ "variable": "retry", "values": [0, 1, 2, 3] }]
161
- }
162
- ```
163
-
164
- ## Before/after comparison (D1-D4)
165
-
166
- When `beforeModel` is passed to `logicprobe_verify`, the engine treats `model` as AFTER and runs four extra checks after S1-A11:
167
-
168
- | Check | Purpose |
169
- |---|---|
170
- | D1 Behavioral Preservation | Every BEFORE (state, event) that could fire must still be fireable from the mapped AFTER state |
171
- | D2 Invariant Continuity | Every BEFORE invariant (mapped through `stateMapping`) must still hold in AFTER |
172
- | D3 Regression Delta | Lists added/removed states, events, and transitions |
173
- | D4 Deadlock/Liveness Regression | New deadlock states or closed SCCs not present in BEFORE |
174
-
175
- `stateMapping` maps BEFORE state ids to AFTER state ids. Omit it when state names are unchanged.
176
-
177
- Example tool call shape:
178
-
179
- ```json
180
- {
181
- "model": { "...": "AFTER LogicModelV1" },
182
- "beforeModel": { "...": "BEFORE LogicModelV1" },
183
- "stateMapping": { "OLD_INIT": "INIT", "OLD_ACTIVE": "ACTIVE" }
184
- }
185
- ```
186
-
187
- The report's `comparison` object includes both model hashes, state/transition counts, and delta arrays.
188
-
189
- ## Idempotent replay (A8)
190
-
191
- 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.
192
-
193
- ## Advanced constraints (S8, A9-A11)
194
-
195
- - **S8 Monotonic Variables**: declare `monotonic: "inc"|"dec"` on a variable; updates must not move in the opposite direction.
196
- - **A9 Leads-To**: `{ kind: "leads-to", from, to }` — every path from `from` must eventually reach `to`.
197
- - **A10 Sequence**: `{ kind: "sequence", events }` — events must appear in order.
198
- - **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.
199
-
200
- ## Limits
201
-
202
- - State-space exploration caps at `maxStates` (default 10000); larger guards/domains may report truncation instead of a false pass.
203
- - A3 samples the first `maxPermutationEvents` events (default 5).
204
- - The engine is a finite-state model checker. It cannot prove properties of the real implementation; follow with code-level review.
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.
@@ -0,0 +1,36 @@
1
+ # Gap Routing Guide
2
+
3
+ logicprobe is a design-time, qualitative model checker. For semantic dimensions it does
4
+ not model, it does not stay silent: it routes claims to dedicated tools. This reference
5
+ is the routing table shared by the verification report (`coverageNotes`), the
6
+ concurrency scanner (`suggestions` on absolute claims), and manual review.
7
+
8
+ Routing is a hint, not a proof. A note never verifies the claim — it marks the dimension
9
+ as outside this engine and points at tooling that can handle it.
10
+
11
+ | Dimension | What logicprobe covers today | Claim examples | Dedicated tooling | Why not here |
12
+ |---|---|---|---|---|
13
+ | Hard real time (timing) | Order, counts, path budgets (A12); time modeled only as counters + events | "500 ms to SAFE", "no deadline miss", "period ≤ 1 ms" | UPPAAL, IMITATOR (timed automata); binary-level timing analysis for WCET | No clocks, deadlines, or period semantics |
14
+ | Preemptive concurrency | Event-order interleavings (A2/A3), resource pairing (A4), idempotent replay (A8) | "thread-safe", "interrupt-safe", "no priority inversion" | TSan/Helgrind (runtime), CBMC (proof), TLA+ (interleaving model), RTOS-aware analysis | Single-threaded event model; no preemption, IRQ nesting, or atomics at instruction level |
15
+ | Hybrid control (continuous plant) | Discrete transitions and modes only | "switching is stable", "no chattering", "settling within T" | SpaceEx, Flow* (hybrid reachability), Simulink/Stateflow verification | No continuous dynamics; stability is not expressible |
16
+ | Probabilistic / reliability | Qualitative reachability and invariants | "MTBF ≥ X", "failure rate ≤ p", "availability ≥ 99.9%" | PRISM, Storm (stochastic model checking), fault-tree / FMEA | No probability semantics |
17
+ | Execution cost / performance | A12 budget checks over declared transition `cost` (absent = 1) | "worst-case dispatch ≤ 100 cycles" | aiT, RapiTime (real WCET at binary level) | `cost` is a modeler label, not measured execution time |
18
+ | Multi-machine composition | Single machine verified; cross-machine contract must be documented separately | "A sends E, B always handles E" | CSP (FDR), mCRL2, TLA+ (compositional models) | No composition semantics between machines |
19
+ | Nested/hierarchical statecharts | Flat models only; flatten before verification | parent/child states, orthogonal regions | SCXML / Stateflow (native hierarchy) | Flat state list only — flatten manually and re-confirm |
20
+
21
+ ## Where the routing surfaces
22
+
23
+ - `logicprobe_verify` report: a model whose state/event/action names match the timing,
24
+ preemption, hybrid, or probability vocabulary gets informational `coverageNotes`
25
+ with the same routing as above.
26
+ - A12 advisory: transitions declaring `cost` without a `budget` invariant produce
27
+ `A12_COST_WITHOUT_BUDGET`, pointing at the budget check.
28
+ - `logicprobe_concurrency_scan`: absolute concurrency claims (thread-safe, lock-free,
29
+ interrupt-safe, ...) carry `suggestions` naming dedicated tools.
30
+ - Manual review: when reading a plan, match its claims against the table above and
31
+ require dedicated evidence before accepting "always"/"never"/"guaranteed" language
32
+ - Generators: `exportModel(model, format)` emits native inputs for UPPAAL (.xta + queries),
33
+ TLA+ (TLC spec + safety), PRISM (.pm + .pctl), and SPIN (Promela + ltl) from a
34
+ LogicModelV1 — v1 translates the core machine (guards/updates/weights; booleans as
35
+ integers) and leaves unrepresentable invariants as explicit warnings.
36
+ Self-tests: tests/exporters/run.mjs.
@@ -319,6 +319,56 @@ ATOMIC_GROUPS = [
319
319
  ]
320
320
  ```
321
321
 
322
+ ### A12: Budget (Worst-Case Path Cost)
323
+
324
+ For performance-sensitive claims ("the dispatch path stays within 100 cycles", "worst-case latency ≤ budget"), model each transition with an execution cost and declare a budget:
325
+
326
+ ```python
327
+ TRANSITION_COSTS = { # (from, event): cost; absent entries count as 1
328
+ ("BOOT", "calibrate_done"): 40,
329
+ ("IDLE", "enable"): 5,
330
+ ("RUN", "watchdog_expiry"): 15,
331
+ }
332
+ BUDGETS = [
333
+ {"id": "dispatch-budget", "description": "worst-case path within budget", "budget": 100},
334
+ ]
335
+ ```
336
+
337
+ Probe: find the shortest path from init whose accumulated cost exceeds the budget; report it verbatim. A reachable cycle with positive total cost means cost can grow without bound — flag it as a budget violation regardless of the declared budget.
338
+
339
+ In DSH (`logicprobe_verify`), `cost` is an optional field on transitions (absent = 1) and `budget` is an invariant kind; A12 runs automatically. Cost values are modeler-provided static labels — they verify the model against the declared budget, not the real WCET, which needs binary-level timing analysis.
340
+
341
+ ### A13: Probability Reachability (DTMC)
342
+
343
+ For claims with a reliability/probability flavor ("at least 90% of runs reach SAFE", "P(broke first) ≥ 0.75"), attach a relative weight to each branch and declare the bound. Absent weight = 1; weight 0 means the branch never fires probabilistically.
344
+
345
+ ```python
346
+ TRANSITION_WEIGHTS = { # (from_state, event): weight; absent entries count as 1
347
+ ("RUN", "ok"): 9,
348
+ ("RUN", "bad"): 1,
349
+ }
350
+ PROBABILITY_INVARIANTS = [
351
+ {"id": "reliability", "target": "SAFE", "op": ">=", "p": 0.9},
352
+ ]
353
+ ```
354
+
355
+ Probe: solve P(ever reaching `target`) from INIT by value iteration over the reachable absorbing chain; compare against the bound. In DSH (`logicprobe_verify`), `weight` on transitions plus a `probability` invariant runs A13 automatically. This is a qualitative DTMC check of the modeler's weights — real MTBF/failure-rate numbers need PRISM/Storm or fault-tree analysis.
356
+
357
+ ### A14: Deadline (Discrete Tick Clock)
358
+
359
+ For real-time-sounding claims ("must leave BUSY within 3 ticks", "watchdog resets before deadline"), declare which events advance the clock and cap per-state residency:
360
+
361
+ ```python
362
+ TICK_EVENTS = {"tick"}
363
+ STATE_MAX_TICKS = {"BUSY": 3}
364
+ ```
365
+
366
+ Probe: explore residency — a `tick` step that keeps the machine resident past `maxTicks` is a deadline miss; report the over-residency path. In DSH (`logicprobe_verify`), state `maxTicks` plus top-level `tickEvents` run A14 automatically. This checks the model's declared deadlines, not real execution time (see Known Limitations below; hard real-time semantics route to UPPAAL).
367
+
368
+ ### Standalone engine (non-DSH, JSON models)
369
+
370
+ `references/logicprobe-engine.py` is an exact Python mirror of the DSH tools: `verify model.json` runs all 22 checks + D1-D4 with a `--before-model`/optional `--state-mapping`; `compose m1.json m2.json ... --rendezvous a,b` runs C1/C2 composition; `export model.json --format uppaal|tla|prism|spin` reproduces the four exporters byte-for-byte. It reads the same LogicModelV1 JSON as DSH, so a model verified in one host verifies identically in the other (checked by tests/python/run.mjs).
371
+
322
372
  ## Counter-Example Interpretation
323
373
 
324
374
  When a probe finds a counter-example, classify it:
@@ -360,14 +410,16 @@ When a probe finds a counter-example, classify it:
360
410
  - **Hardware-specific behavior**: Memory-mapped I/O timing, DMA races, cache coherency
361
411
  - **Undocumented behavior**: If the plan doesn't describe a transition, the model can't either
362
412
  - **Real concurrency**: The model is single-threaded; true preemptive multitasking bugs are out of scope
363
- - **Entry/exit actions**: State entry/exit side effects (e.g., `lock()` on enter, `unlock()` on exit) are not modeled as events. A4 Pair Symmetry may miss unbalanced pairs that exist only in entry/exit actions. If the plan describes these, manually extract them as pseudo-events (`ENTER_state`, `EXIT_state`) before running verification.
413
+ - **Entry/exit actions**: State entry/exit side effects (e.g., `lock()` on enter, `unlock()` on exit) are not modeled as events. A4 Pair Symmetry may miss unbalanced pairs that exist only in entry/exit actions. If the plan describes these, manually extract them as pseudo-events (`ENTER_state`, `EXIT_state`) before running verification. In DSH (`logicprobe_verify`), state `onEntry`/`onExit` declarations are modeled natively and A4 treats them as implicit acquire/release events — no manual pseudo-events needed.
414
+ - **Execution cost is modeler-annotated**: transition `cost` (absent = 1) and `budget` invariants are static labels; A12 verifies the model against them. Real execution time/WCET needs binary-level timing analysis (e.g. aiT, RapiTime).
415
+ - **Unverified semantic dimensions**: when model or document vocabulary references hard real time, preemptive concurrency, hybrid control stability, or probability/reliability, logicprobe reports informational notes / routes to dedicated tools — it never verifies those claims itself. See `references/gap-routing-guide.md`.
364
416
  - **Guard variable scope**: The model treats guard variables as global to the machine. If a guard variable's lifetime is state-scoped (reset on entry) but the model assumes it accumulates globally, boundary-blast results will be wrong. Verify guard variable scope during extraction.
365
417
  - **Nested/hierarchical states (Harel statecharts)**: The model only supports flat state machines. Parent/child state nesting, history pseudostates, and orthogonal regions are not supported — flatten them manually before verification.
366
418
  - **Cross-machine protocols**: Two interacting state machines are verified independently. Composition bugs (e.g., Machine A sends event E to Machine B, but B is in a state that doesn't handle E) are invisible to single-machine verification. If the plan describes multi-machine interaction, document the protocol contract separately.
367
419
 
368
420
  ### Model Fidelity Warning
369
421
 
370
- The Python model is an APPROXIMATION. It models state transitions, not execution semantics. A model that passes all 14 checks means the plan's LOGIC is consistent — NOT that the implementation will work. Always follow logic verification with code-level review.
422
+ The Python model is an APPROXIMATION. It models state transitions, not execution semantics. A model that passes all 22 checks (S1-S8 structural, A1-A14) means the plan's LOGIC is consistent — NOT that the implementation will work. Always follow logic verification with code-level review.
371
423
 
372
424
  ## Refactoring Verification
373
425
 
@@ -485,8 +537,8 @@ Manual verification takes longer but produces identical-quality findings. The ke
485
537
  6a. Python ≥ 3.6 → load verification-harness.py → fill in MODEL → run
486
538
  6b. No Python → use Manual Verification Mode (see above) — execute each check step by step
487
539
  7. Extract model → show transition table → GET USER CONFIRMATION
488
- 8. Run Phase 2a (7 structural primitives) → log results
489
- 9. Run Phase 2b (7 adversarial probes) → log results
540
+ 8. Run Phase 2a (8 structural primitives) → log results
541
+ 9. Run Phase 2b (14 adversarial probes) → log results
490
542
  10. Classify counter-examples (true positive / model error / acceptable risk)
491
543
  11. Feed confirmed findings into Phase 3 (gap analysis)
492
544
  12. Include in Phase 5 (structured output)