dsh-logicprobe 1.0.0 → 1.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/lib/tool.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools';
2
2
  import { runVerification } from './engine.js';
3
+ import { renderReportText } from './report-view.js';
3
4
  export const LOGICPROBE_VERIFY_TOOL_NAME = 'logicprobe_verify';
4
5
  /**
5
6
  * Model-visible DSH tool wrapping the bundled TypeScript verification engine.
@@ -35,14 +36,19 @@ export const logicProbeVerifyTool = defineTool({
35
36
  type: 'json',
36
37
  description: 'Optional object mapping BEFORE state ids to AFTER state ids. Omit for identity mapping (same state names).',
37
38
  },
39
+ reportView: {
40
+ type: 'string',
41
+ enum: ['digest', 'full'],
42
+ description: 'digest (default) prints the readable body; full prints the structured report, which logicprobe_report_diff needs.',
43
+ },
38
44
  },
39
45
  output: {
40
46
  schema: {
41
47
  type: 'json',
42
48
  description: 'logicprobe verification report with summary and per-check findings.',
43
49
  },
44
- render(_args, value) {
45
- return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
50
+ render(args, value) {
51
+ return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full' }) }];
46
52
  },
47
53
  },
48
54
  timeoutMs: 10_000,
@@ -177,7 +177,11 @@ export interface PathStep {
177
177
  }
178
178
  export interface Finding {
179
179
  code: string;
180
- severity: 'error' | 'warning';
180
+ /**
181
+ * `info` is a note, not a defect: it is reported so a machine-readable reader can see it,
182
+ * and it never contributes to `verdict`, `summary.errors` or `summary.warnings`.
183
+ */
184
+ severity: 'error' | 'warning' | 'info';
181
185
  message: string;
182
186
  /** Longer explanation: the shortest-path note, the per-machine reasons, the quoted source lines. */
183
187
  detail?: string;
