dsh-logicprobe 0.8.1 → 1.0.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.
Files changed (65) hide show
  1. package/README.en-US.md +79 -14
  2. package/README.md +79 -14
  3. package/lib/baseline.js +142 -0
  4. package/lib/client.js +56 -1
  5. package/lib/compose-tool.js +1 -1
  6. package/lib/concurrency-tool.js +1 -1
  7. package/lib/concurrency.js +14 -0
  8. package/lib/data-engine.js +42 -8
  9. package/lib/engine.js +609 -118
  10. package/lib/export-tool.js +27 -2
  11. package/lib/granularity.js +237 -0
  12. package/lib/index.js +63 -32
  13. package/lib/report-diff-tool.js +39 -0
  14. package/lib/structure-tool.js +64 -0
  15. package/lib/structure.js +648 -0
  16. package/lib/tool.js +3 -3
  17. package/lib/types/baseline.d.ts +69 -0
  18. package/lib/types/concurrency.d.ts +10 -0
  19. package/lib/types/data-engine.d.ts +13 -1
  20. package/lib/types/engine.d.ts +139 -2
  21. package/lib/types/granularity.d.ts +106 -0
  22. package/lib/types/index.d.ts +9 -8
  23. package/lib/types/report-diff-tool.d.ts +8 -0
  24. package/lib/types/structure-tool.d.ts +10 -0
  25. package/lib/types/structure.d.ts +156 -0
  26. package/lib/types/tool.d.ts +2 -2
  27. package/lib/types/uml.d.ts +89 -0
  28. package/lib/uml-tool.js +36 -7
  29. package/lib/uml.js +217 -23
  30. package/locale/en.json +6 -0
  31. package/locale/zh.json +6 -0
  32. package/package.json +16 -14
  33. package/skills/logicprobe/SKILL.md +97 -58
  34. package/skills/logicprobe/references/dsh-model-schema.md +87 -15
  35. package/skills/logicprobe/references/gap-routing-guide.md +37 -36
  36. package/skills/logicprobe/references/hash-spec.md +128 -0
  37. package/skills/logicprobe/references/logic-verification-guide.md +29 -248
  38. package/skills/logicprobe-concurrency/SKILL.md +43 -0
  39. package/skills/{logicprobe → logicprobe-concurrency}/references/concurrency-risk-guide.md +1 -1
  40. package/skills/logicprobe-datamodel/SKILL.md +5 -4
  41. package/skills/logicprobe-datamodel/references/data-model-guide.md +1 -1
  42. package/skills/logicprobe-datamodel/references/data-model-schema.md +1 -1
  43. package/skills/logicprobe-structure/SKILL.md +95 -0
  44. package/skills/logicprobe-structure/references/structure-review-guide.md +177 -0
  45. package/skills/logicprobe-uml/SKILL.md +55 -0
  46. package/skills/{logicprobe → logicprobe-uml}/references/uml-modeling-guide.md +120 -8
  47. package/src/baseline.ts +180 -0
  48. package/src/client.js +56 -1
  49. package/src/compose-tool.ts +47 -47
  50. package/src/concurrency-tool.ts +2 -2
  51. package/src/concurrency.ts +23 -0
  52. package/src/data-engine.ts +51 -9
  53. package/src/engine.ts +656 -101
  54. package/src/export-tool.ts +73 -48
  55. package/src/granularity.ts +312 -0
  56. package/src/index.ts +63 -32
  57. package/src/report-diff-tool.ts +43 -0
  58. package/src/structure-tool.ts +68 -0
  59. package/src/structure.ts +724 -0
  60. package/src/tool.ts +4 -4
  61. package/src/uml-tool.ts +38 -8
  62. package/src/uml.ts +275 -24
  63. package/skills/logicprobe/references/logicprobe-engine.py +0 -4780
  64. package/skills/logicprobe/references/verification-harness.py +0 -959
  65. package/skills/logicprobe-datamodel/references/data-model-harness.py +0 -528
@@ -1,11 +1,27 @@
1
1
  ---
2
2
  name: logicprobe
3
- description: "Use when reviewing design documents, architecture specs, technical proposals, or refactoring plans that make claims about API names, file locations, enum values, or mechanism feasibility. When the document contains state machines, protocol logic, or behavioral claims (≥3 states, ACK/NACK/retry sequences, 'always'/'never'/'guaranteed' assertions, or refactoring that modifies state topology), escalate into logic-primitive verification — generate and run executable models to check mathematical completeness before trusting any claim. For refactoring specifically, the pipeline compares before/after models to verify behavioral preservation and regression freedom. ALSO use when the task is to model a code flow as UML, or to audit a diagram somebody drew. That covers reverse-engineering a handler and documenting a protocol. The skill renders the model as a state, activity or sequence diagram, reads a hand-drawn diagram back into a model, and reviews the modelling: unreachable states, dead ends, ambiguous branches, documentation gaps, and diagram-versus-model fidelity. ALSO proactively SUGGEST this skill (do not require) when a user asks code-level behavioral questions — 'check this timing for bugs', 'could this state machine deadlock', 'is this retry limit safe' — since plan-level verification has usually already been done."
3
+ description: "Use when you want to verify something about code: a design doc, spec, plan, review comment, README or refactor asserting API names, paths, enums, feasibility, state-machine or protocol behaviour, timing guarantees, or 'always/never' properties. Enumerates the claims, verifies each against the code (file:line), and escalates to executable model checks (S1-S8 + A1-A14) when behaviour is at stake. Diagrams: logicprobe-uml; concurrency: logicprobe-concurrency; data: logicprobe-datamodel."
4
4
  ---
