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/README.en-US.md +97 -58
- package/README.md +84 -46
- package/lib/engine.js +55 -19
- package/lib/index.js +11 -4
- package/lib/json-value.js +18 -0
- package/lib/types/json-value.d.ts +21 -0
- package/lib/types/uml-tool.d.ts +14 -0
- package/lib/types/uml.d.ts +167 -0
- package/lib/uml-tool.js +104 -0
- package/lib/uml.js +1544 -0
- package/package.json +27 -21
- package/skills/logicprobe/SKILL.md +36 -1
- package/skills/logicprobe/references/logicprobe-engine.py +1632 -21
- package/skills/logicprobe/references/uml-modeling-guide.md +166 -0
- package/src/compose-tool.ts +2 -1
- package/src/concurrency-tool.ts +2 -1
- package/src/data-tool.ts +2 -1
- package/src/engine.ts +57 -15
- package/src/export-tool.ts +2 -1
- package/src/index.ts +11 -4
- package/src/json-value.ts +20 -0
- package/src/tool.ts +2 -1
- package/src/uml-tool.ts +106 -0
- package/src/uml.ts +1461 -0
- package/skills/logicprobe/references/__pycache__/logicprobe-engine.cpython-310.pyc +0 -0
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
|
|
1728
|
-
const
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
if (
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
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
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
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
|
-
|
|
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;
|
package/lib/uml-tool.js
ADDED
|
@@ -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
|
+
}
|