@@ -296,6 +300,7 @@ export declare const REPORT_SCHEMAS: {
296
300
  readonly umlRender: 'logicprobe/uml/render/v1';
297
301
  readonly umlParse: 'logicprobe/uml/parse/v1';
298
302
  readonly umlReview: 'logicprobe/uml/review/v1';
303
+ readonly umlExplainLabels: 'logicprobe/uml/explain-labels/v1';
299
304
  readonly structure: 'logicprobe/structure/v1';
300
305
  readonly granularity: 'logicprobe/granularity/v1';
301
306
  readonly baseline: 'logicprobe/baseline/v1';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * logicprobe — DeepSeek Harness native plugin for the Logic Probe toolbox.
3
- * Injects the session-start gate text (what this plugin checks, the 1% Rule,
4
- * the Red Flags, the routing to the skills, the tool list, how to read a report)
3
+ * Injects the session-start gate text (what this plugin checks, the routing to
4
+ * the skills, the tool list, how to read a report)
5
5
  * into the first model step of every agent session, mirroring the SessionStart
6
6
  * hook the Claude Code plugin installs.
7
7
  * The skill ships in this package's `skills/` directory and is registered at
@@ -18,8 +18,9 @@
18
18
  * to the first step after their promotion, and the history guard re-injects it
19
19
  * there. The default gate text is the dsh-native adaptation of
20
20
  * `hooks/session-start-content.md`. Both are a guide to calling the tools: what
21
- * this plugin checks, the 1% Rule and the Red Flags that raise the chance of a
22
- * call, the routing to the five skills, the tool list, and how to read a report.
21
+ * this plugin checks, the routing to the sibling skills, the tool list, and how to
22
+ * read a report. The Red Flags table is enforcement scaffolding and lives in the
23
+ * skill body instead, where a session that needs it can load it.
23
24
  * They stay in sync. Method, doctrine and per-domain detail live in the skills
24
25
  * and their references, not here. Deployments override via Config.
25
26
  *
@@ -0,0 +1,43 @@
1
+ /** Rendering limits. Defaults are the shipped behaviour; tests pin them. */
2
+ export interface ReportViewOptions {
3
+ /** Maximum finding lines per check. Error findings are never dropped, only warnings are capped. */
4
+ maxFindingsPerCheck?: number;
5
+ /** Evidence is inlined for `error` findings only, truncated to this many characters. */
6
+ maxEvidenceChars?: number;
7
+ /** Include passing checks' bodies (default false: a passing check carries no actionable finding). */
8
+ includePassing?: boolean;
9
+ /**
10
+ * Print the whole structured report instead of the body. This is the escape hatch for a
11
+ * caller that must hand the report to another tool verbatim — `logicprobe_report_diff`
12
+ * matches on check + code + evidence, so a body cannot stand in for the object.
13
+ */
14
+ full?: boolean;
15
+ }
16
+ type Record_ = Record<string, unknown>;
17
+ /** One normalized finding. `evidence` is kept because a finding's identity depends on it. */
18
+ export interface ViewFinding {
19
+ code: string;
20
+ severity: string;
21
+ message: string;
22
+ evidence?: Record_;
23
+ file?: string;
24
+ line?: number;
25
+ check?: string;
26
+ }
27
+ /** Normalize one raw finding. Unknown fields are ignored; a missing message renders as the code. */
28
+ export declare function toViewFinding(value: unknown, check?: string): ViewFinding;
29
+ /**
30
+ * The fact a finding states, as a comparable key. The verify/datamodel families report the
31
+ * same (state, event) fact from more than one check, so the key ignores which check said it;
32
+ * anything else falls back to code + message.
33
+ */
34
+ export declare function factKey(finding: ViewFinding): string;
35
+ /**
36
+ * Render any logicprobe result as the text a model reads.
37
+ *
38
+ * Order of recognition: a `checks[]` report, a flat `findings[]` report, a baseline delta,
39
+ * a payload result (`diagram`/`primary`), and finally compact JSON so an unrecognized shape
40
+ * is still readable rather than silently empty.
41
+ */
42
+ export declare function renderReportText(value: unknown, options?: ReportViewOptions): string;
43
+ export {};
package/lib/uml-tool.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { REPORT_SCHEMAS, DEFAULT_HASH_SPEC, modelHash, refusalVerdict, verdictOf, verdictOfFindings } from './engine.js';
4
- import { renderUml, parseUml, parseFindings, reviewUml, UmlError } from './uml.js';
4
+ import { renderUml, parseUml, parseFindings, reviewUml, explainLabels, UmlError } from './uml.js';
5
+ import { renderReportText } from './report-view.js';
5
6
  export const LOGICPROBE_UML_TOOL_NAME = 'logicprobe_uml';
6
7
  /**
7
8
  * DSH tool wrapping the UML front end: model a code flow as a UML diagram
@@ -17,13 +18,13 @@ export const LOGICPROBE_UML_TOOL_NAME = 'logicprobe_uml';
17
18
  */
18
19
  export const logicProbeUmlTool = defineTool({
19
20
  name: LOGICPROBE_UML_TOOL_NAME,
20
- description: 'Model a code flow as UML, and audit the modelling. action="render" turns a LogicModelV1 into diagram source: mermaid (state, activity, sequence) or plantuml (state, sequence). action="parse" reads Mermaid or PlantUML state or activity text back into a LogicModelV1, so a hand-drawn diagram can be verified. A sequence diagram is refused: a trace cannot reconstruct a machine. So is any other diagram family (class, ER, gantt, mindmap). That refusal carries errorCode "UML_NOT_A_STATE_DIAGRAM". A PlantUML component, package or deployment diagram declares no kind, so the parser detects those constructs itself. It returns a by-product model and reports the error UML_NOT_A_STATE_DIAGRAM, with `discardedConstructs` and `discardedEdges`. Never treat that model as a model of the file. action="review" audits the modelling: structural defects (UML002-UML009), documentation gaps (UML010-UML015), and a fidelity check that re-parses the rendered diagram (UML017). Give it a model, a diagram, or both. Rendering never invents structure. A construct the notation cannot express becomes a warning. For dependency questions on a component or class diagram, use `logicprobe_structure_verify` instead. Read `verdict`, not `ok`: error findings mean a failed review.',
21
+ description: 'Model a code flow as UML, and audit the modelling. action="render" turns a LogicModelV1 into diagram source: mermaid (state, activity, sequence) or plantuml (state, sequence). action="parse" reads Mermaid or PlantUML state or activity text back into a LogicModelV1, so a hand-drawn diagram can be verified. A sequence diagram is refused: a trace cannot reconstruct a machine. So is any other diagram family (class, ER, gantt, mindmap). That refusal carries errorCode "UML_NOT_A_STATE_DIAGRAM". A PlantUML component, package or deployment diagram declares no kind, so the parser detects those constructs itself. It returns a by-product model and reports the error UML_NOT_A_STATE_DIAGRAM, with `discardedConstructs` and `discardedEdges`. Never treat that model as a model of the file. action="review" audits the modelling: structural defects (UML002-UML009), documentation gaps (UML010-UML015), and a fidelity check that re-parses the rendered diagram (UML017). Give it a model, a diagram, or both. action="explain-labels" prints the label and comment conventions the parser accepts, without needing a diagram. Rendering never invents structure. A construct the notation cannot express becomes a warning. For dependency questions on a component or class diagram, use `logicprobe_structure_verify` instead. Read `verdict`, not `ok`: error findings mean a failed review.',
21
22
  parameters: {
22
23
  action: {
23
24
  type: 'string',
24
25
  required: true,
25
- enum: ['render', 'parse', 'review'],
26
- description: 'render = model → UML source; parse = UML source → model; review = audit the modelling (and its fidelity to the model).',
26
+ enum: ['render', 'parse', 'review', 'explain-labels'],
27
+ description: 'render = model → UML source; parse = UML source → model; review = audit the modelling (and its fidelity to the model); explain-labels = print the label and comment conventions this parser accepts.',
27
28
  },
28
29
  model: {
29
30
  type: 'json',
@@ -51,23 +52,21 @@ export const logicProbeUmlTool = defineTool({
51
52
  type: 'integer',
52
53
  description: 'Cap on the sequence trace length when rendering kind=sequence. Default 60.',
53
54
  },
55
+ reportView: {
56
+ type: 'string',
57
+ enum: ['digest', 'full'],
58
+ description: 'digest (default) prints the diagram or report body; full prints the structured result as JSON.',
59
+ },
54
60
  },
55
61
  output: {
56
62
  schema: {
57
63
  type: 'json',
58
64
  description: 'Render result (diagram source), parse result (LogicModelV1 + labels), or the modelling review report with findings, round-trip diff and next steps.',
59
65
  },
60
- render(_args, value) {
61
- const record = value;
62
- // A rendered diagram is source text: print it verbatim so it can be copied
63
- // into a file, then the structured result (warnings, notation, kind).
64
- if (typeof record.diagram === 'string') {
65
- return [
66
- { type: 'text', text: record.diagram },
67
- { type: 'text', text: JSON.stringify(value, null, 2) },
68
- ];
69
- }
70
- return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
66
+ render(args, value) {
67
+ // A rendered diagram is source text: the report body prints it verbatim, once.
68
+ // Echoing the whole result as JSON printed the same diagram a second time, escaped.
69
+ return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full' }) }];
71
70
  },
72
71
  },
73
72
  timeoutMs: 10_000,
@@ -105,6 +104,19 @@ export const logicProbeUmlTool = defineTool({
105
104
  warnings: result.warnings,
106
105
  };
107
106
  }
107
+ // R9: the label contract was documented in the library and in `explainLabels()`, but no
108
+ // tool exposed it, so it was reachable only by reading the source or the docs.
109
+ if (args.action === 'explain-labels') {
110
+ const notation = (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation);
111
+ return {
112
+ ok: true,
113
+ ran: true,
114
+ ...verdictOf(0, 0),
115
+ schema: REPORT_SCHEMAS.umlExplainLabels,
116
+ action: 'explain-labels',
117
+ ...explainLabels(notation),
118
+ };
119
+ }
108
120
  const report = reviewUml({
109
121
  model: args.model,
110
122
  diagram: args.diagram,
package/lib/uml.js CHANGED
@@ -827,21 +827,25 @@ function declaredIdentifier(text) {
827
827
  * file, which is a false guarantee of exactly the kind this plugin exists to prevent.
828
828
  */
829
829
  export function parseFindings(parsed) {
830
- if (parsed.discardedConstructs.length === 0)
830
+ // A note block is reported in `discardedConstructs` for transparency, but it does not make
831
+ // the text "not a state diagram": the rest of the file still is one, so only the constructs
832
+ // that actually replace a state declaration count towards the refusal.
833
+ const unsupported = parsed.discardedConstructs.filter((entry) => entry.construct !== 'note');
834
+ if (unsupported.length === 0)
831
835
  return [];
832
836
  const counts = new Map();
833
- for (const entry of parsed.discardedConstructs)
837
+ for (const entry of unsupported)
834
838
  counts.set(entry.construct, (counts.get(entry.construct) ?? 0) + 1);
835
839
  const summary = [...counts.entries()]
836
840
  .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
837
841
  .map(([construct, count]) => construct + ' ×' + String(count))
838
842
  .join(', ');
839
- const shown = parsed.discardedConstructs.slice(0, 12).map((entry) => 'line ' + String(entry.line) + ': ' + entry.text).join(' | ');
843
+ const shown = unsupported.slice(0, 12).map((entry) => 'line ' + String(entry.line) + ': ' + entry.text).join(' | ');
840
844
  return [{
841
845
  code: 'UML_NOT_A_STATE_DIAGRAM',
842
846
  severity: 'error',
843
- message: 'the text is not a state or activity diagram: ' + String(parsed.discardedConstructs.length) + ' declaration(s) of unsupported construct(s) (' + summary + ') and ' + String(parsed.discardedEdges) + ' arrow(s) between them were read as states and transitions, so the parsed model is not this diagram.',
844
- detail: shown + (parsed.discardedConstructs.length > 12 ? ' | … ' + String(parsed.discardedConstructs.length - 12) + ' more' : ''),
847
+ message: 'the text is not a state or activity diagram: ' + String(unsupported.length) + ' declaration(s) of unsupported construct(s) (' + summary + ') and ' + String(parsed.discardedEdges) + ' arrow(s) between them were read as states and transitions, so the parsed model is not this diagram.',
848
+ detail: shown + (unsupported.length > 12 ? ' | … ' + String(unsupported.length - 12) + ' more' : ''),
845
849
  }];
846
850
  }
847
851
  function parseStateDiagram(text, notation) {
@@ -885,6 +889,12 @@ function parseStateDiagram(text, notation) {
885
889
  if (inNote)
886
890
  return;
887
891
  if (noteStart && !/:\s*.+$/.test(line)) {
892
+ // R7: a multi-line note block is prose the notation carries and LogicModelV1 does not.
893
+ // Its body is still skipped line by line, but the block itself is NOT silent: it is
894
+ // reported once, like any other unsupported construct, so "the diagram hash changed and
895
+ // the report said nothing" cannot happen. It stays out of `warnings` (no per-line noise)
896
+ // and `parseFindings` keeps it out of the "not a state diagram" verdict.
897
+ raw.discarded.push({ construct: 'note', line: index + 1, text: line });
888
898
  inNote = true;
889
899
  return;
890
900
  }
@@ -1078,7 +1088,7 @@ export function explainLabels(notation = 'mermaid') {
1078
1088
  renderedForm: 'state "ID(meaning)" as ID',
1079
1089
  ignoredLines: [
1080
1090
  { pattern: 'a `' + prefix + '` comment line that is not a `logicprobe:` directive', behaviour: 'UML_PARSE_IGNORED_LINE warning: it carries no state-machine statement' },
1081
- { pattern: 'a `note …` block written over several lines', behaviour: 'skipped silently (a block is not a state meaning)' },
1091
+ { pattern: 'a `note …` block written over several lines', behaviour: 'its body is skipped line by line (prose, not a statement) and the block itself is reported once in discardedConstructs' },
1082
1092
  { pattern: 'notation chrome: @startuml/@enduml, stateDiagram-v2, direction, classDef/style/linkStyle/click, scale, skinparam, title, hide, autonumber', behaviour: 'skipped silently' },
1083
1093
  { pattern: 'anything else the parser cannot read', behaviour: 'UML_PARSE_IGNORED_LINE warning, and the model is built from what it did read' },
1084
1094
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-logicprobe",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Verifies claims about designs and code. Facts go against the source. Behaviour goes through an executable model: state machines, protocols, composition, data models, migrations. Structure diagrams are read as dependency graphs, at one level or several. Two reports can be compared for regression. Exports UPPAAL, TLA+, PRISM and SPIN input. Ships a native DeepSeek Harness bundle that injects the verification gate into the first model step.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -79,7 +79,7 @@
79
79
  "build": "tsc -p tsconfig.json && node scripts/build-client.mjs",
80
80
  "typecheck": "tsc -p tsconfig.json --noEmit",
81
81
  "bump": "node scripts/bump-version.mjs",
82
- "test:engine": "npm run build && node tests/engine/run.mjs && node tests/data-engine/run.mjs && node tests/concurrency/run.mjs && node tests/uml/run.mjs && node tests/apply-smoke.mjs && node tests/dsh-client-half.test.mjs && node tests/dsh-volatile-fallback.test.mjs && node tests/claude-session-start-hook.test.mjs && node tests/skills/run.mjs && node tests/structure/run.mjs && node tests/baseline/run.mjs && node tests/granularity/run.mjs && node tests/exporters/run.mjs && node tests/external/run.mjs && node tests/python/run.mjs",
82
+ "test:engine": "npm run build && node tests/engine/run.mjs && node tests/data-engine/run.mjs && node tests/concurrency/run.mjs && node tests/uml/run.mjs && node tests/apply-smoke.mjs && node tests/dsh-client-half.test.mjs && node tests/dsh-volatile-fallback.test.mjs && node tests/claude-session-start-hook.test.mjs && node tests/skills/run.mjs && node tests/structure/run.mjs && node tests/baseline/run.mjs && node tests/granularity/run.mjs && node tests/exporters/run.mjs && node tests/report-view/run.mjs && node tests/external/run.mjs && node tests/python/run.mjs",
83
83
  "test:full": "npm run build && node tests/full-suite.mjs",
84
84
  "test:python": "npm run build && node tests/python/run.mjs"
85
85
  },
@@ -60,7 +60,7 @@ This step is NOT skippable — it creates an explicit, auditable record of what
60
60
  |-------|----------|
61
61
  | LIGHTWEIGHT | All 5 checklist items (file paths, API/type names, line numbers, behavioral claims, mechanism feasibility) answered in context with explicit results per item |
62
62
  | STANDARD | Phase 1-2: enumerate every verifiable claim → verify each against codebase with evidence |
63
- | ESCALATED | Full pipeline: Phase 1-5 including Logic Primitive Verification (Phase 2a + 2b, 22 checks) |
63
+ | ESCALATED | Full pipeline: Phase 1-5 including Logic Primitive Verification (Phase 2a structural, Phase 2b adversarial) |
64
64
 
65
65
  ### Plan Verification Block
66
66
 
@@ -151,7 +151,7 @@ Document claims → Extract model → Runtime check:
151
151
 
152
152
  Refactoring variant:
153
153
  Old code + Refactoring plan → Extract BEFORE model + AFTER model
154
- → Run pipeline on AFTER model (22 checks)
154
+ → Run the pipeline on the AFTER model (the same checks)
155
155
  → Compare BEFORE vs AFTER: behavioral preservation, regression, complexity delta
156
156
  → Flag any invariant that held in BEFORE but fails in AFTER
157
157
 
@@ -170,9 +170,11 @@ UML modelling variant (the task is to draw a flow, not to check a claim):
170
170
  | `logicprobe_compose_verify` | Two or more machines checked together (rendezvous handshake semantics): C1 composition deadlock, C2 rendezvous never fires. |
171
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
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. |
173
+ | `logicprobe_uml` | render / parse / review / explain-labels for UML modelling — the diagram domain belongs to the `logicprobe-uml` skill. |
174
+ | `logicprobe_structure_verify` | Reviews a structure diagram as a dependency graph. It reports isolated nodes, dangling endpoints, cycles, disallowed edges, layer violations and missing required edges, and it checks refinement across declared levels. The diagram domain belongs to the `logicprobe-structure` skill. |
175
+ | `logicprobe_report_diff` | Compares two reports of the same family. A finding's identity is its check, its code and its machine-readable locator, so reworded prose reads as changed rather than new. This is slice acceptance computed instead of eyeballed. Without the tool, `verify --baseline REPORT` prints the same diff. |
174
176
 
175
- The repository engine mirrors the same checks and carries the `compose` and `export` subcommands for hosts without the native tools.
177
+ The repository engine mirrors the same checks for hosts without the native tools, and its `--help` lists the subcommands. Two capabilities have no mirror. Data-model verification takes the data harness or the manual mode there. The concurrency claim scan is the same story: `logicprobe-concurrency` routes the claims instead. That engine prints the report JSON, not the body — the body is the dsh tools' rendering.
176
178
 
177
179
  ### Reading a Report: `ok` Is Not a Verdict
178
180
 
@@ -189,7 +191,9 @@ Every tool report carries the same outcome contract. **`ok` says a report was pr
189
191
  | `hashes` | Every hash the report carries, in one place (a model hash, a diagram hash, before/after hashes). |
190
192
  | `nextSteps[]` | What to do next, derived from the findings; never empty, and identical for two runs over the same input. |
191
193
  | `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. |
194
+ | `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. The gap is also a `NARRATIVE_PARTIAL` (info) finding, so a gate can read it without parsing `nextSteps` text. |
195
+
196
+ A tool prints a body, not JSON. Passing checks stay in the `CHECKS` inventory line, and a fact that several checks report is listed once. Only error findings inline their evidence. Pass `reportView: "full"` when you need the structured report itself, for example to hand it to `logicprobe_report_diff`.
193
197
 
194
198
  Turn the verdict into an answer, never into a summary of `ok`:
195
199
 
@@ -231,7 +235,7 @@ When the document under review is a refactoring plan (modifying existing state m
231
235
  1. **Extract the BEFORE model** from the existing codebase (not the plan — verify what the code actually does, not what the plan claims it does)
232
236
  2. **Extract the AFTER model** from the refactoring plan
233
237
  3. **Show both tables AND their model narratives** to the user side by side and confirm the delta is intentional
234
- 4. **Run Phase 2a + 2b on the AFTER model** — same 22 checks as new design
238
+ 4. **Run Phase 2a + 2b on the AFTER model** — the same checks a new design gets
235
239
  5. **Compare BEFORE vs AFTER**:
236
240
 
237
241
  | Check | Method | Severity if Violated |
@@ -248,7 +252,7 @@ When the document under review is a refactoring plan (modifying existing state m
248
252
 
249
253
  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`.
250
254
 
251
- ### Phase 2a: Structural Primitives (8 Checks)
255
+ ### Phase 2a: Structural Primitives
252
256
 
253
257
  Run these FIRST. They establish basic well-formedness before adversarial probing.
254
258
 
@@ -263,7 +267,7 @@ Run these FIRST. They establish basic well-formedness before adversarial probing
263
267
  | S7 | **Invariant validity** | Does every reachable state satisfy the plan's stated "always/never/guaranteed" assertions? | Error — plan claim is false |
264
268
  | S8 | **Monotonic variables** | If a variable is declared `monotonic: inc/dec`, do all updates respect that direction? | Error — counter can move backwards |
265
269
 
266
- ### Phase 2b: Adversarial Probes (14 Attacks)
270
+ ### Phase 2b: Adversarial Probes
267
271
 
268
272
  Run these SECOND. Each probe actively tries to BREAK the model. If any probe succeeds (finds a violation), the plan has a behavior gap.
269
273
 
@@ -395,13 +399,26 @@ This skill owns behavioural claims. Three neighbouring domains have their own sk
395
399
 
396
400
  | Domain | Skill |
397
401
  |---|---|
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` |
402
+ | Drawing a code flow as UML, or auditing a diagram somebody drew (render / parse / review / explain-labels, round-trip fidelity, component and class diagrams refused) | `logicprobe-uml` |
399
403
  | Reviewing architecture, module structure or a dependency diagram (isolated nodes, dangling endpoints, cycles, allowed-dependency and layer violations, missing required edges) | `logicprobe-structure` |
400
404
  | 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
405
  | Entities, fields, relationships, data invariants, schema migrations (DS1-DS4, DA1-DA12, DD1-DD4) | `logicprobe-datamodel` |
402
406
 
403
407
  ---
404
408
 
409
+ ## Red Flags
410
+
411
+ If you catch yourself thinking any of these, stop:
412
+
413
+ | You think | Reality |
414
+ |-----------|---------|
415
+ | "Too simple to check" | The check is cheap. The silent skip is not. |
416
+ | "I read the code, so I know" | Knowing is not evidence. Cite `file:line`, or run the model, or call the claim unverified. |
417
+ | "The spec or the diagram says so" | Both are claims about the code. Reconcile them with a source-side scan. |
418
+ | "I'll check while implementing" | Check before the decision, not after. |
419
+ | "Reasoning is enough" | Reasoning is not a run. One counter-example kills a universal claim. |
420
+ | "The report passed, so the code is fine" | The report covers the model you gave it. Reconcile it, and say what you did not check. |
421
+
405
422
  ## Rules
406
423
 
407
424
  1. Never trust a document's claim without codebase verification.
@@ -1,6 +1,6 @@
1
1
  # DSH Model Schema v1 — logicprobe_verify
2
2
 
3
- The dsh-native `logicprobe_verify` tool accepts a structured JSON model. The engine runs 22 checks (S1-S8 structural, A1-A14) and returns a JSON report. When `beforeModel` is supplied, it also runs D1-D4 before/after regression checks. Guards and updates are structured data — no code strings, no arbitrary execution.
3
+ The dsh-native `logicprobe_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
4
 
5
5
  ## Top-level model
6
6
 
@@ -77,6 +77,13 @@ it, and the report lists it under `metadataKeys`:
77
77
  "metadataKeys": ["_extraction_caveats", "_source", "_verified", "states[0]._note"]
78
78
  ```
79
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
+
80
87
  Why it exists: provenance, verification snapshots and extraction caveats belong *with*
81
88
  the model. Pushed into a sidecar they drift away from it — nobody can say which hash
82
89
  belongs to which note. With this exemption the archive record travels inside the file
@@ -166,7 +173,7 @@ block is valid. What must hold is that whatever is written is *right*:
166
173
  | Case | Result |
167
174
  |---|---|
168
175
  | `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. |
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: `logicprobe_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. |
170
177
  | `narrative` present but `{}` | **Error**: an empty block claims documentation that does not exist. Give at least one dimension. |
171
178
  | Unknown state/event id, empty description, duplicate `(from, event)` scenario | **Error**: the narrative is wrong, not merely incomplete. |
172
179
 
@@ -220,6 +227,13 @@ A guard is exactly one of:
220
227
 
221
228
  ## Invariants
222
229
 
230
+ Every invariant carries `id` and **`description`** — both required strings — plus a
231
+ `kind` and that kind's own fields. `description` is not decoration: it is echoed into
232
+ the finding a violation produces, and it takes part in `modelHash`, so a model that
233
+ gains one hashes differently. An invariant without it is rejected with `MODEL_INVALID`,
234
+ and the report's `nextSteps` names the field to add; a `_`-prefixed key is not a
235
+ substitute for it.
236
+
223
237
  | Kind | Shape | Checks |
224
238
  |---|---|---|
225
239
  | `never-states` | `{ states: ["ERROR"] }` | No reachable runtime state may be in the forbidden set |
@@ -314,6 +328,13 @@ Two or more machines can be checked together with `runCompositionVerification` (
314
328
  - non-rendezvous events advance exactly one firing machine;
315
329
  - 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
330
  - 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.
331
+ - **there is no cross-machine shared state.** Each machine's `variables` are private to
332
+ it: machine A's `ready` and machine B's `ready` are two different
333
+ quantities, and no guard can read another machine's variable. Rendezvous events are
334
+ the *only* coupling between machines. So "two machines must agree on one predicate"
335
+ cannot be modelled by giving each a copy of it — model the shared condition inside a
336
+ single machine, or make the agreement an explicit handshake. The report names the
337
+ variables it saw when two or more machines each declare some, in `nextSteps`.
317
338
 
318
339
  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
340
 
@@ -321,8 +342,8 @@ Inputs: the DSH tool takes an array of model objects (an agent reads the files a
321
342
 
322
343
  Both findings carry the *why*, not just the *what*:
323
344
 
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".
345
+ - **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.
346
+ - **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".
326
347
 
327
348
  `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.
328
349
 
@@ -10,14 +10,15 @@ as outside this engine and points at tooling that can handle it.
10
10
 
11
11
  | Dimension | What logicprobe covers today | Claim examples | Dedicated tooling | Why not here |
12
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 |
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
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
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
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
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 |
18
+ | Multi-machine composition | `logicprobe_compose_verify` (dsh) and the engine's `compose` subcommand (non-dsh) 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. `logicprobe_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.** 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 (it is a dsh-only capability; without it, route the claim by hand) |
21
22
 
22
23
  ## Where the routing surfaces
23
24
 
@@ -30,8 +31,8 @@ as outside this engine and points at tooling that can handle it.
30
31
  interrupt-safe, ...) carry `suggestions` naming dedicated tools.
31
32
  - Manual review: when reading a plan, match its claims against the table above and
32
33
  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.
34
+ - Generators: `logicprobe_export` (dsh) and the engine's `export` subcommand (non-dsh) emit
35
+ native inputs for UPPAAL (.xta + queries), TLA+ (TLC spec + safety), PRISM (.pm + .pctl),
36
+ and SPIN (Promela + ltl) from a LogicModelV1. Both translate the core machine (guards,
37
+ updates, weights; booleans as integers) and return unrepresentable invariants as explicit
38
+ warnings.
@@ -69,6 +69,13 @@ Round-trip reports (`logicprobe_uml action=review`, `roundTrip`) carry `hashSpec
69
69
  alongside `modelHash` and `parsedHash`, so a fidelity comparison is always
70
70
  spec-qualified.
71
71
 
72
+ **Read `diffs`, not `ok`.** `roundTrip.ok` says only that the model was rendered and the
73
+ result read back; it is `true` while `modelHash` and `parsedHash` legitimately differ,
74
+ because the model hash covers what a diagram cannot carry (the `narrative` block, in
75
+ particular) and the parsed hash covers what the diagram actually says. Fidelity is
76
+ `roundTrip.diffs.length === 0`. Equal hashes are neither necessary nor sufficient for
77
+ fidelity, and the two must not be forced to agree.
78
+
72
79
  Every report also aggregates its hashes under `hashes`, so a baseline diff or an archive
73
80
  record reads one field instead of walking the report:
74
81
 
@@ -83,6 +90,24 @@ dependency matrix was supplied. A data-model report carries `hashes.modelHash`;
83
90
  data-model hash uses the same normalization but its schema is not covered by the
84
91
  published specification.
85
92
 
93
+ A `diagram` hash is a **text** hash, not a semantic one: `sha256` over the exact
94
+ characters, so comments and `note` blocks change it, and so does a trailing newline.
95
+ Two renderings that mean the same thing hash differently when their text differs, and
96
+ that is intended — it is what makes "the diagram I reviewed" checkable. For one model
97
+ (`modelHash` `77a43a39822147707229638515cf8f0c6bef4b08635ea6197d2181fc6c26f0b4`) four
98
+ probe variants hash as:
99
+
100
+ | Diagram text | `hashes.diagram` |
101
+ |--------------|------------------|
102
+ | clean | `25ee1d0b803c9477d526d92c1dbb9059e408e282e515499dba06d14630286c8d` |
103
+ | one `%%` comment line | `8c41f046c954e1dc086eee8bd24c3d1d033b489840895520002c1e8120dda052` |
104
+ | one `'` comment line (PlantUML style) | `5552a91a73972edb71ea98f460fa6a393c6556bd1241461f0f29cd51531e9e56` |
105
+ | one multi-line `note … end note` block | `359182313f451713a937f5148328d238e8ea1941fbcef663c4c34e0a7f1bbcda` |
106
+
107
+ Compare diagram hashes only between identical byte strings: the same diagram handed
108
+ over without its trailing newline hashes differently (`8c41f046…` becomes
109
+ `c8f3a41b…`). That is the specification, not drift.
110
+
86
111
  ## Checking an archived hash
87
112
 
88
113
  `--hash-spec` recomputes under a named specification; `--hash-check` answers whether a
@@ -202,7 +202,7 @@ When a probe finds a counter-example, classify it:
202
202
 
203
203
  ### Model Fidelity Warning
204
204
 
205
- The model is an APPROXIMATION. It models state transitions, not execution semantics. A model that passes all 22 checks (S1-S8 structural, A1-A14) means the plan's LOGIC is consistent — NOT that the implementation will work. Always follow logic verification with code-level review.
205
+ The model is an APPROXIMATION. It models state transitions, not execution semantics. A model that passes every check (S1-S8 structural, A1-A14) means the plan's LOGIC is consistent — NOT that the implementation will work. Always follow logic verification with code-level review.
206
206
 
207
207
  ## Refactoring Verification
208
208
 
@@ -41,3 +41,4 @@ The full routing table, including hard-real-time, hybrid-control and execution-c
41
41
  2. **No absolute language in the answer.** "This is thread-safe" is exactly the claim being mined, not a result this skill can produce.
42
42
  3. **One counterexample is enough to refute.** If a race is suspected, the deliverable is the interleaving that breaks it plus the harness that demonstrates it (TSan/Helgrind output), not an argument.
43
43
  4. **Sequential targets are out of scope.** If nothing runs concurrently, say so and stop.
44
+ 5. **The scanner is dsh-only.** `logicprobe_concurrency_scan` has no mirror subcommand. Without it, mine the text by hand against the keyword table above and route each claim the same way.
@@ -16,6 +16,7 @@ This skill covers the diagram half of the Logic Probe toolbox. The claim-verific
16
16
  | Modelling a code flow as a state, activity or sequence diagram | `logicprobe_uml action=render`: model to diagram source, with a citation per state, event, guard and action |
17
17
  | Reading a hand-drawn diagram back into a model | `action=parse`: Mermaid or PlantUML state/activity text into a LogicModelV1 that `logicprobe_verify` can check |
18
18
  | Auditing a diagram somebody drew | `action=review`: structural defects, documentation gaps, and render-versus-model round-trip fidelity |
19
+ | Printing what the parser accepts for labels and comments | `action=explain-labels`: the accepted label spellings, the form the renderer writes, and which lines are ignored (the CLI mirror is `uml-review --explain-labels`) |
19
20
  | A diagram whose claims must be tied to the code | Every element needs a source citation; the review says what the diagram says, not whether the code is correct |
20
21
 
21
22
  Pipeline (DSH):
@@ -29,8 +30,9 @@ code flow → model (citation per element) → logicprobe_uml action=render →
29
30
  - **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.
30
31
  - **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. Two inputs are refused because they cannot become a machine: a sequence diagram (a trace cannot reconstruct a machine) and a different Mermaid family (`classDiagram`, `erDiagram`, `gantt`, `mindmap`, …) — both come back as `errorCode: "UML_NOT_A_STATE_DIAGRAM"` with the discarded construct named.
31
32
  - **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?
33
+ - **explain-labels** — the parser's own account of labels and comments: the accepted spellings, the form the renderer writes, and which lines it ignores. No diagram needed. It answers "why does my hand-drawn diagram report drift or ignored lines?" without reading the source.
32
34
 
33
- A host without the tools runs the same three actions through the repository engine. The engine is deliberately single-source — do not copy it into this skill.
35
+ A host without the tools runs the same four actions through the repository engine. The engine is deliberately single-source — do not copy it into this skill.
34
36
 
35
37
  ## A Structure Diagram Is the Dangerous Case, Because It Parses
36
38
 
@@ -227,10 +227,10 @@ Now delete the `ERROR --cooldown--> STARTING` arrow and review again. `ACTIVE` a
227
227
  ## Labels and comments: what the parser expects
228
228
 
229
229
  Two things cause most of the friction with hand-written diagrams. Both are printed by the
230
- tool itself: `logicprobe_uml` does not have a flag for it (it documents the convention in
231
- its description), and the CLI has `uml-review --explain-labels` (add `--notation
232
- plantuml` for that flavour), which prints exactly the table below from the same rules the
233
- parser applies.
230
+ tools themselves: `logicprobe_uml action="explain-labels"` returns the convention (pass
231
+ `notation: "plantuml"` for that flavour), and the CLI mirror is `uml-review
232
+ --explain-labels` (with `--notation plantuml`). Either one prints exactly the table below,
233
+ from the same rules the parser applies.
234
234
 
235
235
  **1. State meanings, in the spellings the parser accepts.** A label carries a *meaning*
236
236
  when it differs from the bare id. Accepted, in this order:
@@ -254,12 +254,13 @@ directives the renderer writes (`%%logicprobe:` in Mermaid, `'logicprobe:` in Pl
254
254
  `uml v1 …`, `init ID`, `terminal ID`, `alias X id`, `variable NAME kind`. They are
255
255
  ordinary comments to every renderer, which is why the diagram stays valid.
256
256
 
257
- Everything else is either notation chrome (skipped silently) or an ignored line:
257
+ Everything else is notation chrome (skipped silently), an ignored line, or a reported
258
+ construct:
258
259
 
259
260
  | Line | Behaviour |
260
261
  |---|---|
261
262
  | a `%%` / `'` comment that is not a directive | `UML_PARSE_IGNORED_LINE` warning — the line carried no statement |
262
- | a multi-line `note … end note` block | skipped silently (prose is not a state meaning) |
263
+ | a multi-line `note … end note` block | its body is skipped (prose is not a state meaning) and the block is reported once in `discardedConstructs` |
263
264
  | `@startuml`/`@enduml`, `stateDiagram-v2`, `direction`, `classDef`/`style`/`linkStyle`/`click`, `scale`, `skinparam`, `title`, `hide`, `autonumber` | skipped silently |
264
265
  | anything else the parser cannot read | `UML_PARSE_IGNORED_LINE` warning, and the model is built from what it did read |
265
266