5
5
 
6
6
  # Logic Probe
7
7
 
8
- Documents are not truth — code is. Verify every verifiable claim before accepting or acting on any design.
8
+ For an unverified design, failure is only a matter of time. Complex code becomes clear once you model it and run it. Check every claim before you act on it.
9
+
10
+ ## When This Skill Applies
11
+
12
+ **This is the entry point.** If you have any thought of checking whether something about the code is actually true — however small, however informal the claim — load this skill. It owns the doctrine (enumerate the claims, verify each against the code, cite `file:line`, read `verdict` and never `ok`) and the executable verification for behaviour. When the task lands in a neighbouring domain, its routing table below hands it over rather than stretching the doctrine to cover it.
13
+
14
+ | The task involves… | What it does |
15
+ |---|---|
16
+ | Any claim about code you want to check — a design doc, spec, plan, review comment, README, commit message, refactor proposal, or a "this is safe / this always holds / this can't deadlock" assertion | Enumerate the verifiable claims (Phase 1), verify each against the codebase with `file:line` evidence (Phase 2), classify the gaps (Phase 3) |
17
+ | A state machine or protocol — ≥3 states, guards, ACK/NACK/retry, lock/unlock, start/stop ordering | Build an executable model: S1-S8 structural checks, A1-A14 adversarial probes |
18
+ | A quantitative or temporal guarantee — worst-case path cost ≤ budget, "≥90% of runs reach SAFE", "must leave within 2 ticks" | A12 budget, A13 probabilistic reachability, A14 deadlines, each with a counterexample path |
19
+ | A handshake or power-up sequencing across two or more components | `logicprobe_compose_verify`: C1 composition deadlock, C2 rendezvous that never fires |
20
+ | A refactoring that changes state topology or guard conditions | BEFORE/AFTER models compared for behavioral preservation, invariant continuity and deadlock regression (D1-D4) |
21
+ | Explicit request to prove code correct, formally verify a module, or check a migration for breakage | Route by domain — the table under [Routing to the Sibling Skills](#routing-to-the-sibling-skills) — and keep the doctrine |
22
+ | An architecture or dependency question about a component/package/class diagram | `logicprobe-structure`: parse the diagram into a dependency graph and judge every edge against the dependency matrix |
23
+
24
+ **Catalog budget**: DSH renders only the frontmatter `description`, and truncates it at 500 characters (`dsh-tool-skill`, `DEFAULT_CATALOG_DESCRIPTION_MAX_LENGTH`). Every description in this plugin is written to fit that budget, and the triggers that do not fit live in this table. `tests/skills/run.mjs` fails when any skill's catalog line would be cut, or would lose one of its use-case triggers.
9
25
 
10
26
  <HARD-GATE>
11
27
 
@@ -131,9 +147,7 @@ When Phase 2 triggers escalation, do NOT proceed to Phase 3 until the verificati
131
147
  ```text
132
148
  Document claims → Extract model → Runtime check:
133
149
  ├── DSH + `logicprobe_verify` tool available → build Model schema v1 (references/dsh-model-schema.md) → call the tool → structured report
134
- ├── Python available + LogicModelV1 JSON at hand → references/logicprobe-engine.py verify model.json (S1-S8/A1-A14/D1-D4; subcommands compose / export add C1-C2 and UPPAAL/TLA+/PRISM/SPIN output)
135
- ├── Python available + model only as extracted dicts → fill in references/verification-harness.py → run → report
136
- └── No Python → Manual Verification Mode (see references/logic-verification-guide.md#manual-verification-mode)
150
+ └── No engine available → Manual Verification Mode (see references/logic-verification-guide.md#manual-verification-mode)
137
151
 
138
152
  Refactoring variant:
139
153
  Old code + Refactoring plan → Extract BEFORE model + AFTER model
@@ -147,6 +161,69 @@ UML modelling variant (the task is to draw a flow, not to check a claim):
147
161
  → logicprobe_verify → the behavioural checks (S1-S8 / A1-A14)
148
162
  ```
149
163
 
164
+ ### Native dsh Tools
165
+
166
+ | Tool | Covers |
167
+ |------|--------|
168
+ | `logicprobe_verify` | S1-S8 structural + A1-A14 adversarial, including A12 budget / worst-case path cost, A13 probabilistic reachability over a DTMC, and A14 discrete-tick deadlines. `beforeModel` + `stateMapping` add the D1-D4 before/after regression. |
169
+ | `logicprobe_datamodel_verify` | Data models and schema migrations: DS/DA checks plus the DD1-DD4 data regression. See the `logicprobe-datamodel` skill. |
170
+ | `logicprobe_compose_verify` | Two or more machines checked together (rendezvous handshake semantics): C1 composition deadlock, C2 rendezvous never fires. |
171
+ | `logicprobe_concurrency_scan` | Mines concurrency claims (thread-safe, lock-free, race condition, mutex, ISR-safe) and routes them to dedicated verification. It does not prove concurrency safety. |
172
+ | `logicprobe_export` | Emits external-checker input from a verified model: UPPAAL, TLA+, PRISM, SPIN. |
173
+ | `logicprobe_uml` | render / parse / review for UML modelling — the diagram domain belongs to the `logicprobe-uml` skill. |
174
+
175
+ The repository engine mirrors the same checks and carries the `compose` and `export` subcommands for hosts without the native tools.
176
+
177
+ ### Reading a Report: `ok` Is Not a Verdict
178
+
179
+ Every tool report carries the same outcome contract. **`ok` says a report was produced; `verdict` says how the review went.** A report may be `ok: true` and still be a failed review — gating on `ok` is how a failing verification gets read as a passing one.
180
+
181
+ | Field | Meaning |
182
+ |-------|---------|
183
+ | `schema` | The versioned report contract (`logicprobe/verify/v1`, `logicprobe/uml/review/v1`, …). Branch on this instead of sniffing fields. |
184
+ | `ran` | The tool executed and produced this report. `false` only when the tool refused the request before running (see `errorCode`/`error`). |
185
+ | `verdict` | `pass` \| `pass_with_findings` \| `fail`. **This is the outcome you act on.** |
186
+ | `verdictReason` | Why, e.g. `2 error finding(s) (first: S2_NO_TRANSITIONS)` or `no error findings; 3 warning finding(s)`. |
187
+ | `findings[]` | `{code, severity, message, detail?, evidence?, path?}`. Any `severity: "error"` forces `verdict: "fail"`. |
188
+ | `hashSpec` | Which published model-hash specification `modelHash` follows — see `references/hash-spec.md`. |
189
+ | `hashes` | Every hash the report carries, in one place (a model hash, a diagram hash, before/after hashes). |
190
+ | `nextSteps[]` | What to do next, derived from the findings; never empty, and identical for two runs over the same input. |
191
+ | `metadataKeys[]` | Paths of the `_`-prefixed annotation keys the schema ignored, e.g. `["_source", "_verified", "states[0]._note"]` (present only when the input carried any). |
192
+ | `narrativeCoverage` | `{states: "5/5", events: "3/9", scenarios: "0/12"}` when the model carries a narrative — a partial narrative is valid, and this is how much is still undocumented. |
193
+
194
+ Turn the verdict into an answer, never into a summary of `ok`:
195
+
196
+ - `verdict: "pass"` — no error and no warning finding. Safe to report as verified, still with the scope the model covered.
197
+ - `verdict: "pass_with_findings"` — verification ran clean, but warnings exist (e.g. `S5` implicit-ignore events, `UML012_NO_TERMINAL`). Report the pass **and** the warnings; do not silently upgrade it to "no issues".
198
+ - `verdict: "fail"` — an error-severity finding exists, or the input was rejected (`MODEL_INVALID`). Say plainly that the claim is **not** verified, quote the counterexample path the finding carries, and only then propose a correction. Never describe a `fail` as "the tool ran successfully".
199
+
200
+ Exit codes: `0` for `pass` / `pass_with_findings`, `2` for `fail`, a refusal, or an unreadable input. In DSH the native tools return the report as data — read `verdict`, and treat `errorCode`/`error` as the refusal path.
201
+
202
+ ### Slice Acceptance: Baseline Diffs
203
+
204
+ The acceptance criterion for a refactoring slice is usually "no **new** findings", not
205
+ "zero findings". Do not compare two JSON reports by eye — run the comparison:
206
+
207
+ - DSH: `logicprobe_report_diff { baseline: <earlier report>, current: <new report> }`
208
+ - Any host without the tools: run the repository engine against the same JSON.
209
+ (also available on `compose`, `structure` and `uml-review`)
210
+
211
+ A finding's identity is its **check id + code + canonical locator** (built from
212
+ `evidence`/`path`; the message only when a finding carries neither), so a reworded
213
+ message reads as `changed`, never as a new finding. The diff's `verdict` describes the
214
+ **delta** — a newly added error finding fails it — while `currentVerdict` keeps the
215
+ absolute result visible, and a clean delta over a still-failing report says so in
216
+ `nextSteps` rather than passing quietly. Two runs over the same input add zero findings:
217
+ run that self-check before trusting any baseline.
218
+
219
+ ### Archiving a Model: `_`-Prefixed Metadata
220
+
221
+ Keep provenance with the model instead of in a sidecar that drifts:
222
+
223
+ - Any key starting with `_` at any level (`_source`, `_verified`, `_uml`, `_extraction_caveats`, `states[0]._note`) is **annotation metadata**: the schema ignores it, `modelHash` excludes it, and the report echoes it as `metadataKeys`.
224
+ - Everything else stays closed — a mistyped `sttes` is still a validation error, and that is deliberate.
225
+ - Because the hash ignores `_` keys, "the model as archived" and "the model with metadata stripped" hash identically; record the reported `hashSpec` next to the hash so a later reader can reproduce it (`verify model.json --hash-check <hex>` answers whether a recorded hash belongs to a published spec at all).
226
+
150
227
  ### Refactoring Verification Mode
