dsh-logicprobe 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/engine.js CHANGED
@@ -1721,31 +1721,67 @@ function S8_monotonicVariables(model) {
1721
1721
  }
1722
1722
  return checkResult('S8', 'Monotonic Variables', findings, findings.length === 0 ? 'Monotonic variables are respected' : 'Monotonic findings: ' + findings.length);
1723
1723
  }
1724
+ /**
1725
+ * Find a run from `start` that never reaches `target`, or return undefined when
1726
+ * every run does. The property is universal (see the A9 row in `SKILL.md`): one
1727
+ * branch that loops forever, or that stops before the target, refutes it.
1728
+ *
1729
+ * The search is a depth-first walk of the run graph with three colours, and target
1730
+ * runs are success leaves that are never expanded:
1731
+ *
1732
+ * - a node reached while it is on the current walk (GRAY) closes a cycle that avoids
1733
+ * the target, so some run never reaches it;
1734
+ * - a node with no outgoing step at all stops the machine where it stands, which is
1735
+ * the same violation for a different reason;
1736
+ * - a node whose walk completed without a violation is BLACK, and reaching it again
1737
+ * from another branch is a shared sub-graph, not a cycle.
1738
+ *
1739
+ * That last point is why the colour map cannot be a plain visited set: a diamond
1740
+ * (two branches rejoining) is acyclic and must pass, while a genuine cycle must not.
1741
+ * A run is identified by its state plus its variable values, so the same state id
1742
+ * with different values is a different node.
1743
+ */
1724
1744
  function findLeadsToBadPath(model, start, target) {
1725
1745
  if (start.state === target)
1726
1746
  return undefined;
1727
- const visited = new Set();
1728
- const queue = [{ runtime: start, path: [] }];
1729
- while (queue.length > 0) {
1730
- const entry = queue.shift();
1731
- const key = runtimeKey(entry.runtime);
1732
- if (entry.runtime.state === target)
1733
- continue;
1734
- if (visited.has(key))
1735
- return { path: entry.path, reason: 'Cycle avoids target ' + target };
1736
- visited.add(key);
1737
- const nexts = [];
1738
- for (const event of allEvents(model)) {
1739
- for (const next of stepRuntime(model, entry.runtime, event))
1740
- nexts.push({ next, event });
1747
+ const GRAY = 1;
1748
+ const BLACK = 2;
1749
+ const color = new Map();
1750
+ const stack = [{ runtime: start, path: [], nexts: undefined, index: 0 }];
1751
+ color.set(runtimeKey(start), GRAY);
1752
+ while (stack.length > 0) {
1753
+ const frame = stack[stack.length - 1];
1754
+ if (frame.nexts === undefined) {
1755
+ const nexts = [];
1756
+ for (const event of allEvents(model)) {
1757
+ for (const next of stepRuntime(model, frame.runtime, event))
1758
+ nexts.push({ next, event });
1759
+ }
1760
+ frame.nexts = nexts;
1761
+ if (nexts.length === 0)
1762
+ return { path: frame.path, reason: 'Dead end before target ' + target };
1741
1763
  }
1742
- if (nexts.length === 0)
1743
- return { path: entry.path, reason: 'Dead end before target ' + target };
1744
- for (const { next, event } of nexts) {
1745
- queue.push({ runtime: next, path: [...entry.path, { from: entry.runtime.state, event, to: next.state }] });
1764
+ if (frame.index >= frame.nexts.length) {
1765
+ color.set(runtimeKey(frame.runtime), BLACK);
1766
+ stack.pop();
1767
+ continue;
1746
1768
  }
1769
+ const { next, event } = frame.nexts[frame.index];
1770
+ frame.index += 1;
1771
+ const step = { from: frame.runtime.state, event, to: next.state };
1772
+ if (next.state === target)
1773
+ continue;
1774
+ const key = runtimeKey(next);
1775
+ const seen = color.get(key);
1776
+ if (seen === GRAY)
1777
+ return { path: [...frame.path, step], reason: 'Cycle avoids target ' + target };
1778
+ if (seen === BLACK)
1779
+ continue;
1780
+ color.set(key, GRAY);
1781
+ stack.push({ runtime: next, path: [...frame.path, step], nexts: undefined, index: 0 });
1747
1782
  }
1748
- return { path: [], reason: 'No path reaches target ' + target };
1783
+ // Every branch either reached the target or rejoined a branch that did.
1784
+ return undefined;
1749
1785
  }
1750
1786
  function A9_leadsTo(model, exploration) {
1751
1787
  const findings = [];
package/lib/index.js CHANGED
@@ -33,6 +33,7 @@ import { logicProbeDataModelVerifyTool, DATA_ENGINE_SCHEMA_VERSION } from './dat
33
33
  import { logicProbeConcurrencyScanTool } from './concurrency-tool.js';
34
34
  import { logicProbeComposeTool } from './compose-tool.js';
35
35
  import { logicProbeExportTool } from './export-tool.js';
36
+ import { logicProbeUmlTool } from './uml-tool.js';
36
37
  import { ENGINE_SCHEMA_VERSION } from './engine.js';
37
38
  export const name = 'logicprobe';
38
39
  // Skills are contributed through the registry service, which dsh-base always
@@ -59,7 +60,7 @@ Plugin logicprobe is active. Documents are not truth — code is. Verify every v
59
60
  | "I'll verify while implementing" | Verification happens before implementation, not during. |
60
61
  | "I can check this with reasoning alone" | Behavioral claims are verified with code/models, not intuition. One counter-example refutes a universal claim. |
61
62
 
62
- **Native verification path**: In dsh, prefer the \`logicprobe_verify\` tool for state-machine checks and \`logicprobe_datamodel_verify\` for data-model/schema migration checks. Both support before/after regression and common domain constraints (idempotency, monotonic, sequence, leads-to, atomicity). Python harnesses remain the fallback for non-dsh hosts.
63
+ **Native verification path**: In dsh, prefer the \`logicprobe_verify\` tool for state-machine checks and \`logicprobe_datamodel_verify\` for data-model/schema migration checks. Both support before/after regression and common domain constraints (idempotency, monotonic, sequence, leads-to, atomicity). To model a code flow as UML, or to audit such a modelling, use \`logicprobe_uml\` (render | parse | review). Python harnesses remain the fallback for non-dsh hosts.
63
64
 
64
65
  **Proactive suggestion**: When a user asks code-level behavioral questions — "could this state machine deadlock", "is this retry limit safe", "check this timing sequence for bugs", "is this migration non-breaking", "does this copy cover all required fields" — suggest logicprobe as an optional verification pass (do not auto-escalate).
65
66
  </EXTREMELY_IMPORTANT>`;
@@ -146,6 +147,7 @@ function modeContextText(config, session) {
146
147
  const interaction = resolveInteraction(config, session);
147
148
  const lines = [
148
149
  'logicprobe: use `logicprobe_verify` for state machines and `logicprobe_datamodel_verify` for data models; both cover before/after regression and common domain constraints.',
150
+ 'logicprobe: use `logicprobe_uml` to model a code flow as UML (render), to read a UML diagram back into a model (parse), or to audit the modelling (review: structural defects, documentation gaps, diagram-vs-model round-trip fidelity).',
149
151
  interaction === 'auto'
150
152
  ? 'logicprobe interaction=auto: do NOT call ask_user_question for model confirmation; run round-trip validation of the extracted transition table and mark the result UNCONFIRMED.'
151
153
  : 'logicprobe interaction=ask: show the extracted transition table and get user confirmation before running verification.',
@@ -160,7 +162,7 @@ function modeContextText(config, session) {
160
162
  * lets the model read this plugin's runtime status without guessing. Mirrors
161
163
  * the registration pattern of the official dsh-tool-cordis host providers.
162
164
  */
163
- function inspectProvider(config, isToolRegistered, isDataToolRegistered, isConcurrencyToolRegistered, isComposeToolRegistered, isExportToolRegistered) {
165
+ function inspectProvider(config, isToolRegistered, isDataToolRegistered, isConcurrencyToolRegistered, isComposeToolRegistered, isExportToolRegistered, isUmlToolRegistered) {
164
166
  return {
165
167
  manifest: {
166
168
  id: 'logicprobe',
@@ -186,10 +188,11 @@ function inspectProvider(config, isToolRegistered, isDataToolRegistered, isConcu
186
188
  concurrencyToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_concurrency_scan tool is registered on ctx.tools.' },
187
189
  composeToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_compose_verify tool is registered on ctx.tools.' },
188
190
  exportToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_export tool is registered on ctx.tools.' },
191
+ umlToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_uml tool (UML modelling + modelling review) is registered on ctx.tools.' },
189
192
  engineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled state-machine verification engine accepts.' },
190
193
  dataEngineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled data-model verification engine accepts.' },
191
194
  },
192
- required: ['enabled', 'gateContentLength', 'interaction', 'toolRegistered', 'dataToolRegistered', 'concurrencyToolRegistered', 'composeToolRegistered', 'exportToolRegistered', 'engineSchemaVersion', 'dataEngineSchemaVersion'],
195
+ required: ['enabled', 'gateContentLength', 'interaction', 'toolRegistered', 'dataToolRegistered', 'concurrencyToolRegistered', 'composeToolRegistered', 'exportToolRegistered', 'umlToolRegistered', 'engineSchemaVersion', 'dataEngineSchemaVersion'],
193
196
  additionalProperties: false,
194
197
  },
195
198
  },
@@ -206,6 +209,7 @@ function inspectProvider(config, isToolRegistered, isDataToolRegistered, isConcu
206
209
  concurrencyToolRegistered: isConcurrencyToolRegistered(),
207
210
  composeToolRegistered: isComposeToolRegistered(),
208
211
  exportToolRegistered: isExportToolRegistered(),
212
+ umlToolRegistered: isUmlToolRegistered(),
209
213
  engineSchemaVersion: ENGINE_SCHEMA_VERSION,
210
214
  dataEngineSchemaVersion: DATA_ENGINE_SCHEMA_VERSION,
211
215
  };
@@ -224,6 +228,7 @@ export function apply(ctx, config) {
224
228
  let concurrencyToolRegistered = false;
225
229
  let composeToolRegistered = false;
226
230
  let exportToolRegistered = false;
231
+ let umlToolRegistered = false;
227
232
  let modeContextRegistered = false;
228
233
  const registerProvider = () => {
229
234
  if (providerRegistered)
@@ -232,7 +237,7 @@ export function apply(ctx, config) {
232
237
  if (inspect === undefined)
233
238
  return;
234
239
  try {
235
- ctx.effect(() => inspect.register(inspectProvider(config, () => toolRegistered, () => dataToolRegistered, () => concurrencyToolRegistered, () => composeToolRegistered, () => exportToolRegistered)), 'logicprobe: inspect provider');
240
+ ctx.effect(() => inspect.register(inspectProvider(config, () => toolRegistered, () => dataToolRegistered, () => concurrencyToolRegistered, () => composeToolRegistered, () => exportToolRegistered, () => umlToolRegistered)), 'logicprobe: inspect provider');
236
241
  providerRegistered = true;
237
242
  }
238
243
  catch (err) {
@@ -251,11 +256,13 @@ export function apply(ctx, config) {
251
256
  ctx.effect(() => tools.register(logicProbeConcurrencyScanTool), 'logicprobe: concurrency scan tool');
252
257
  ctx.effect(() => tools.register(logicProbeComposeTool), 'logicprobe: compose tool');
253
258
  ctx.effect(() => tools.register(logicProbeExportTool), 'logicprobe: export tool');
259
+ ctx.effect(() => tools.register(logicProbeUmlTool), 'logicprobe: uml tool');
254
260
  toolRegistered = true;
255
261
  dataToolRegistered = true;
256
262
  concurrencyToolRegistered = true;
257
263
  composeToolRegistered = true;
258
264
  exportToolRegistered = true;
265
+ umlToolRegistered = true;
259
266
  }
260
267
  catch (err) {
261
268
  console.warn('[logicprobe] logicprobe_verify/logicprobe_datamodel_verify tool registration failed', err);
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Structural mirror of the JSON value union the dsh tool registry accepts.
3
+ *
4
+ * `@deepseek-ai/dsh-tools` exported this type through the 0.1.x line and stopped
5
+ * exporting it in 0.2.x, where the same type now lives in
6
+ * `@deepseek-ai/dsh-util-values`. This package declares peer support for both
7
+ * lines, so it cannot import the type from either one without breaking the other
8
+ * build. Declaring the one-line union here keeps `npm run typecheck` green against
9
+ * both, and it costs nothing at runtime: every use is a type-only cast on a value
10
+ * this package already produced.
11
+ *
12
+ * Keep it structurally identical to the official type. A cast through `unknown` to
13
+ * this alias is only a way to satisfy `defineTool`'s return-type constraint; if the
14
+ * official union ever widens, this alias must widen with it.
15
+ *
16
+ * @module logicprobe-json-value
17
+ */
18
+ export {};
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Structural mirror of the JSON value union the dsh tool registry accepts.
3
+ *
4
+ * `@deepseek-ai/dsh-tools` exported this type through the 0.1.x line and stopped
5
+ * exporting it in 0.2.x, where the same type now lives in
6
+ * `@deepseek-ai/dsh-util-values`. This package declares peer support for both
7
+ * lines, so it cannot import the type from either one without breaking the other
8
+ * build. Declaring the one-line union here keeps `npm run typecheck` green against
9
+ * both, and it costs nothing at runtime: every use is a type-only cast on a value
10
+ * this package already produced.
11
+ *
12
+ * Keep it structurally identical to the official type. A cast through `unknown` to
13
+ * this alias is only a way to satisfy `defineTool`'s return-type constraint; if the
14
+ * official union ever widens, this alias must widen with it.
15
+ *
16
+ * @module logicprobe-json-value
17
+ */
18
+ /** JSON-serializable value: null, boolean, number, string, or an array/object of those. */
19
+ export type JsonValue = null | boolean | number | string | JsonValue[] | {
20
+ [key: string]: JsonValue;
21
+ };
@@ -0,0 +1,14 @@
1
+ export declare const LOGICPROBE_UML_TOOL_NAME = "logicprobe_uml";
2
+ /**
3
+ * DSH tool wrapping the UML front end: model a code flow as a UML diagram
4
+ * (`action: "render"`), read a UML diagram back into a LogicModelV1
5
+ * (`action: "parse"`), or review the modelling itself (`action: "review"`).
6
+ *
7
+ * review is the half that makes the feature a check rather than a drawing
8
+ * utility: it reports structural defects the diagram would present as valid
9
+ * flow (unreachable states, dead ends, ambiguous and non-exhaustive branches,
10
+ * self-loops with no exit), documentation gaps (a symbol no reader can map back
11
+ * to code), and — the fidelity check — whether the rendered diagram reads back
12
+ * as the model it was drawn from.
13
+ */
14
+ export declare const logicProbeUmlTool: import("@deepseek-ai/dsh-tools").ToolDefinition;
@@ -0,0 +1,167 @@
1
+ /**
2
+ * UML front end for LogicModelV1 — model a code flow as a UML diagram, then
3
+ * review the modelling itself.
4
+ *
5
+ * Two halves, one data flow:
6
+ *
7
+ * 1. `renderUml` turns a validated LogicModelV1 into Mermaid or PlantUML text
8
+ * (state machine, activity/flow, or sequence trace). The diagram is a
9
+ * *view*: it never invents structure the model does not have, and anything
10
+ * the notation cannot express is reported as a warning instead of being
11
+ * dropped silently.
12
+ * 2. `parseUml` reads that text back into a LogicModelV1, and `reviewUml`
13
+ * compares the two. That comparison is the point of the feature: a UML
14
+ * diagram of a code flow is itself a model, and a model can be wrong —
15
+ * ambiguous branches, dead ends, flows nobody can enter, symbols the
16
+ * source never names. A diagram that does not round-trip to the model it
17
+ * was drawn from is mis-modelled, and the review says so.
18
+ *
19
+ * Why the round trip is the fidelity check: rendering and parsing are inverse
20
+ * only if every construct survives the notation. The generated text carries
21
+ * `logicprobe:` directives (ignored by Mermaid/PlantUML renderers) that pin the
22
+ * initial state, the terminal states and any state id the notation cannot spell
23
+ * verbatim, so an exact comparison is possible rather than a fuzzy one.
24
+ *
25
+ * The review deliberately does NOT replace `logicprobe_verify`: it checks the
26
+ * modelling (structure the diagram claims, documentation coverage, notation
27
+ * fidelity), while S1-S8/A1-A14 check the machine's behaviour (guard
28
+ * exhaustiveness under real valuations, invariant paths, deadlock/liveness in
29
+ * the runtime state space). Findings name the engine check to run next.
30
+ *
31
+ * @module logicprobe-uml
32
+ */
33
+ import type { GuardNode, LogicModelV1, UpdateSpec } from './engine.js';
34
+ export type UmlNotation = 'mermaid' | 'plantuml';
35
+ export type UmlDiagram = 'state' | 'activity' | 'sequence';
36
+ export declare const UML_NOTATIONS: readonly UmlNotation[];
37
+ export declare const UML_DIAGRAMS: readonly UmlDiagram[];
38
+ export interface UmlRenderResult {
39
+ notation: UmlNotation;
40
+ diagram: UmlDiagram;
41
+ /** The diagram source; hand this to the user or a renderer as-is. */
42
+ primary: string;
43
+ warnings: string[];
44
+ }
45
+ export interface UmlParseResult {
46
+ notation: UmlNotation;
47
+ diagram: UmlDiagram;
48
+ model: LogicModelV1;
49
+ /**
50
+ * Display labels found in the diagram, keyed by state id. They are how a
51
+ * reader learns what a symbol means; a missing entry is an undocumented
52
+ * symbol, which the review reports.
53
+ */
54
+ labels: Record<string, string>;
55
+ warnings: string[];
56
+ }
57
+ export interface UmlFinding {
58
+ code: string;
59
+ severity: 'error' | 'warning' | 'info';
60
+ message: string;
61
+ states?: string[];
62
+ events?: string[];
63
+ transitions?: Array<{
64
+ from: string;
65
+ event: string;
66
+ to: string;
67
+ }>;
68
+ detail?: string;
69
+ }
70
+ export interface UmlRoundTripReport {
71
+ notation: UmlNotation;
72
+ diagram: UmlDiagram;
73
+ ok: boolean;
74
+ modelHash: string;
75
+ parsedHash: string;
76
+ diffs: string[];
77
+ warnings: string[];
78
+ }
79
+ export interface UmlReviewReport {
80
+ ok: boolean;
81
+ source: 'model' | 'diagram' | 'model+diagram';
82
+ summary: {
83
+ errors: number;
84
+ warnings: number;
85
+ info: number;
86
+ states: number;
87
+ events: number;
88
+ transitions: number;
89
+ terminalStates: number;
90
+ reachableStates: number;
91
+ documentedStates: number;
92
+ };
93
+ findings: UmlFinding[];
94
+ roundTrip: UmlRoundTripReport | null;
95
+ /** Diagram display labels, when a diagram took part in the review. */
96
+ labels?: Record<string, string>;
97
+ /** Model parsed from the diagram, when a diagram was given (feed it to `logicprobe_verify`). */
98
+ model?: LogicModelV1;
99
+ /** Diagram rendered from the model, when only a model was given. */
100
+ primary?: string;
101
+ warnings: string[];
102
+ nextSteps: string[];
103
+ }
104
+ export declare class UmlError extends Error {
105
+ }
106
+ /** Canonical guard text. Rendering wraps every composite node in parentheses, and the parser flattens same-operator chains, so render∘parse is the identity. */
107
+ export declare function guardText(node: GuardNode): string;
108
+ /**
109
+ * Render a LogicModelV1 as UML.
110
+ *
111
+ * PlantUML has no faithful activity view here: its activity syntax is a
112
+ * structured flowchart language, so a graph with merges or cycles needs a
113
+ * while/if reconstruction this module does not perform. Refusing is the honest
114
+ * outcome — quietly emitting a state diagram under an "activity" request would
115
+ * mislabel the model. Mermaid covers all three views.
116
+ *
117
+ * @param input - candidate LogicModelV1.
118
+ * @param notation - `mermaid` (default) or `plantuml`.
119
+ * @param diagram - `state` (default), `activity`, or `sequence`.
120
+ * @param maxSteps - cap on the sequence trace length.
121
+ */
122
+ export declare function renderUml(input: unknown, notation?: UmlNotation, diagram?: UmlDiagram, maxSteps?: number): UmlRenderResult;
123
+ /** Parse a guard expression such as `(retry < 3 && armed == true)`. */
124
+ export declare function parseGuardText(text: string): GuardNode;
125
+ /** Parse a UML action clause such as `retry := retry + 1, armed := true`. */
126
+ export declare function parseUpdatesText(text: string, warnings: string[]): UpdateSpec[];
127
+ /**
128
+ * Parse a Mermaid or PlantUML diagram back into a LogicModelV1.
129
+ *
130
+ * State and activity diagrams carry the whole machine, so they parse into a
131
+ * complete model. Sequence diagrams do not: a trace shows the paths that were
132
+ * walked, not the branches that were not, so parsing one would silently prune
133
+ * the machine. That case is refused rather than approximated.
134
+ *
135
+ * @param text - diagram source.
136
+ * @param notation - `auto` (default) detects Mermaid vs PlantUML from the text.
137
+ */
138
+ export declare function parseUml(text: string, notation?: UmlNotation | 'auto'): UmlParseResult;
139
+ /** Compare two machines by structure — the fidelity measure behind the round-trip check. */
140
+ export declare function diffModels(left: LogicModelV1, right: LogicModelV1): string[];
141
+ export interface UmlReviewOptions {
142
+ /** LogicModelV1 to review. Provide it, `diagram`, or both. */
143
+ model?: unknown;
144
+ /** Diagram text: reviewed on its own, or compared against `model` when both are given. */
145
+ diagram?: string;
146
+ notation?: UmlNotation | 'auto';
147
+ diagramKind?: UmlDiagram;
148
+ /** Render-and-reparse fidelity check (default true; only meaningful with a model). */
149
+ roundTrip?: boolean;
150
+ /** Cap on the sequence trace used for the fidelity check. */
151
+ maxSteps?: number;
152
+ }
153
+ /**
154
+ * Review a UML model of a code flow.
155
+ *
156
+ * Three inputs are possible and each answers a different question:
157
+ *
158
+ * - `model` only — "is this machine well-modelled?" The review checks the
159
+ * structure the diagram would draw, then renders and re-parses it to prove the
160
+ * diagram carries the machine faithfully (round trip).
161
+ * - `diagram` only — "what does this diagram actually say?" The diagram is parsed
162
+ * into a model, and that model is reviewed; nothing can be said about fidelity
163
+ * to a machine the caller did not provide.
164
+ * - both — "does this diagram match this model?" Any structural difference is a
165
+ * modelling defect and is reported both as round-trip diffs and as a finding.
166
+ */
167
+ export declare function reviewUml(options: UmlReviewOptions): UmlReviewReport;
@@ -0,0 +1,104 @@
1
+ import { defineTool } from '@deepseek-ai/dsh-tools';
2
+ import { renderUml, parseUml, reviewUml, UmlError } from './uml.js';
3
+ export const LOGICPROBE_UML_TOOL_NAME = 'logicprobe_uml';
4
+ /**
5
+ * DSH tool wrapping the UML front end: model a code flow as a UML diagram
6
+ * (`action: "render"`), read a UML diagram back into a LogicModelV1
7
+ * (`action: "parse"`), or review the modelling itself (`action: "review"`).
8
+ *
9
+ * review is the half that makes the feature a check rather than a drawing
10
+ * utility: it reports structural defects the diagram would present as valid
11
+ * flow (unreachable states, dead ends, ambiguous and non-exhaustive branches,
12
+ * self-loops with no exit), documentation gaps (a symbol no reader can map back
13
+ * to code), and — the fidelity check — whether the rendered diagram reads back
14
+ * as the model it was drawn from.
15
+ */
16
+ export const logicProbeUmlTool = defineTool({
17
+ name: LOGICPROBE_UML_TOOL_NAME,
18
+ description: 'Model a code flow as UML, and review the modelling. action="render" turns a LogicModelV1 (schemaVersion=1) into diagram source: notation mermaid (state | activity flowchart | sequence) or plantuml (state | sequence). action="parse" reads Mermaid/PlantUML state or activity text back into a LogicModelV1 (plus the display labels it found), so a hand-drawn diagram can be verified with logicprobe_verify; a sequence diagram is refused because a trace cannot reconstruct a machine. action="review" audits the modelling: structural defects (UML002 unreachable state, UML003 dead end, UML004 ambiguous branch, UML005 overlapping guard, UML006 inexhaustive branch, UML007 unused event, UML008 self-loop with no exit, UML009 duplicate transition), documentation gaps (UML010 unused variable, UML011 unbounded variable, UML012 no terminal, UML013 no narrative, UML014 undocumented state, UML015 label drift), and the fidelity check — the diagram is rendered and re-parsed and any structural difference is reported as UML017 round-trip mismatch. Give review a model (checks it, renders and re-parses it), a diagram (parses and reviews that), or both (checks the diagram against the model). Rendering never invents structure and never silently drops a construct the notation cannot express: those become warnings. Review does NOT replace logicprobe_verify — it covers the modelling, not behaviour; findings name the engine check to run next.',
19
+ parameters: {
20
+ action: {
21
+ type: 'string',
22
+ required: true,
23
+ enum: ['render', 'parse', 'review'],
24
+ description: 'render = model → UML source; parse = UML source → model; review = audit the modelling (and its fidelity to the model).',
25
+ },
26
+ model: {
27
+ type: 'json',
28
+ description: 'LogicModelV1 machine. Required for render; accepted by review (alone, or next to diagram to check the two against each other).',
29
+ },
30
+ diagram: {
31
+ type: 'string',
32
+ description: 'UML source text. Required for parse; accepted by review.',
33
+ },
34
+ notation: {
35
+ type: 'string',
36
+ enum: ['auto', 'mermaid', 'plantuml'],
37
+ description: 'Diagram language. Default auto: detected from the text for parse/review, mermaid for render.',
38
+ },
39
+ kind: {
40
+ type: 'string',
41
+ enum: ['state', 'activity', 'sequence'],
42
+ description: 'Diagram kind to render. Default state. sequence is one BFS trace, not the whole machine. PlantUML activity is refused (its structured-flowchart syntax cannot faithfully carry a graph with merges or cycles) — use mermaid for that view.',
43
+ },
44
+ roundTrip: {
45
+ type: 'boolean',
46
+ description: 'review only: render and re-parse the model to prove the diagram carries it. Default true.',
47
+ },
48
+ maxSteps: {
49
+ type: 'integer',
50
+ description: 'Cap on the sequence trace length when rendering kind=sequence. Default 60.',
51
+ },
52
+ },
53
+ output: {
54
+ schema: {
55
+ type: 'json',
56
+ description: 'Render result (diagram source), parse result (LogicModelV1 + labels), or the modelling review report with findings, round-trip diff and next steps.',
57
+ },
58
+ render(_args, value) {
59
+ const record = value;
60
+ // A rendered diagram is source text: print it verbatim so it can be copied
61
+ // into a file, then the structured result (warnings, notation, kind).
62
+ if (typeof record.diagram === 'string') {
63
+ return [
64
+ { type: 'text', text: record.diagram },
65
+ { type: 'text', text: JSON.stringify(value, null, 2) },
66
+ ];
67
+ }
68
+ return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
69
+ },
70
+ },
71
+ timeoutMs: 10_000,
72
+ isConcurrencySafe: () => true,
73
+ async execute(args) {
74
+ try {
75
+ if (args.action === 'render') {
76
+ if (args.model === undefined)
77
+ return errorResult('action=render needs `model` (a LogicModelV1 object)');
78
+ const result = renderUml(args.model, (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation), (args.kind ?? 'state'), args.maxSteps);
79
+ return { ok: true, action: 'render', notation: result.notation, kind: result.diagram, diagram: result.primary, warnings: result.warnings };
80
+ }
81
+ if (args.action === 'parse') {
82
+ if (args.diagram === undefined)
83
+ return errorResult('action=parse needs `diagram` (Mermaid or PlantUML text)');
84
+ const result = parseUml(args.diagram, args.notation ?? 'auto');
85
+ return { ok: true, action: 'parse', notation: result.notation, kind: result.diagram, model: result.model, labels: result.labels, warnings: result.warnings };
86
+ }
87
+ const report = reviewUml({
88
+ model: args.model,
89
+ diagram: args.diagram,
90
+ notation: args.notation ?? 'auto',
91
+ diagramKind: (args.kind ?? 'state'),
92
+ roundTrip: args.roundTrip,
93
+ maxSteps: args.maxSteps,
94
+ });
95
+ return report;
96
+ }
97
+ catch (error) {
98
+ return errorResult(error instanceof Error ? error.message : String(error), error instanceof UmlError);
99
+ }
100
+ },
101
+ });
102
+ function errorResult(message, isUmlError = false) {
103
+ return { ok: false, ...(isUmlError ? { errorCode: 'UML_INPUT' } : {}), error: message };
104
+ }