dsh-model-everything 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en-US.md +332 -0
- package/README.md +332 -3
- package/cordis.patch.yml +20 -0
- package/examples/README.md +48 -0
- package/examples/control-dispatcher.json +113 -0
- package/examples/control-dispatcher.overbudget.json +113 -0
- package/examples/ecommerce-data-model.json +185 -0
- package/examples/order-state-machine.after.json +116 -0
- package/examples/order-state-machine.before.json +101 -0
- package/examples/user-migration.after.json +29 -0
- package/examples/user-migration.before.json +29 -0
- package/gate/locale/en.json +6 -0
- package/gate/locale/zh.json +6 -0
- package/gate/package.json +5 -0
- package/lib/baseline.js +249 -0
- package/lib/compose-tool.js +80 -0
- package/lib/concurrency-tool.js +40 -0
- package/lib/concurrency.js +206 -0
- package/lib/data-engine.js +1141 -0
- package/lib/data-tool.js +87 -0
- package/lib/design-engine.js +1149 -0
- package/lib/design-tool.js +63 -0
- package/lib/engine.js +4343 -0
- package/lib/export-tool.js +78 -0
- package/lib/exporters.js +434 -0
- package/lib/gate.js +274 -0
- package/lib/granularity.js +237 -0
- package/lib/index.js +237 -0
- package/lib/json-value.js +18 -0
- package/lib/model-file.js +48 -0
- package/lib/option-shape.js +62 -0
- package/lib/report-diff-tool.js +63 -0
- package/lib/report-view.js +433 -0
- package/lib/structure-tool.js +91 -0
- package/lib/structure.js +711 -0
- package/lib/tool.js +94 -0
- package/lib/types/baseline.d.ts +65 -0
- package/lib/types/compose-tool.d.ts +7 -0
- package/lib/types/concurrency-tool.d.ts +8 -0
- package/lib/types/concurrency.d.ts +37 -0
- package/lib/types/data-engine.d.ts +211 -0
- package/lib/types/data-tool.d.ts +10 -0
- package/lib/types/design-engine.d.ts +215 -0
- package/lib/types/design-tool.d.ts +11 -0
- package/lib/types/engine.d.ts +494 -0
- package/lib/types/export-tool.d.ts +7 -0
- package/lib/types/exporters.d.ts +37 -0
- package/lib/types/gate.d.ts +91 -0
- package/lib/types/granularity.d.ts +106 -0
- package/lib/types/index.d.ts +45 -0
- package/lib/types/json-value.d.ts +21 -0
- package/lib/types/model-file.d.ts +7 -0
- package/lib/types/option-shape.d.ts +22 -0
- package/lib/types/report-diff-tool.d.ts +8 -0
- package/lib/types/report-view.d.ts +50 -0
- package/lib/types/structure-tool.d.ts +10 -0
- package/lib/types/structure.d.ts +152 -0
- package/lib/types/tool.d.ts +11 -0
- package/lib/types/uml-communication.d.ts +109 -0
- package/lib/types/uml-composite.d.ts +107 -0
- package/lib/types/uml-interaction-overview.d.ts +91 -0
- package/lib/types/uml-profile.d.ts +112 -0
- package/lib/types/uml-structure.d.ts +53 -0
- package/lib/types/uml-syntax.d.ts +296 -0
- package/lib/types/uml-timing.d.ts +152 -0
- package/lib/types/uml-tool.d.ts +12 -0
- package/lib/types/uml-usecase.d.ts +58 -0
- package/lib/types/uml.d.ts +303 -0
- package/lib/uml-communication.js +353 -0
- package/lib/uml-composite.js +442 -0
- package/lib/uml-interaction-overview.js +277 -0
- package/lib/uml-profile.js +373 -0
- package/lib/uml-structure.js +306 -0
- package/lib/uml-syntax.js +545 -0
- package/lib/uml-timing.js +974 -0
- package/lib/uml-tool.js +213 -0
- package/lib/uml-usecase.js +532 -0
- package/lib/uml.js +2110 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +128 -6
- package/skills/model-everything/SKILL.md +450 -0
- package/skills/model-everything/references/dsh-model-schema.md +437 -0
- package/skills/model-everything/references/gap-routing-guide.md +42 -0
- package/skills/model-everything/references/hash-spec.md +152 -0
- package/skills/model-everything/references/logic-verification-guide.md +422 -0
- package/skills/model-everything-datamodel/SKILL.md +130 -0
- package/skills/model-everything-datamodel/references/data-model-guide.md +84 -0
- package/skills/model-everything-datamodel/references/data-model-schema.md +128 -0
- package/skills/model-everything-design/SKILL.md +115 -0
- package/skills/model-everything-interleaving/SKILL.md +63 -0
- package/skills/model-everything-interleaving/SKILL.zh-CN.md +60 -0
- package/skills/model-everything-interleaving/references/concurrency-risk-guide.md +63 -0
- package/skills/model-everything-report-reading/SKILL.md +137 -0
- package/skills/model-everything-report-reading/references/reading-a-report.md +166 -0
- package/skills/model-everything-report-reading/references/reading-a-report.mmd +59 -0
- package/skills/model-everything-structure/SKILL.md +96 -0
- package/skills/model-everything-structure/references/structure-review-guide.md +201 -0
- package/skills/model-everything-uml/SKILL.md +60 -0
- package/skills/model-everything-uml/references/uml-modeling-guide.md +551 -0
- package/src/baseline.ts +288 -0
- package/src/compose-tool.ts +82 -0
- package/src/concurrency-tool.ts +44 -0
- package/src/concurrency.ts +254 -0
- package/src/data-engine.ts +1208 -0
- package/src/data-tool.ts +91 -0
- package/src/design-engine.ts +1263 -0
- package/src/design-tool.ts +68 -0
- package/src/engine.ts +4564 -0
- package/src/export-tool.ts +81 -0
- package/src/exporters.ts +399 -0
- package/src/gate.ts +330 -0
- package/src/granularity.ts +312 -0
- package/src/index.ts +289 -0
- package/src/json-value.ts +20 -0
- package/src/model-file.ts +49 -0
- package/src/option-shape.ts +64 -0
- package/src/report-diff-tool.ts +67 -0
- package/src/report-view.ts +483 -0
- package/src/structure-tool.ts +93 -0
- package/src/structure.ts +786 -0
- package/src/tool.ts +103 -0
- package/src/uml-communication.ts +428 -0
- package/src/uml-composite.ts +520 -0
- package/src/uml-interaction-overview.ts +321 -0
- package/src/uml-profile.ts +439 -0
- package/src/uml-structure.ts +317 -0
- package/src/uml-syntax.ts +764 -0
- package/src/uml-timing.ts +1106 -0
- package/src/uml-tool.ts +230 -0
- package/src/uml-usecase.ts +566 -0
- package/src/uml.ts +2139 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# DSH Model Schema v1 — model_verify
|
|
2
|
+
|
|
3
|
+
The dsh-native `model_verify` tool accepts a structured JSON model. The engine runs the structural and adversarial checks (S1-S8, 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: `model_verify` uses `{ variable, values }` (a variable's boundary values for A5), while `model_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
|
+
### `_`-prefixed keys are annotation metadata (the one exception)
|
|
60
|
+
|
|
61
|
+
A key whose name starts with `_` is **not** part of the model. At any level — on the
|
|
62
|
+
model, a state, a transition, inside `narrative` — the schema skips it, the hash ignores
|
|
63
|
+
it, and the report lists it under `metadataKeys`:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"schemaVersion": 1,
|
|
68
|
+
"init": "IDLE",
|
|
69
|
+
"states": [{ "id": "IDLE", "_note": "power-on, not ready" }],
|
|
70
|
+
"_source": "docs/uml/order-service.model.json",
|
|
71
|
+
"_verified": { "modelHash": "0000000000000000…", "at": "2026-10-09" },
|
|
72
|
+
"_extraction_caveats": ["guard thresholds read from the ISR, not the task"]
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
"metadataKeys": ["_extraction_caveats", "_source", "_verified", "states[0]._note"]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The list echoes *this input*, it is not a memory: a `_` key the current object does not
|
|
81
|
+
carry is absent, even if an earlier version of the file had it. Nested keys are spelled
|
|
82
|
+
the way the walker sees them (`states[0]._note`, `invariants[0]._probe`,
|
|
83
|
+
`transitions[0]._why`), and the field is absent entirely when the input carries no `_`
|
|
84
|
+
key at all. Read it as "what this run ignored", never as "what this model has ever
|
|
85
|
+
recorded".
|
|
86
|
+
|
|
87
|
+
Why it exists: provenance, verification snapshots and extraction caveats belong *with*
|
|
88
|
+
the model. Pushed into a sidecar they drift away from it — nobody can say which hash
|
|
89
|
+
belongs to which note. With this exemption the archive record travels inside the file
|
|
90
|
+
and still cannot change the model's identity: `modelHash` excludes `_` keys, so the
|
|
91
|
+
model as archived and the model with the metadata stripped hash identically.
|
|
92
|
+
|
|
93
|
+
The exemption is namespaced, not a loosening of the closed schema. `_` keys are never
|
|
94
|
+
*read* by any check, so ignoring them cannot produce a false result; a mistyped
|
|
95
|
+
non-prefixed key (`sttes`, `gaurd`, `maximum`) is still a validation error, which is
|
|
96
|
+
the whole point of the closed schema.
|
|
97
|
+
|
|
98
|
+
### Reading a report: `ran`, `verdict`, `ok`
|
|
99
|
+
|
|
100
|
+
`model_verify` (and `model_compose_verify`) reports separate "the tool ran"
|
|
101
|
+
from "the review passed":
|
|
102
|
+
|
|
103
|
+
| Field | Meaning |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `schema` | The versioned report contract, e.g. `model-everything/verify/v1` — branch on this instead of sniffing fields. |
|
|
106
|
+
| `ok` | The engine produced a report. For `verify` this is `false` only when model *validation* failed. It is **not** a verdict. |
|
|
107
|
+
| `ran` | The tool executed. `false` only for a tool-level refusal (`errorCode`/`error` present). |
|
|
108
|
+
| `verdict` | `pass` \| `pass_with_findings` \| `fail`. Any `severity: "error"` finding — or a validation failure — makes this `fail`. |
|
|
109
|
+
| `verdictReason` | e.g. `1 error finding(s) (first: S2_NO_TRANSITIONS)`, `no error findings; 3 warning finding(s)`, `no error or warning findings`. |
|
|
110
|
+
| `hashSpec` | The published hash specification `modelHash` follows — see [hash-spec.md](hash-spec.md). |
|
|
111
|
+
| `hashes` | Every hash the report carries, in one place (`modelHash`, `beforeModelHash`/`afterModelHash` for a comparison). |
|
|
112
|
+
| `metadataKeys` | Present only when the input carried `_` keys (see above). |
|
|
113
|
+
| `narrativeCoverage` | Present only when the input carried a narrative: `covered/total` per dimension. |
|
|
114
|
+
| `nextSteps` | What to do next, derived from the findings; never empty, and identical for two runs over the same model. |
|
|
115
|
+
|
|
116
|
+
A caller that reads `ok` sees `true` for a model with a deadlock; the verdict is what
|
|
117
|
+
says so. Report the verdict, quote the counterexample path from the finding, and never
|
|
118
|
+
describe a `fail` as "the tool ran fine".
|
|
119
|
+
|
|
120
|
+
### References must resolve
|
|
121
|
+
|
|
122
|
+
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:
|
|
123
|
+
|
|
124
|
+
| Reference | Must name |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `states[].id`, `init` | a declared state (unique) |
|
|
127
|
+
| `transitions[].from` / `.to` | a declared state |
|
|
128
|
+
| `transitions[].updates[].variable`, `boundaryChecks[].variable` | a declared variable |
|
|
129
|
+
| `transitions[].guard` variables | a declared variable |
|
|
130
|
+
| `resourcePairs[].acquireEvent` / `.releaseEvent` / `.failEvent` | a declared event |
|
|
131
|
+
| `invariants[].event` (`event-before-state`) | a declared event |
|
|
132
|
+
| `invariants[].state` / `.states[]` / `.from` / `.target` / `.when.state` | a declared state |
|
|
133
|
+
| `invariants[].to` and every member of `invariants[].to[]` (`leads-to`) | a declared state |
|
|
134
|
+
|
|
135
|
+
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.
|
|
136
|
+
|
|
137
|
+
## Model narrative (natural-language context)
|
|
138
|
+
|
|
139
|
+
The model may carry a `narrative` block explaining, in natural language, what
|
|
140
|
+
every symbol means in the real scenario. It is what gets shown to the user when
|
|
141
|
+
the extracted model is presented for confirmation, and the report echoes it back
|
|
142
|
+
so findings can be read against real scenarios instead of bare ids.
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
"narrative": {
|
|
146
|
+
"states": {
|
|
147
|
+
"NEW": "订单已创建,等待支付",
|
|
148
|
+
"PAID": "已支付,等待发货",
|
|
149
|
+
"SHIPPED": "已发货,等待签收",
|
|
150
|
+
"DONE": "已完成(终态)",
|
|
151
|
+
"CANCELLED": "已取消(终态)"
|
|
152
|
+
},
|
|
153
|
+
"events": {
|
|
154
|
+
"pay": "买家完成支付",
|
|
155
|
+
"ship": "仓库发货",
|
|
156
|
+
"deliver": "买家签收",
|
|
157
|
+
"cancel": "取消订单"
|
|
158
|
+
},
|
|
159
|
+
"scenarios": [
|
|
160
|
+
{ "from": "NEW", "event": "pay", "scenario": "下单后支付成功,订单进入待发货" },
|
|
161
|
+
{ "from": "PAID", "event": "ship", "scenario": "已支付订单发货,进入运输中" },
|
|
162
|
+
{ "from": "SHIPPED", "event": "deliver", "scenario": "签收完成,订单结束" },
|
|
163
|
+
{ "from": "NEW", "event": "cancel", "scenario": "未支付订单被取消" },
|
|
164
|
+
{ "from": "PAID", "event": "cancel", "scenario": "已支付订单取消并退款" }
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Coverage contract**: a narrative may cover **part** of the model — writing the states
|
|
170
|
+
first, then the events, then the scenarios is the natural authoring order, and a partial
|
|
171
|
+
block is valid. What must hold is that whatever is written is *right*:
|
|
172
|
+
|
|
173
|
+
| Case | Result |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `narrative` absent | Valid. The review reports `UML013_NO_NARRATIVE` (info); `model_verify` reports no coverage at all. |
|
|
176
|
+
| One or two of `states` / `events` / `scenarios` given | Valid. The report adds `narrativeCoverage: {states: "5/5", events: "3/9", scenarios: "0/12"}` (covered/total per dimension), and `nextSteps` names the gap. Both sides also report it as a finding: `model_verify` carries `NARRATIVE_PARTIAL` (info) and a UML review carries `UML027_NARRATIVE_PARTIAL` (info). An `info` finding never changes `verdict`, `summary.errors`, `summary.warnings` or the check count. |
|
|
177
|
+
| `narrative` present but `{}` | **Error**: an empty block claims documentation that does not exist. Give at least one dimension. |
|
|
178
|
+
| Unknown state/event id, empty description, duplicate `(from, event)` scenario | **Error**: the narrative is wrong, not merely incomplete. |
|
|
179
|
+
|
|
180
|
+
Totals are the model's own: states = declared states, events = distinct transition
|
|
181
|
+
events, scenarios = distinct `(from, event)` groups (so two guarded transitions of one
|
|
182
|
+
event need one scenario, not two). The report's `narrative` field echoes the block
|
|
183
|
+
unchanged.
|
|
184
|
+
|
|
185
|
+
**Presenting the model**: when showing the extracted model for confirmation, render
|
|
186
|
+
the natural language INLINE in the model presentation, not as a separate block.
|
|
187
|
+
Three rendering forms (all derive from the same `narrative` data):
|
|
188
|
+
|
|
189
|
+
- **Form A — integrated transition table (default)**: one row per transition with
|
|
190
|
+
state/event meanings in parentheses and the scenario as the last column. Keep
|
|
191
|
+
meanings short (state/event ≤ 6 characters, scenario ≤ 10) and estimate row
|
|
192
|
+
width (CJK counts as 2) so rows fit the display area — a wrapped row loses
|
|
193
|
+
column alignment and readability collapses.
|
|
194
|
+
- **Form B — sentence blocks (reading-accessible)**: scenario sentence first, then
|
|
195
|
+
a fixed three-line frame (状态…/发生…/进入…). Use for detailed confirmation,
|
|
196
|
+
users with reading difficulties, or ≤ 10 transitions.
|
|
197
|
+
- **Form C — grouped by source state (large machines / narrow panes)**: one
|
|
198
|
+
section per state, each rendered as its own small 3-column table
|
|
199
|
+
(`event(含义)| NEXT(含义)| 场景`); no cross-group column alignment to
|
|
200
|
+
track. Use for ≥ 15 transitions or narrow display areas; if a group table would
|
|
201
|
+
still wrap, fall back to a one-line-per-event bullet list for that group.
|
|
202
|
+
|
|
203
|
+
## Guards
|
|
204
|
+
|
|
205
|
+
A guard is exactly one of:
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{ "variable": "retry", "op": "<", "value": 3 }
|
|
209
|
+
{ "all": [ { "variable": "armed", "op": "==", "value": true }, { "variable": "retry", "op": ">", "value": 0 } ] }
|
|
210
|
+
{ "any": [ { "variable": "mode", "op": "==", "value": 1 }, { "variable": "mode", "op": "==", "value": 2 } ] }
|
|
211
|
+
{ "not": { "variable": "locked", "op": "==", "value": true } }
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
- Boolean variables only support `==` / `!=`.
|
|
215
|
+
- 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.
|
|
216
|
+
- 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.
|
|
217
|
+
|
|
218
|
+
## Updates
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{ "variable": "retry", "op": "inc", "value": 1 }
|
|
222
|
+
{ "variable": "retry", "op": "set", "value": 0 }
|
|
223
|
+
{ "variable": "retry", "op": "dec", "value": 1 }
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`inc`/`dec` default to 1 when `value` is omitted. `set` defaults to 0.
|
|
227
|
+
|
|
228
|
+
`inc` and `dec` are **never clamped** to the variable's declared `min`/`max`. The declared
|
|
229
|
+
range is a property the checks verify (S7 `var-in-range`), not a bound on the search: a
|
|
230
|
+
counter whose guard does not pin it ranges past its own declaration, so the reachable set
|
|
231
|
+
is unbounded however narrow the declaration looks. S8 reports that as
|
|
232
|
+
`S8_DOMAIN_NOT_ENFORCED` and names the transition, rather than clamping the value and
|
|
233
|
+
deleting the states a range violation is witnessed by. A `set` constant outside the
|
|
234
|
+
declared range contradicts the declaration and is `S8_DOMAIN_SET_OUTSIDE` (error).
|
|
235
|
+
|
|
236
|
+
To bound a counter, add a guard that pins it — `"max": 4` with a guard
|
|
237
|
+
`{ "variable": "retry", "op": "<", "value": 4 }` — not a narrower `max`.
|
|
238
|
+
|
|
239
|
+
## Invariants
|
|
240
|
+
|
|
241
|
+
Every invariant carries `id` and **`description`** — both required strings — plus a
|
|
242
|
+
`kind` and that kind's own fields. `description` is not decoration: it is echoed into
|
|
243
|
+
the finding a violation produces, and it takes part in `modelHash`, so a model that
|
|
244
|
+
gains one hashes differently. An invariant without it is rejected with `MODEL_INVALID`,
|
|
245
|
+
and the report's `nextSteps` names the field to add; a `_`-prefixed key is not a
|
|
246
|
+
substitute for it.
|
|
247
|
+
|
|
248
|
+
| Kind | Shape | Checks |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| `never-states` | `{ states: ["ERROR"] }` | No reachable runtime state may be in the forbidden set |
|
|
251
|
+
| `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) |
|
|
252
|
+
| `event-before-state` | `{ event: "power_ready", state: "ACTIVE" }` | Every path entering `state` must have passed through `event` first |
|
|
253
|
+
| `leads-to` | `{ from: "MIGRATING", to: "DONE" }`, or `to: ["DONE", "FAILED"]` | Every path from `from` must eventually reach `to`. With a target set, every path must reach at least one member. The set must be non-empty and free of duplicates |
|
|
254
|
+
| `sequence` | `{ events: ["backup", "modify", "commit"] }` | Events must occur in the given order |
|
|
255
|
+
| `atomicity` | `{ events: ["write"], commit: "commit", rollback?: "rollback" }` | Atomic group must end with commit/rollback before leaving scope |
|
|
256
|
+
| `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 |
|
|
257
|
+
| `probability` | `{ target, op: one of >= <= > <, p }` | P(ever hitting target) must satisfy the bound (A13, DTMC from transition `weight`, default 1; value iteration) |
|
|
258
|
+
|
|
259
|
+
A7 reports a violating path for each failed invariant, and an empty path means the initial state already violates it. The state-predicate kinds, `sequence`, `atomicity` and `budget` are breadth-first, so their witness is the shortest one; `leads-to` and `probability` report a witness without claiming minimality.
|
|
260
|
+
|
|
261
|
+
### Scoping a range to a state (`when`)
|
|
262
|
+
|
|
263
|
+
`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.
|
|
264
|
+
|
|
265
|
+
```json
|
|
266
|
+
{ "id": "depth-in-probe", "description": "probe depth stays bounded while in PROBE",
|
|
267
|
+
"kind": "var-in-range", "variable": "depth", "min": 0, "max": 4,
|
|
268
|
+
"when": { "state": "PROBE" } }
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Two scope shapes are accepted:
|
|
272
|
+
|
|
273
|
+
- `{ "state": "PROBE" }` — the state-scoped form, for "this range applies only here".
|
|
274
|
+
- 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.
|
|
275
|
+
|
|
276
|
+
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.
|
|
277
|
+
|
|
278
|
+
`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.
|
|
279
|
+
|
|
280
|
+
## Permission presets and interaction mode
|
|
281
|
+
|
|
282
|
+
| Preset | sandbox | approval | plugin behavior |
|
|
283
|
+
|---|---|---|---|
|
|
284
|
+
| `workspace-write` | workspace-write | ask | Evidence stays in workspace; model confirmation defaults to ask |
|
|
285
|
+
| `danger-full-access` | danger-full-access | never | Full file access; interaction resolves to auto; never request sandbox escalation |
|
|
286
|
+
| custom | any | any | The session folds the last `sandbox/mode` and `approval/policy` events; interaction follows approval only |
|
|
287
|
+
|
|
288
|
+
Confirm the extracted model before treating it as fact. When the user has already told you to proceed without confirmation in this session, round-trip the extracted model into a transition table, compare it against the source extraction, and mark the report `UNCONFIRMED`.
|
|
289
|
+
|
|
290
|
+
## Cost and budget (A12)
|
|
291
|
+
|
|
292
|
+
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:
|
|
293
|
+
|
|
294
|
+
```json
|
|
295
|
+
{
|
|
296
|
+
"id": "dispatch-budget",
|
|
297
|
+
"description": "worst-case dispatch path stays within 100 cycles",
|
|
298
|
+
"kind": "budget",
|
|
299
|
+
"budget": 100
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
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`.
|
|
304
|
+
|
|
305
|
+
## Probability reachability (A13)
|
|
306
|
+
|
|
307
|
+
Declare `weight` on transitions (default 1) to interpret the machine as a DTMC, then add a `probability` invariant:
|
|
308
|
+
|
|
309
|
+
```json
|
|
310
|
+
{ "id": "reliability", "description": "at least 90% of runs reach SAFE", "kind": "probability", "target": "SAFE", "op": ">=", "p": 0.9 }
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
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.
|
|
314
|
+
|
|
315
|
+
## Deadline check (A14)
|
|
316
|
+
|
|
317
|
+
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:
|
|
318
|
+
|
|
319
|
+
```json
|
|
320
|
+
{ "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"] }
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
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`.
|
|
324
|
+
|
|
325
|
+
## State entry/exit actions (onEntry / onExit)
|
|
326
|
+
|
|
327
|
+
States may declare ordered action-name lists that fire automatically:
|
|
328
|
+
|
|
329
|
+
```json
|
|
330
|
+
{ "id": "ACTIVE", "onEntry": ["sync_lock"], "onExit": ["sync_unlock"] }
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
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.
|
|
334
|
+
|
|
335
|
+
## Composition verification
|
|
336
|
+
|
|
337
|
+
Two or more machines can be checked together with `runCompositionVerification` (DSH tool `model_compose_verify`):
|
|
338
|
+
|
|
339
|
+
- non-rendezvous events advance exactly one firing machine;
|
|
340
|
+
- 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. A machine whose alphabet does not contain the event does not participate, and a terminal machine is stopped;
|
|
341
|
+
- therefore a machine that is *waiting* for a rendezvous is not blocked from everything else: it may take any number of its own non-rendezvous steps first. That is how different rates are modelled — there is **no rate ratio** and no fairness bound, so "the 1 ms pump runs N times while the ISR waits" is expressed by the pump's own tick event, not by a tempo setting. A bound would only prune interleavings, never add behaviour; if you need to *forbid* a fast machine from overtaking, encode that in the model (a guard on a counter the slow machine resets), not in the composition options.
|
|
342
|
+
- **there is no cross-machine shared state.** Each machine's `variables` are private to
|
|
343
|
+
it: machine A's `ready` and machine B's `ready` are two different
|
|
344
|
+
quantities, and no guard can read another machine's variable. Rendezvous events are
|
|
345
|
+
the *only* coupling between machines. So "two machines must agree on one predicate"
|
|
346
|
+
cannot be modelled by giving each a copy of it — model the shared condition inside a
|
|
347
|
+
single machine, or make the agreement an explicit handshake. The report names the
|
|
348
|
+
variables it saw when two or more machines each declare some, in `nextSteps`.
|
|
349
|
+
|
|
350
|
+
Inputs: the DSH tool takes an array of model objects (an agent reads the files and passes them); the CLI takes any number of files directly — `compose models/*.json --rendezvous req,ack` — so nothing has to be concatenated by hand.
|
|
351
|
+
|
|
352
|
+
### Reading C1 and C2
|
|
353
|
+
|
|
354
|
+
Both findings carry the *why*, not just the *what*:
|
|
355
|
+
|
|
356
|
+
- **C1_COMPOSITION_DEADLOCK** — the composite state, the number of steps to reach it (**shortest**, because the search is breadth-first), and `evidence.perMachine`: for every machine, its state, whether it is terminal, and every event in its alphabet with a reason — `rendezvous-needs-partner` (fewer than two machines declare it), `rendezvous-partner-not-ready` (a declaring partner is terminal or has no enabled transition), `no-enabled-transition` (guard false or no such transition), or `enabled`. An event this machine can fire on its own is never labelled `enabled` when the handshake itself is missing. In practice a census only ever shows the three blocked reasons: it is emitted only when no machine can move, and "can move" is decided by the same predicate that would label an event `enabled`, so that fourth label is unreachable there. Treat the three observable values as the contract, and keep tolerating the literal.
|
|
357
|
+
- **C2_RENDEZVOUS_NEVER_FIRES** — an **error**, the same severity as C1: a handshake that can never fire leaves the composition unable to advance through it, so a gate that reads only `verdict` stops on it. `evidence.machines` gives, per machine, whether it declares the event, whether it ever had it enabled, and the states where it did. The reason distinguishes "fewer than two machines declare it" from "machine *i* declares it but never enables it".
|
|
358
|
+
|
|
359
|
+
`maxStates` caps the product space, and the cap is **reported, never hidden**: a truncated run cannot claim "no deadlock reachable" (the C1 check detail says so), and a C2 finding gained under truncation carries "may be an artefact of the cap" in its message. Bump `maxStates` before believing a negative result on a large product. Note that a machine with an unbounded counter produces an infinite composite space and will always truncate.
|
|
360
|
+
|
|
361
|
+
## Minimal example
|
|
362
|
+
|
|
363
|
+
```json
|
|
364
|
+
{
|
|
365
|
+
"schemaVersion": 1,
|
|
366
|
+
"init": "INIT",
|
|
367
|
+
"states": [
|
|
368
|
+
{ "id": "INIT" },
|
|
369
|
+
{ "id": "RETRY" },
|
|
370
|
+
{ "id": "FATAL", "terminal": true }
|
|
371
|
+
],
|
|
372
|
+
"transitions": [
|
|
373
|
+
{ "from": "INIT", "event": "timeout", "guard": { "variable": "retry", "op": "<", "value": 3 }, "to": "RETRY", "updates": [{ "variable": "retry", "op": "inc" }] },
|
|
374
|
+
{ "from": "INIT", "event": "timeout", "guard": { "variable": "retry", "op": ">=", "value": 3 }, "to": "FATAL" }
|
|
375
|
+
],
|
|
376
|
+
"variables": [{ "name": "retry", "kind": "integer", "init": 0, "min": 0, "max": 3 }],
|
|
377
|
+
"boundaryChecks": [{ "variable": "retry", "values": [0, 1, 2, 3] }]
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Before/after comparison (D1-D4)
|
|
382
|
+
|
|
383
|
+
When `beforeModel` is passed to `model_verify`, the engine treats `model` as AFTER and runs four extra checks after S1-A11:
|
|
384
|
+
|
|
385
|
+
| Check | Purpose |
|
|
386
|
+
|---|---|
|
|
387
|
+
| D1 Behavioral Preservation | Every BEFORE (state, event) that could fire must still be fireable from the mapped AFTER state |
|
|
388
|
+
| D2 Invariant Continuity | Every BEFORE invariant (mapped through `stateMapping`) must still hold in AFTER. State predicates go through the same reachability search as S7; `leads-to`, `sequence`, `atomicity`, `budget` and `probability` are re-decided by the searches their own probes (A9-A13) use |
|
|
389
|
+
| D3 Regression Delta | Lists added/removed states, events, and transitions |
|
|
390
|
+
| D4 Deadlock/Liveness Regression | New deadlock states or closed SCCs not present in BEFORE |
|
|
391
|
+
|
|
392
|
+
`stateMapping` maps BEFORE state ids to AFTER state ids. Omit it when state names are unchanged.
|
|
393
|
+
|
|
394
|
+
It is checked at the tool boundary, because every wrong shape of it used to read as "no mapping at all": a
|
|
395
|
+
string, a list, or a map whose values are not state ids is refused with `VERIFY_OPTION_INVALID` and a message
|
|
396
|
+
naming the entry. A mapping passed without `beforeModel` is refused too — there is no BEFORE model to map from,
|
|
397
|
+
and the caller plainly meant to compare.
|
|
398
|
+
|
|
399
|
+
Example tool call shape:
|
|
400
|
+
|
|
401
|
+
```json
|
|
402
|
+
{
|
|
403
|
+
"model": { "...": "AFTER LogicModelV1" },
|
|
404
|
+
"beforeModel": { "...": "BEFORE LogicModelV1" },
|
|
405
|
+
"stateMapping": { "OLD_INIT": "INIT", "OLD_ACTIVE": "ACTIVE" }
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
The report's `comparison` object includes both model hashes, state/transition counts, and delta arrays.
|
|
410
|
+
|
|
411
|
+
## Idempotent replay (A8)
|
|
412
|
+
|
|
413
|
+
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.
|
|
414
|
+
|
|
415
|
+
## Advanced constraints (S6, S8, A9-A14)
|
|
416
|
+
|
|
417
|
+
- **S6 Guard Completeness** also reports `S6_DEAD_GUARD`: a guarded branch no reachable
|
|
418
|
+
variable value can satisfy is a branch that can never fire. The test runs over the
|
|
419
|
+
values the model can produce — each variable's `init` plus every `set` constant — not
|
|
420
|
+
over the declared domain, so a guard on a value the model never writes is satisfiable on
|
|
421
|
+
paper and dead in practice.
|
|
422
|
+
- **S8 Variable Declarations**: declare `monotonic: "inc"|"dec"` on a variable; updates must not move in the opposite direction. The same check reads the declared range: `S8_DOMAIN_NOT_ENFORCED` reports an `inc`/`dec` no guard keeps inside `min`/`max`, and `S8_DOMAIN_SET_OUTSIDE` (error) reports a `set` constant outside it.
|
|
423
|
+
- **A9 Leads-To**: `{ kind: "leads-to", from, to }` — every path from `from` must eventually reach `to`. `to` is one state id or a non-empty array of state ids for "reach any one of these"; a duplicated member is a validation error rather than a shorthand. Either way the property stays universal: one branch that loops forever, or that stops before every target, refutes it.
|
|
424
|
+
- **A10 Sequence**: `{ kind: "sequence", events }` — events must appear in order.
|
|
425
|
+
- **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.
|
|
426
|
+
- **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.
|
|
427
|
+
- **A13 Probability**: `{ kind: "probability", target, op, p }` — P(ever hitting target) must satisfy the bound under the DTMC induced by transition `weight` (default 1).
|
|
428
|
+
- **A14 Deadline**: state `maxTicks` + top-level `tickEvents` — a tick step may not keep a state resident past its deadline; reports the over-residency path.
|
|
429
|
+
|
|
430
|
+
## Limits
|
|
431
|
+
|
|
432
|
+
- Each breadth-first exploration caps at `maxStates` (default 10000). The leads-to walk caps at twenty times that, **shared across every start state of one check**: it is depth-first over run states, and an unbounded counter makes every one of them distinct, so it has no shorter bound available.
|
|
433
|
+
- A cap that bites is **reported, never silently passed**. The report sets `summary.truncated`, and the check that stopped carries a finding whose code ends in `_UNDECIDED`. An undecided check has proven neither the violation nor its absence; read it as "not decided", never as a pass.
|
|
434
|
+
- A3 samples the first `maxPermutationEvents` events (default 5).
|
|
435
|
+
- The engine is a finite-state model checker. It cannot prove properties of the real implementation; follow with code-level review.
|
|
436
|
+
- `cost` values are modeler-provided static labels — A12 verifies against them; real execution time/WCET needs binary-level timing analysis.
|
|
437
|
+
- 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,42 @@
|
|
|
1
|
+
# Gap Routing Guide
|
|
2
|
+
|
|
3
|
+
This plugin 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 this plugin covers today | Claim examples | Dedicated tooling | Why not here |
|
|
12
|
+
|---|---|---|---|---|
|
|
13
|
+
| Hard real time (timing) | Order, counts, path budgets (A12), and deadlines over discrete ticks (A14). Time otherwise appears as counters and events | "500 ms to SAFE", "no deadline miss", "period ≤ 1 ms" | UPPAAL, IMITATOR (timed automata); binary-level timing analysis for WCET | No continuous clocks. A deadline that is not a tick count cannot be expressed |
|
|
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 | `model_compose_verify` check machines together: C1 composition deadlock, C2 a rendezvous that never fires. They stop at one handshake event per rendezvous | "A sends E, B always handles E", "these two never deadlock" | CSP (FDR), mCRL2, TLA+ (compositional models) | No payloads, no rate ratio and no fairness bound; each machine's own step order is free |
|
|
19
|
+
| Protocol semantics (message contents, channels, per-message ordering) | Events and handshakes between machines | "QoS 1 delivers exactly once", "the broker never reorders", "the queue never overflows" | TLA+, mCRL2, SPIN (Promela channels) | The model has states and events, not messages, queues or payloads |
|
|
20
|
+
| Nested/hierarchical statecharts | Flat models only; flatten before verification | parent/child states, orthogonal regions | SCXML / Stateflow (native hierarchy) | A flat state list only. `model_compose_verify` places machines side by side; it does not nest states. Flatten manually and re-confirm |
|
|
21
|
+
| Write-site / single-owner analysis | **Not covered, and not planned.** this toolbox reviews the diagram and the model; it does not read the source to see *who writes* a variable, a register or a field | "only the ISR writes this", "one owner per flag", "no double write to the CAN mailbox" | A source-side AST/rule check (e.g. the project's own include/arch checker), a static analyzer (clang-tidy, PC-lint, Coverity), or a model checker over the real code (CBMC) | It needs the source and a rule table about statements — a different input than a model or a diagram. `model_interleaving_scan` will flag the *claim* as unverified and route it here, which is the honest part of this plugin's answer |
|
|
22
|
+
|
|
23
|
+
## Where the routing surfaces
|
|
24
|
+
|
|
25
|
+
- `model_verify` report: a model whose state/event/action names match the timing,
|
|
26
|
+
preemption, hybrid, or probability vocabulary gets informational `coverageNotes`
|
|
27
|
+
with the same routing as above.
|
|
28
|
+
- A12 advisory: transitions declaring `cost` without a `budget` invariant produce
|
|
29
|
+
`A12_COST_WITHOUT_BUDGET`, pointing at the budget check.
|
|
30
|
+
- `model_interleaving_scan`: absolute synchrony/async/concurrency claims (thread-safe, lock-free, non-blocking,
|
|
31
|
+
interrupt-safe, ...) carry `suggestions` naming dedicated tools.
|
|
32
|
+
- Manual review: when reading a plan, match its claims against the table above and
|
|
33
|
+
require dedicated evidence before accepting "always"/"never"/"guaranteed" language
|
|
34
|
+
- Generators: `model_export` emit
|
|
35
|
+
native inputs for UPPAAL (.xta + queries), TLA+ (TLC spec + safety), PRISM (.pm + .pctl),
|
|
36
|
+
and SPIN (Promela + ltl) from a LogicModelV1. It translates the core machine (guards,
|
|
37
|
+
updates, weights; booleans as integers) and returns unrepresentable invariants as explicit
|
|
38
|
+
warnings. A field the target has no place for is reported the same way (`EXPORT_FIELD_DROPPED`
|
|
39
|
+
names `cost` and `maxTicks`), and a model too large for the PRISM enumeration is refused as
|
|
40
|
+
`EXPORT_TOO_LARGE` rather than as an invalid model. The UPPAAL export is one template, so its
|
|
41
|
+
transitions carry the event name as a comment rather than as a rendezvous: `e!` waits for a
|
|
42
|
+
partner this file does not contain.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Model Hash Specification
|
|
2
|
+
|
|
3
|
+
`modelHash` is how a report, an archive record and a CI comparison agree on *which*
|
|
4
|
+
model was verified. A hash nobody can recompute is decoration, so the algorithm, the
|
|
5
|
+
normalization rules and the excluded keys are published here — and pinned by golden
|
|
6
|
+
tests (`tests/engine/run.mjs`).
|
|
7
|
+
|
|
8
|
+
## Published specifications
|
|
9
|
+
|
|
10
|
+
| Spec | Since | Normalization |
|
|
11
|
+
|------|-------|---------------|
|
|
12
|
+
| `v1` | 1.0.0 | sorted keys, no insignificant whitespace, UTF-8 with non-ASCII preserved, **every `_`-prefixed key removed at every level** |
|
|
13
|
+
| `v0` | never published | the same normalization **without** metadata filtering — the pre-1.0 behaviour, kept only so an archived hash can be checked against the old engine |
|
|
14
|
+
|
|
15
|
+
`v0` was never a specification; it is the behaviour that existed before this document
|
|
16
|
+
did. Do not record new hashes with it.
|
|
17
|
+
|
|
18
|
+
For a model that carries no `_`-prefixed key, `v0` and `v1` produce the *identical*
|
|
19
|
+
hash. That is why adopting `v1` does not invalidate any hash recorded before it.
|
|
20
|
+
|
|
21
|
+
## The algorithm (`v1`)
|
|
22
|
+
|
|
23
|
+
1. Take the model as JSON.
|
|
24
|
+
2. Remove every key whose name starts with `_`, at every level — including objects
|
|
25
|
+
nested inside arrays.
|
|
26
|
+
3. Serialize deterministically:
|
|
27
|
+
- object keys sorted by code-unit order,
|
|
28
|
+
- tokens separated by `,` and `:` with no whitespace,
|
|
29
|
+
- strings escaped as JSON, non-ASCII preserved (`réussi` stays `réussi`, not `r\u00e9ussi`),
|
|
30
|
+
- numbers written the way JavaScript `JSON.stringify` writes them,
|
|
31
|
+
- `null`, `true`, `false` spelled as JSON.
|
|
32
|
+
4. `sha256` of the UTF-8 bytes, lower-case hex.
|
|
33
|
+
|
|
34
|
+
In other words, with `strip_metadata` removing `_`-prefixed keys at every level:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
sha256(json.dumps(strip_metadata(model), sort_keys=True, separators=(',', ':'), ensure_ascii=False))
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The TypeScript engine does not call `json.dumps`; it implements the same serialization
|
|
41
|
+
directly (`stableStringify`). The specification, not either implementation, is the
|
|
42
|
+
contract.
|
|
43
|
+
|
|
44
|
+
Nothing else is normalized: no key renaming, no default filling, no unit conversion,
|
|
45
|
+
no ordering of `states` or `transitions` arrays. Two models that differ only in array
|
|
46
|
+
order hash differently — that is intended, because the order is part of the input.
|
|
47
|
+
|
|
48
|
+
## Recomputing a hash
|
|
49
|
+
|
|
50
|
+
Both commands below reproduce the engine's value exactly.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Node
|
|
54
|
+
node -e "const m=JSON.parse(require('fs').readFileSync('model.json','utf8'));const s=(v)=>Array.isArray(v)?v.map(s):v&&typeof v==='object'?Object.fromEntries(Object.keys(v).sort().filter((k)=>!k.startsWith('_')).map((k)=>[k,s(v[k])])):v;console.log(require('crypto').createHash('sha256').update(JSON.stringify(s(m))).digest('hex'))"
|
|
55
|
+
|
|
56
|
+
# Node.js
|
|
57
|
+
node -e "const c=require('node:crypto'),f=require('node:fs');const s=v=>Array.isArray(v)?'['+v.map(s).join(',')+']':(v&&typeof v==='object'?'{'+Object.keys(v).filter(k=>!k.startsWith('_')).sort().map(k=>JSON.stringify(k)+':'+s(v[k])).join(',')+'}':JSON.stringify(v));console.log(c.createHash('sha256').update(s(JSON.parse(f.readFileSync('model.json','utf8')))).digest('hex'))"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The reports carry the specification next to the value, e.g. `"hashSpec": "v1"`, so an
|
|
61
|
+
archive record does not need to guess:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{ "hashSpec": "v1", "modelHash": "f583202888b827922ac512a312758fa2d8cf9055b102205ecad18f81a0a7d539" }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Round-trip reports (`uml_review`, `roundTrip`) carry `hashSpec`
|
|
68
|
+
alongside `modelHash` and `parsedHash`, so a fidelity comparison is always
|
|
69
|
+
spec-qualified.
|
|
70
|
+
|
|
71
|
+
**Read `diffs`, not `ok`.** `roundTrip.ok` says only that the model was rendered and the
|
|
72
|
+
result read back; it is `true` while `modelHash` and `parsedHash` legitimately differ,
|
|
73
|
+
because the model hash covers what a diagram cannot carry (the `narrative` block, in
|
|
74
|
+
particular) and the parsed hash covers what the diagram actually says. Fidelity is
|
|
75
|
+
`roundTrip.diffs.length === 0`. Equal hashes are neither necessary nor sufficient for
|
|
76
|
+
fidelity, and the two must not be forced to agree.
|
|
77
|
+
|
|
78
|
+
Every report also aggregates its hashes under `hashes`, so a baseline diff or an archive
|
|
79
|
+
record reads one field instead of walking the report:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{ "hashes": { "hashSpec": "v1", "modelHash": "f5832028…", "afterModelHash": "f5832028…" } }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
A structure review hashes its inputs under the same rule but with its own keys:
|
|
86
|
+
`hashes.diagram` (sha256 of the diagram text) and `hashes.matrix` (sha256 of the
|
|
87
|
+
canonical — key-sorted — matrix JSON, so key order does not change the hash) when a
|
|
88
|
+
dependency matrix was supplied. A data-model report carries `hashes.modelHash`; the
|
|
89
|
+
data-model hash uses the same normalization but its schema is not covered by the
|
|
90
|
+
published specification.
|
|
91
|
+
|
|
92
|
+
A `diagram` hash is a **text** hash, not a semantic one: `sha256` over the exact
|
|
93
|
+
characters, so comments and `note` blocks change it, and so does a trailing newline.
|
|
94
|
+
Two renderings that mean the same thing hash differently when their text differs, and
|
|
95
|
+
that is intended — it is what makes "the diagram I reviewed" checkable. For one model
|
|
96
|
+
(`modelHash` `77a43a39822147707229638515cf8f0c6bef4b08635ea6197d2181fc6c26f0b4`) four
|
|
97
|
+
probe variants hash as:
|
|
98
|
+
|
|
99
|
+
| Diagram text | `hashes.diagram` |
|
|
100
|
+
|--------------|------------------|
|
|
101
|
+
| clean | `25ee1d0b803c9477d526d92c1dbb9059e408e282e515499dba06d14630286c8d` |
|
|
102
|
+
| one `%%` comment line | `8c41f046c954e1dc086eee8bd24c3d1d033b489840895520002c1e8120dda052` |
|
|
103
|
+
| one `'` comment line (PlantUML style) | `5552a91a73972edb71ea98f460fa6a393c6556bd1241461f0f29cd51531e9e56` |
|
|
104
|
+
| one multi-line `note … end note` block | `359182313f451713a937f5148328d238e8ea1941fbcef663c4c34e0a7f1bbcda` |
|
|
105
|
+
|
|
106
|
+
Compare diagram hashes only between identical byte strings: the same diagram handed
|
|
107
|
+
over without its trailing newline hashes differently (`8c41f046…` becomes
|
|
108
|
+
`c8f3a41b…`). That is the specification, not drift.
|
|
109
|
+
|
|
110
|
+
## Checking an archived hash
|
|
111
|
+
|
|
112
|
+
`--hash-spec` recomputes under a named specification; `--hash-check` answers whether a
|
|
113
|
+
recorded value belongs to any published specification *at all*:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# verify under a named spec
|
|
117
|
+
The repository CLI takes the same flag: `verify model.json --hash-spec v0`
|
|
118
|
+
|
|
119
|
+
# ask about a recorded hash: prints {checked, matches, published[], verdict, verdictReason}
|
|
120
|
+
The repository CLI reports which spec reproduces an archived hash: `verify model.json --hash-check 0000…deadbeef`
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`--hash-check` exits `0` when a published spec reproduces the value and `2` when none
|
|
124
|
+
does, with the explicit answer `no published hash spec reproduces <hex>`. It never
|
|
125
|
+
guesses a spec to make a number match.
|
|
126
|
+
|
|
127
|
+
The in-process equivalents are `modelHash(model, spec)` (TypeScript,
|
|
128
|
+
`lib/engine.js`) and `model_hash(model, spec)`, plus `hashPayload(model, spec)`
|
|
129
|
+
and `hash_payload(model, spec)` for the exact bytes that are hashed.
|
|
130
|
+
|
|
131
|
+
## When the hash changes
|
|
132
|
+
|
|
133
|
+
A change that alters the bytes alters the hash, and that is a compatibility event:
|
|
134
|
+
|
|
135
|
+
- Adding, removing or renaming an *excluded* key (`_`-prefixed): no hash change by
|
|
136
|
+
construction.
|
|
137
|
+
- Adding a declared model field that a model actually uses: that model's hash changes.
|
|
138
|
+
The change must be named in the release note, and it is covered by the golden tests
|
|
139
|
+
rather than discovered by a user comparing snapshots.
|
|
140
|
+
- Changing any normalization rule (separator, escaping, key order, exclusion set): a
|
|
141
|
+
**new** specification, never a silent change to `v1`. The old spec keeps its name and
|
|
142
|
+
its behaviour so archived hashes stay checkable.
|
|
143
|
+
|
|
144
|
+
`--hash-spec` changes the reported `modelHash` only. The `beforeModelHash` /
|
|
145
|
+
`afterModelHash` pair inside a `comparison` block is a diagnostic of one run and always
|
|
146
|
+
uses `v1`.
|
|
147
|
+
|
|
148
|
+
### Version history
|
|
149
|
+
|
|
150
|
+
| Release | Hash change | Affected models |
|
|
151
|
+
|---------|-------------|-----------------|
|
|
152
|
+
| 1.0.0 | `hashSpec: "v1"` published; `_`-prefixed keys are now excluded from the hash (and accepted by the schema) | **None.** Before 1.0.0 a model carrying `_` keys was rejected outright, so no accepted model's hash moves. `v0` reproduces the old behaviour for archived values, and the two specs are byte-identical for a model without `_` keys — pinned by golden tests on `happy-path.json` and `narrative-complete.json`. |
|