151
228
 
152
229
  When the document under review is a refactoring plan (modifying existing state machine logic, not designing from scratch), adapt the pipeline:
@@ -169,15 +246,7 @@ When the document under review is a refactoring plan (modifying existing state m
169
246
 
170
247
  6. **Flag any behavioral delta not documented in the plan** — the most common refactoring bug is an unintended side effect that the plan doesn't acknowledge
171
248
 
172
- **Detection step**: Before generating any verification code, run `python3 --version 2>&1` or `python --version 2>&1`. Check the output:
173
-
174
- - Returns `Python 3.x.y` with x ≥ 6 → use Python harness
175
- - Returns anything else (command not found, "Python was not found" Windows stub, version < 3.6) → fall back to Manual Verification Mode
176
- - On Windows, if `python` launches the Microsoft Store, treat as unavailable
177
-
178
- Do NOT attempt to install Python — the user's embedded development machine may be air-gapped or locked down.
179
-
180
- In DSH, prefer the native `logicprobe_verify` tool (model JSON, structured guard DSL, path-aware invariants) — see `references/dsh-model-schema.md`. For non-DSH hosts: when the model is already a LogicModelV1 JSON, run the standalone JSON engine `references/logicprobe-engine.py` (verify | compose | export — an exact Python mirror of the DSH tools, cross-checked byte-for-byte by tests/python/run.mjs); when the model exists only as extracted dicts, fill in the reusable template `references/verification-harness.py`. For detailed probe patterns, model extraction methodology, and manual verification procedures, load `references/logic-verification-guide.md`.
249
+ Run the checks with the native `logicprobe_verify` tool (model JSON, structured guard DSL, path-aware invariants) — see `references/dsh-model-schema.md`. A host without the tools can run the repository engine against the same JSON. For probe patterns, extraction methodology and manual procedures, load `references/logic-verification-guide.md`.
181
250
 
182
251
  ### Phase 2a: Structural Primitives (8 Checks)
183
252
 
@@ -206,9 +275,9 @@ Run these SECOND. Each probe actively tries to BREAK the model. If any probe suc
206
275
  | A4 | **Pair symmetry** | Match every `start/stop`, `lock/unlock`, `alloc/free` pair. Flag if any state allows a path where a pair is unbalanced (start without stop, lock without unlock). State `onEntry`/`onExit` actions are treated as implicit acquire/release. | "Resources are always released" |
207
276
  | A5 | **Boundary blast** | Probe counters at 0, 1, max-1, max, max+1. Probe timestamps at 0, tick_wraparound. Flag overflow, underflow, or undefined behavior. | "Handles all counter/timer values" |
208
277
  | A6 | **Resource injection** | Simulate `malloc→NULL`, `queue→full`, `semaphore→timeout` at each state that calls them. Flag if any state has no recovery path. | "Graceful degradation under resource pressure" |
209
- | A7 | **Minimal counter-example** | For any invariant that fails, find the SHORTEST event sequence that violates it (BFS from init to violating state). Output the exact path. | "This invariant holds" → refuted by shortest path |
278
+ | A7 | **Counter-example witness** | For any invariant that fails, find the event sequence that violates it and output the exact path. Breadth-first searches (state predicates, sequence, atomicity, budget) yield the shortest witness; the leads-to walk and the probability bound report the witness they found without claiming minimality. | "This invariant holds" → refuted by a concrete path |
210
279
  | A8 | **Idempotent replay** | For each event declared in `idempotentEvents`, apply it twice from every reachable state. Flag if the second application changes state or is not replayable. | "This event is safe to retry/replay" |
211
- | A9 | **Leads-to** | From a declared source state, every path must eventually reach the target state. | "This state always progresses to completion" |
280
+ | A9 | **Leads-to** | From a declared source state, every path must eventually reach the target state, or reach one member of a declared target set. | "This state always progresses to completion" |
212
281
  | A10 | **Sequence order** | Events declared in a sequence must occur in the specified order. | "backup before modify before commit" |
213
282
  | A11 | **Atomicity** | Once an atomic event starts, the machine must commit or roll back before leaving the scope or terminating. | "all-or-nothing transaction" |
214
283
  | A12 | **Budget (worst-case path cost)** | With `cost` on transitions (absent = 1) and a `budget` invariant, every reachable path must stay within budget; reports the shortest over-budget counterexample and flags reachable positive-cost cycles as unbounded. | "Worst-case path cost ≤ budget" |
@@ -312,35 +381,6 @@ If the user says yes, extract the model from the existing code (not a plan docum
312
381
 
313
382
  This covers the gap where behavioral verification is useful even when no design document is being reviewed.
314
383
 
315
- ## UML Modelling and Modelling Review
316
-
317
- Use this when the task is to **draw** a code flow rather than to check a claim. Typical cases are reverse-engineering a handler, documenting a protocol exchange, and auditing a diagram somebody else drew.
318
-
319
- A diagram is a model, and a model can be wrong. A tidy flow chart is not evidence about the code. A diagram that disagrees with its own model is worse than no diagram, because a reader will believe it.
320
-
321
- Pipeline (DSH):
322
-
323
- ```text
324
- code flow → model (citation per element) → logicprobe_uml action=render → diagram source
325
- → logicprobe_uml action=review → modelling findings + round-trip fidelity
326
- → logicprobe_verify → behaviour (S1-S8 structural, A1-A14 adversarial)
327
- ```
328
-
329
- - **render** — model to diagram. Mermaid covers `state`, `activity` and `sequence`. PlantUML covers `state` and `sequence`. Any construct the notation cannot carry becomes a warning, never a silent drop. PlantUML activity is refused instead of approximated.
330
- - **parse** — diagram to model. It reads Mermaid and PlantUML state or activity text, so a hand-drawn diagram can be verified like any other model. A sequence diagram is refused, because a trace cannot reconstruct a machine.
331
- - **review** — it answers one of three questions. Give it a model: is the machine well-modelled? Give it a diagram: what does the diagram say? Give it both: does the diagram match the model?
332
-
333
- The review reports structural defects and documentation gaps. The structural defects are unreachable states, dead ends, ambiguous or non-exhaustive branches, self-loops with no exit, and duplicate transitions. The documentation gaps are a missing narrative, unbounded variables, states the reader cannot map back to code, and label drift. It also runs the fidelity check. Any structural difference between the diagram and its model is `UML017_ROUND_TRIP_MISMATCH`.
334
-
335
- Rules for this mode:
336
-
337
- 1. **A diagram is not evidence.** Every state, event, guard and action needs a citation. Use `file:line` for code and a section reference for a document. Present the citations with the diagram.
338
- 2. **Review before you present.** Run `logicprobe_uml action=review` and fix the error findings first. An ambiguous or dead-ended diagram misleads every later reader.
339
- 3. **The review never replaces verification.** It covers the modelling. S1-S8 and A1-A14 cover the behaviour, and each finding names the check that settles it.
340
- 4. **Keep the narrative with the model.** Write `narrative.states`, `narrative.events` and `narrative.scenarios`. Then the diagram stays readable against the code, and label drift shows up as a finding instead of as a stale picture.
341
-
342
- The full checklist (codes `UML001` to `UML019`), the directive format generated diagrams carry, a worked example, and the limits of each view are in `references/uml-modeling-guide.md`.
343
-
344
384
  ### When NOT to Escalate
345
385
 
346
386
  Skip logic-primitive verification when:
@@ -349,19 +389,16 @@ Skip logic-primitive verification when:
349
389
  - The state machine has ≤2 states, no guards, and trivial transitions (IDLE↔ACTIVE)
350
390
  - The claim is purely structural (file paths, type names, numeric constants) — Phase 2 grep verification is sufficient
351
391
 
352
- ## Concurrency Risk Mining
353
-
354
- **Only use this after confirming the verification target actually has concurrency requirements or behavior** — e.g., multiple threads, async tasks, interrupts, shared state, or parallel execution. If the target is purely sequential, do not invoke concurrency mining.
355
-
356
- logicprobe does **not** prove concurrency safety. It mines documents and plans for concurrency-related claims and flags them for dedicated verification.
357
-
358
- - Absolute claims ("thread-safe", "lock-free", "no data race") → error / `UNVERIFIED` unless dedicated evidence is provided.
359
- - Risk keywords ("race condition", "shared variable", "mutex", "atomic", "shared memory") → warning; review whether the plan addresses them.
360
- - Interrupt safety is included: `interrupt-safe` / `ISR-safe` are absolute claims; `ISR`, `IRQ`, `NMI`, `critical section`, `disable_irq` / `enable_irq` are risk keywords.
392
+ ### Routing to the Sibling Skills
361
393
 
362
- In DSH, use the `logicprobe_concurrency_scan` tool. For manual review, follow `references/concurrency-risk-guide.md`.
394
+ This skill owns behavioural claims. Three neighbouring domains have their own skill, and loading the wrong one wastes the verification:
363
395
 
364
- For routing claims in dimensions logicprobe does not verify — hard real time (deadlines/periods), preemptive concurrency, hybrid control stability, probabilistic reliability, and execution-cost budgets — to dedicated tools, see `references/gap-routing-guide.md`. In `logicprobe_verify` reports, matching models carry informational `coverageNotes` with the same routing.
396
+ | Domain | Skill |
397
+ |---|---|
398
+ | Drawing a code flow as UML, or auditing a diagram somebody drew (render / parse / review, round-trip fidelity, component and class diagrams refused) | `logicprobe-uml` |
399
+ | Reviewing architecture, module structure or a dependency diagram (isolated nodes, dangling endpoints, cycles, allowed-dependency and layer violations, missing required edges) | `logicprobe-structure` |
400
+ | Concurrency claims — "thread-safe", "lock-free", "no data race", "ISR-safe" — mined and routed to TSan/Helgrind/CBMC/TLA+, never proven here | `logicprobe-concurrency` |
401
+ | Entities, fields, relationships, data invariants, schema migrations (DS1-DS4, DA1-DA12, DD1-DD4) | `logicprobe-datamodel` |
365
402
 
366
403
  ---
367
404
 
@@ -373,5 +410,7 @@ For routing claims in dimensions logicprobe does not verify — hard real time (
373
410
  4. Don't fix during review — point the way, let implementation happen after approval.
374
411
  5. **For behavioral claims: verify with code, not reasoning.** If a plan says "always", "never", or "guaranteed", generate and run a model. One counter-example is enough to refute a universal claim.
375
412
  6. **Confirm the model before running it** — unless the runtime reports `logicprobe interaction=auto`. Extraction errors are the dominant failure mode of formal verification. In auto mode, substitute evidence-cited extraction + round-trip validation and mark the report `UNCONFIRMED`.
376
- 7. **Don't verify what the code already checks.** If the existing codebase has compile-time assertions, static analysis, or runtime checks for a property, cite those — don't re-verify in a Python model.
377
- 8. **A diagram is a model, not evidence.** When you model a code flow as UML, cite the source for every state, event, guard and action. Run `logicprobe_uml` with `action=review` before you show the diagram. A diagram that does not read back as the model it was drawn from is mis-modelled, however tidy it looks.
413
+ 7. **Don't verify what the code already checks.** If the existing codebase has compile-time assertions, static analysis, or runtime checks for a property, cite those — don't re-verify in a model.
414
+ 8. **Hand a neighbouring domain to its own skill.** A diagram is a model, not evidence: modelling or auditing one belongs to `logicprobe-uml`. A concurrency claim is mined and routed, never proven here: that is `logicprobe-concurrency`. Schema, data invariants and migrations are `logicprobe-datamodel`. This skill keeps the doctrine and the behavioural checks.
415
+ 9. **Say what this does not prove.** For dimensions logicprobe does not verify — hard real time (deadlines, periods), preemptive concurrency, hybrid-control stability, probabilistic reliability, execution-cost budgets — route to the dedicated tool named in `references/gap-routing-guide.md`. `logicprobe_verify` reports carry the same routing as informational `coverageNotes` on matching models.
416
+ 10. **Write-site analysis is out of scope, and stays there.** "Only the ISR writes this flag", "one owner per register" — logicprobe verifies models and diagrams, not *who writes what in the source*. That claim needs a source-side AST/rule check or a static analyzer; this plugin's contribution is to flag the claim as unverified and route it (`logicprobe_concurrency_scan` does exactly that). Do not imply the diagram or the model settled it.
@@ -56,6 +56,60 @@ Declared keys per part:
56
56
 
57
57
  `boundaryChecks` is the one place where two schemas share a field name: `logicprobe_verify` uses `{ variable, values }` (a variable's boundary values for A5), while `logicprobe_datamodel_verify` uses `{ entity, field, values }` (a field's boundary values for DA2). Each validator enforces its own shape, so an `(entity, field)` check passed to the state-machine engine is rejected rather than half-understood.
58
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
+ Why it exists: provenance, verification snapshots and extraction caveats belong *with*
81
+ the model. Pushed into a sidecar they drift away from it — nobody can say which hash
82
+ belongs to which note. With this exemption the archive record travels inside the file
83
+ and still cannot change the model's identity: `modelHash` excludes `_` keys, so the
84
+ model as archived and the model with the metadata stripped hash identically.
85
+
86
+ The exemption is namespaced, not a loosening of the closed schema. `_` keys are never
87
+ *read* by any check, so ignoring them cannot produce a false result; a mistyped
88
+ non-prefixed key (`sttes`, `gaurd`, `maximum`) is still a validation error, which is
89
+ the whole point of the closed schema.
90
+
91
+ ### Reading a report: `ran`, `verdict`, `ok`
92
+
93
+ `logicprobe_verify` (and `logicprobe_compose_verify`) reports separate "the tool ran"
94
+ from "the review passed":
95
+
96
+ | Field | Meaning |
97
+ |---|---|
98
+ | `schema` | The versioned report contract, e.g. `logicprobe/verify/v1` — branch on this instead of sniffing fields. |
99
+ | `ok` | The engine produced a report. For `verify` this is `false` only when model *validation* failed. It is **not** a verdict. |
100
+ | `ran` | The tool executed. `false` only for a tool-level refusal (`errorCode`/`error` present). |
101
+ | `verdict` | `pass` \| `pass_with_findings` \| `fail`. Any `severity: "error"` finding — or a validation failure — makes this `fail`. |
102
+ | `verdictReason` | e.g. `1 error finding(s) (first: S2_NO_TRANSITIONS)`, `no error findings; 3 warning finding(s)`, `no error or warning findings`. |
103
+ | `hashSpec` | The published hash specification `modelHash` follows — see [hash-spec.md](hash-spec.md). |
104
+ | `hashes` | Every hash the report carries, in one place (`modelHash`, `beforeModelHash`/`afterModelHash` for a comparison). |
105
+ | `metadataKeys` | Present only when the input carried `_` keys (see above). |
106
+ | `narrativeCoverage` | Present only when the input carried a narrative: `covered/total` per dimension. |
107
+ | `nextSteps` | What to do next, derived from the findings; never empty, and identical for two runs over the same model. |
108
+
109
+ A caller that reads `ok` sees `true` for a model with a deadlock; the verdict is what
110
+ says so. Report the verdict, quote the counterexample path from the finding, and never
111
+ describe a `fail` as "the tool ran fine".
112
+
59
113
  ### References must resolve
60
114
 
61
115
  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:
@@ -68,7 +122,8 @@ The same reasoning applies to every id a check names. A reference to a state, ev
68
122
  | `transitions[].guard` variables | a declared variable |
69
123
  | `resourcePairs[].acquireEvent` / `.releaseEvent` / `.failEvent` | a declared event |
70
124
  | `invariants[].event` (`event-before-state`) | a declared event |
71
- | `invariants[].state` / `.states[]` / `.from` / `.to` / `.target` / `.when.state` | a declared state |
125
+ | `invariants[].state` / `.states[]` / `.from` / `.target` / `.when.state` | a declared state |
126
+ | `invariants[].to` and every member of `invariants[].to[]` (`leads-to`) | a declared state |
72
127
 
73
128
  A vacuous check is worse than a rejected one: a `leads-to` invariant whose `to` state is misspelled can never be violated, and a `never-states` list holding a nonexistent id can never be reached, so both report "pass" for a model that was never actually checked.
74
129
 
@@ -104,13 +159,21 @@ so findings can be read against real scenarios instead of bare ids.
104
159
  }
105
160
  ```
106
161
 
107
- **Completeness contract**: when `narrative` is present, all three parts are
108
- required and must fully cover the model — every declared state needs a
109
- `narrative.states` entry, every event used in `transitions` needs a
110
- `narrative.events` entry, and every distinct `(from, event)` group needs a
111
- `narrative.scenarios` entry. Keys must reference declared ids; unknown
112
- references, missing coverage, and duplicate scenario keys are model validation
113
- errors. The report's `narrative` field echoes the block unchanged.
162
+ **Coverage contract**: a narrative may cover **part** of the model — writing the states
163
+ first, then the events, then the scenarios is the natural authoring order, and a partial
164
+ block is valid. What must hold is that whatever is written is *right*:
165
+
166
+ | Case | Result |
167
+ |---|---|
168
+ | `narrative` absent | Valid. The review reports `UML013_NO_NARRATIVE` (info); `logicprobe_verify` reports no coverage at all. |
169
+ | 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. A `UML027_NARRATIVE_PARTIAL` (info) finding appears in a UML review. |
170
+ | `narrative` present but `{}` | **Error**: an empty block claims documentation that does not exist. Give at least one dimension. |
171
+ | Unknown state/event id, empty description, duplicate `(from, event)` scenario | **Error**: the narrative is wrong, not merely incomplete. |
172
+
173
+ Totals are the model's own: states = declared states, events = distinct transition
174
+ events, scenarios = distinct `(from, event)` groups (so two guarded transitions of one
175
+ event need one scenario, not two). The report's `narrative` field echoes the block
176
+ unchanged.
114
177
 
115
178
  **Presenting the model**: when showing the extracted model for confirmation, render
116
179
  the natural language INLINE in the model presentation, not as a separate block.
@@ -162,13 +225,13 @@ A guard is exactly one of:
162
225
  | `never-states` | `{ states: ["ERROR"] }` | No reachable runtime state may be in the forbidden set |
163
226
  | `var-in-range` | `{ variable, min?, max?, when? }` | Every reachable runtime state selected by `when` keeps the variable in range (at least one of `min`/`max` is required) |
164
227
  | `event-before-state` | `{ event: "power_ready", state: "ACTIVE" }` | Every path entering `state` must have passed through `event` first |
165
- | `leads-to` | `{ from: "MIGRATING", to: "DONE" }` | Every path from `from` must eventually reach `to` |
228
+ | `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 |
166
229
  | `sequence` | `{ events: ["backup", "modify", "commit"] }` | Events must occur in the given order |
167
230
  | `atomicity` | `{ events: ["write"], commit: "commit", rollback?: "rollback" }` | Atomic group must end with commit/rollback before leaving scope |
168
231
  | `budget` | `{ budget: n }` | No reachable path may accumulate transition cost greater than n (A12). Costs are non-negative; a transition without `cost` counts 1, so legacy machines keep step-count semantics |
169
232
  | `probability` | `{ target, op: one of >= <= > <, p }` | P(ever hitting target) must satisfy the bound (A13, DTMC from transition `weight`, default 1; value iteration) |
170
233
 
171
- A7 reports the shortest violating path for each failed invariant. An empty path means the initial state already violates it.
234
+ 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.
172
235
 
173
236
  ### Scoping a range to a state (`when`)
174
237
 
@@ -249,10 +312,19 @@ Actions never change state or variables. Checks that care about resource discipl
249
312
  Two or more machines can be checked together with `runCompositionVerification` (DSH tool `logicprobe_compose_verify`):
250
313
 
251
314
  - non-rendezvous events advance exactly one firing machine;
252
- - a rendezvous (handshake) event fires only when at least two machines declare it and every such non-terminal machine has it jointly enabled (guards held); participants advance simultaneously;
253
- - a terminal machine is stopped and does not participate.
315
+ - 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;
316
+ - 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.
317
+
318
+ 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.
319
+
320
+ ### Reading C1 and C2
321
+
322
+ Both findings carry the *why*, not just the *what*:
323
+
324
+ - **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.
325
+ - **C2_RENDEZVOUS_NEVER_FIRES** — `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".
254
326
 
255
- Checks: `C1_COMPOSITION_DEADLOCK` (a reachable composite state with no move while at least one machine is not terminal) and `C2_RENDEZVOUS_NEVER_FIRES`. This is a product-space BFS, so composite state count is the product of the machines; keep `maxStates` in mind.
327
+ `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.
256
328
 
257
329
  ## Minimal example
258
330
 
@@ -281,7 +353,7 @@ When `beforeModel` is passed to `logicprobe_verify`, the engine treats `model` a
281
353
  | Check | Purpose |
282
354
  |---|---|
283
355
  | D1 Behavioral Preservation | Every BEFORE (state, event) that could fire must still be fireable from the mapped AFTER state |
284
- | D2 Invariant Continuity | Every BEFORE invariant (mapped through `stateMapping`) must still hold in AFTER |
356
+ | 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 |
285
357
  | D3 Regression Delta | Lists added/removed states, events, and transitions |
286
358
  | D4 Deadlock/Liveness Regression | New deadlock states or closed SCCs not present in BEFORE |
287
359
 
@@ -306,7 +378,7 @@ List events that must be idempotent in `idempotentEvents`. For every reachable s
306
378
  ## Advanced constraints (S8, A9-A14)
307
379
 
308
380
  - **S8 Monotonic Variables**: declare `monotonic: "inc"|"dec"` on a variable; updates must not move in the opposite direction.
309
- - **A9 Leads-To**: `{ kind: "leads-to", from, to }` — every path from `from` must eventually reach `to`.
381
+ - **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.
310
382
  - **A10 Sequence**: `{ kind: "sequence", events }` — events must appear in order.
311
383
  - **A11 Atomicity**: `{ kind: "atomicity", events, commit, rollback? }` — once an atomic event starts, the machine must reach commit/rollback before leaving the atomic scope or terminating.
312
384
  - **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.
@@ -1,36 +1,37 @@
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.
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
+ | Write-site / single-owner analysis | **Not covered, and not planned.** logicprobe 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. `logicprobe_concurrency_scan` will flag the *claim* as unverified and route it here, which is the honest part of this plugin's answer |
21
+
22
+ ## Where the routing surfaces
23
+
24
+ - `logicprobe_verify` report: a model whose state/event/action names match the timing,
25
+ preemption, hybrid, or probability vocabulary gets informational `coverageNotes`
26
+ with the same routing as above.
27
+ - A12 advisory: transitions declaring `cost` without a `budget` invariant produce
28
+ `A12_COST_WITHOUT_BUDGET`, pointing at the budget check.
29
+ - `logicprobe_concurrency_scan`: absolute concurrency claims (thread-safe, lock-free,
30
+ interrupt-safe, ...) carry `suggestions` naming dedicated tools.
31
+ - Manual review: when reading a plan, match its claims against the table above and
32
+ require dedicated evidence before accepting "always"/"never"/"guaranteed" language
33
+ - Generators: `exportModel(model, format)` emits native inputs for UPPAAL (.xta + queries),
34
+ TLA+ (TLC spec + safety), PRISM (.pm + .pctl), and SPIN (Promela + ltl) from a
35
+ LogicModelV1 — v1 translates the core machine (guards/updates/weights; booleans as
36
+ integers) and leaves unrepresentable invariants as explicit warnings.
37
+ Self-tests: tests/exporters/run.mjs.
@@ -0,0 +1,128 @@
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`, and the parity test).
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`), and the parity test compares the two engines
42
+ byte-for-byte on every fixture. The specification, not either implementation, is the
43
+ contract.
44
+
45
+ Nothing else is normalized: no key renaming, no default filling, no unit conversion,
46
+ no ordering of `states` or `transitions` arrays. Two models that differ only in array
47
+ order hash differently — that is intended, because the order is part of the input.
48
+
49
+ ## Recomputing a hash
50
+
51
+ Both commands below reproduce the engine's value exactly.
52
+
53
+ ```bash
54
+ # Node
55
+ 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'))"
56
+
57
+ # Node.js
58
+ 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'))"
59
+ ```
60
+
61
+ The reports carry the specification next to the value, e.g. `"hashSpec": "v1"`, so an
62
+ archive record does not need to guess:
63
+
64
+ ```json
65
+ { "hashSpec": "v1", "modelHash": "f583202888b827922ac512a312758fa2d8cf9055b102205ecad18f81a0a7d539" }
66
+ ```
67
+
68
+ Round-trip reports (`logicprobe_uml action=review`, `roundTrip`) carry `hashSpec`
69
+ alongside `modelHash` and `parsedHash`, so a fidelity comparison is always
70
+ spec-qualified.
71
+
72
+ Every report also aggregates its hashes under `hashes`, so a baseline diff or an archive
73
+ record reads one field instead of walking the report:
74
+
75
+ ```json
76
+ { "hashes": { "hashSpec": "v1", "modelHash": "f5832028…", "afterModelHash": "f5832028…" } }
77
+ ```
78
+
79
+ A structure review hashes its inputs under the same rule but with its own keys:
80
+ `hashes.diagram` (sha256 of the diagram text) and `hashes.matrix` (sha256 of the
81
+ canonical — key-sorted — matrix JSON, so key order does not change the hash) when a
82
+ dependency matrix was supplied. A data-model report carries `hashes.modelHash`; the
83
+ data-model hash uses the same normalization but its schema is not covered by the
84
+ published specification.
85
+
86
+ ## Checking an archived hash
87
+
88
+ `--hash-spec` recomputes under a named specification; `--hash-check` answers whether a
89
+ recorded value belongs to any published specification *at all*:
90
+
91
+ ```bash
92
+ # verify under a named spec
93
+ The repository CLI takes the same flag: `verify model.json --hash-spec v0`
94
+
95
+ # ask about a recorded hash: prints {checked, matches, published[], verdict, verdictReason}
96
+ The repository CLI reports which spec reproduces an archived hash: `verify model.json --hash-check 0000…deadbeef`
97
+ ```
98
+
99
+ `--hash-check` exits `0` when a published spec reproduces the value and `2` when none
100
+ does, with the explicit answer `no published hash spec reproduces <hex>`. It never
101
+ guesses a spec to make a number match.
102
+
103
+ The in-process equivalents are `modelHash(model, spec)` (TypeScript,
104
+ `lib/engine.js`) and `model_hash(model, spec)` in the mirror, plus `hashPayload(model, spec)`
105
+ and `hash_payload(model, spec)` for the exact bytes that are hashed.
106
+
107
+ ## When the hash changes
108
+
109
+ A change that alters the bytes alters the hash, and that is a compatibility event:
110
+
111
+ - Adding, removing or renaming an *excluded* key (`_`-prefixed): no hash change by
112
+ construction.
113
+ - Adding a declared model field that a model actually uses: that model's hash changes.
114
+ The change must be named in the release note, and it is covered by the golden tests
115
+ rather than discovered by a user comparing snapshots.
116
+ - Changing any normalization rule (separator, escaping, key order, exclusion set): a
117
+ **new** specification, never a silent change to `v1`. The old spec keeps its name and
118
+ its behaviour so archived hashes stay checkable.
119
+
120
+ `--hash-spec` changes the reported `modelHash` only. The `beforeModelHash` /
121
+ `afterModelHash` pair inside a `comparison` block is a diagnostic of one run and always
122
+ uses `v1`.
123
+
124
+ ### Version history
125
+
126
+ | Release | Hash change | Affected models |
127
+ |---------|-------------|-----------------|
128
+ | 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`. |