dsh-logicprobe 1.4.0 → 1.4.2

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/uml-tool.js CHANGED
@@ -1,153 +1,153 @@
1
- import { defineTool } from '@deepseek-ai/dsh-tools';
2
- import { createHash } from 'node:crypto';
3
- import { REPORT_SCHEMAS, DEFAULT_HASH_SPEC, modelHash, refusalVerdict, verdictOf, verdictOfFindings } from './engine.js';
4
- import { renderUml, parseUml, parseFindings, reviewUml, explainLabels, checkUmlIntent, UmlError } from './uml.js';
5
- import { renderReportText } from './report-view.js';
6
- export const LOGICPROBE_UML_TOOL_NAME = 'logicprobe_uml';
7
- /**
8
- * DSH tool wrapping the UML front end: model a code flow as a UML diagram
9
- * (`action: "render"`), read a UML diagram back into a LogicModelV1
10
- * (`action: "parse"`), or review the modelling itself (`action: "review"`).
11
- *
12
- * review is the half that makes the feature a check rather than a drawing
13
- * utility: it reports structural defects the diagram would present as valid
14
- * flow (unreachable states, dead ends, ambiguous and non-exhaustive branches,
15
- * self-loops with no exit), documentation gaps (a symbol no reader can map back
16
- * to code), and — the fidelity check — whether the rendered diagram reads back
17
- * as the model it was drawn from.
18
- */
19
- export const logicProbeUmlTool = defineTool({
20
- name: LOGICPROBE_UML_TOOL_NAME,
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. Declare `intent` (present, verify, fidelity) to say what the artifact is for: a view that cannot serve it is refused before any work, with errorCode UML_INTENT_UNSERVABLE, UML_VIEW_UNREPRESENTABLE or UML_INTENT_MISMATCH. Omit it and nothing changes. Read `verdict`, not `ok`: error findings mean a failed review.',
22
- parameters: {
23
- action: {
24
- type: 'string',
25
- required: true,
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.',
28
- },
29
- model: {
30
- type: 'json',
31
- description: 'LogicModelV1 machine. Required for render; accepted by review (alone, or next to diagram to check the two against each other).',
32
- },
33
- diagram: {
34
- type: 'string',
35
- description: 'UML source text. Required for parse; accepted by review.',
36
- },
37
- notation: {
38
- type: 'string',
39
- enum: ['auto', 'mermaid', 'plantuml'],
40
- description: 'Diagram language. Default auto: detected from the text for parse/review, mermaid for render.',
41
- },
42
- kind: {
43
- type: 'string',
44
- enum: ['state', 'activity', 'sequence'],
45
- 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.',
46
- },
47
- intent: {
48
- type: 'string',
49
- enum: ['present', 'verify', 'fidelity'],
50
- description: 'What the artifact is for, declared up front: present = a human will read the diagram (any view), verify = it must be parseable into a LogicModelV1 for the engine (state or activity), fidelity = the point is the render-and-reparse proof (review only, state or activity). A view that cannot serve the intent is refused before any work, with errorCode UML_INTENT_UNSERVABLE, UML_VIEW_UNREPRESENTABLE or UML_INTENT_MISMATCH. Omit it and nothing changes: a sequence view is still rendered, and review still reports UML019.',
51
- },
52
- roundTrip: {
53
- type: 'boolean',
54
- description: 'review only: render and re-parse the model to prove the diagram carries it. Default true.',
55
- },
56
- maxSteps: {
57
- type: 'integer',
58
- description: 'Cap on the sequence trace length when rendering kind=sequence. Default 60.',
59
- },
60
- reportView: {
61
- type: 'string',
62
- enum: ['digest', 'full'],
63
- description: 'digest (default) prints the diagram or report body; full prints the structured result as JSON.',
64
- },
65
- },
66
- output: {
67
- schema: {
68
- type: 'json',
69
- description: 'Render result (diagram source), parse result (LogicModelV1 + labels), or the modelling review report with findings, round-trip diff and next steps.',
70
- },
71
- render(args, value) {
72
- // A rendered diagram is source text: the report body prints it verbatim, once.
73
- // Echoing the whole result as JSON printed the same diagram a second time, escaped.
74
- return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full' }) }];
75
- },
76
- },
77
- timeoutMs: 10_000,
78
- isConcurrencySafe: () => true,
79
- async execute(args) {
80
- try {
81
- if (args.action === 'render') {
82
- if (args.model === undefined)
83
- return errorResult('action=render needs `model` (a LogicModelV1 object)');
84
- const result = renderUml(args.model, (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation), (args.kind ?? 'state'), args.maxSteps, args.intent);
85
- return { ok: true, ran: true, ...verdictOf(0, result.warnings.length), schema: REPORT_SCHEMAS.umlRender, action: 'render', notation: result.notation, kind: result.diagram, diagram: result.primary, hashes: { diagram: createHash('sha256').update(result.primary).digest('hex') }, warnings: result.warnings };
86
- }
87
- if (args.action === 'parse') {
88
- if (args.diagram === undefined)
89
- return errorResult('action=parse needs `diagram` (Mermaid or PlantUML text)');
90
- checkUmlIntent(args.intent, 'parse');
91
- const result = parseUml(args.diagram, args.notation ?? 'auto');
92
- // A diagram from another family still parses into *something*. `parseFindings`
93
- // says so, and the verdict is what stops that something from being trusted:
94
- // `ok: true` only means the parser ran.
95
- const findings = parseFindings(result);
96
- return {
97
- ok: true,
98
- ran: true,
99
- ...parseVerdict(result, findings),
100
- schema: REPORT_SCHEMAS.umlParse,
101
- action: 'parse',
102
- notation: result.notation,
103
- kind: result.diagram,
104
- model: result.model,
105
- labels: result.labels,
106
- findings,
107
- discardedConstructs: result.discardedConstructs,
108
- discardedEdges: result.discardedEdges,
109
- hashes: { hashSpec: DEFAULT_HASH_SPEC, modelHash: modelHash(result.model), diagram: createHash('sha256').update(args.diagram).digest('hex') },
110
- warnings: result.warnings,
111
- };
112
- }
113
- // R9: the label contract was documented in the library and in `explainLabels()`, but no
114
- // tool exposed it, so it was reachable only by reading the source or the docs.
115
- if (args.action === 'explain-labels') {
116
- checkUmlIntent(args.intent, 'labels');
117
- const notation = (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation);
118
- return {
119
- ok: true,
120
- ran: true,
121
- ...verdictOf(0, 0),
122
- schema: REPORT_SCHEMAS.umlExplainLabels,
123
- action: 'explain-labels',
124
- ...explainLabels(notation),
125
- };
126
- }
127
- const report = reviewUml({
128
- model: args.model,
129
- diagram: args.diagram,
130
- notation: args.notation ?? 'auto',
131
- diagramKind: (args.kind ?? 'state'),
132
- roundTrip: args.roundTrip,
133
- maxSteps: args.maxSteps,
134
- intent: args.intent,
135
- });
136
- return report;
137
- }
138
- catch (error) {
139
- const message = error instanceof Error ? error.message : String(error);
140
- return errorResult(message, error instanceof UmlError ? error.code : undefined);
141
- }
142
- },
143
- });
144
- /** The parse verdict counts parser notes as findings: an ignored line is information the model does not carry. */
145
- function parseVerdict(result, findings) {
146
- const errors = findings.filter((finding) => finding.severity === 'error').length;
147
- const warnings = findings.filter((finding) => finding.severity === 'warning').length + result.warnings.length;
148
- const firstCode = findings.find((finding) => finding.severity === 'error')?.code ?? findings.find((finding) => finding.severity === 'warning')?.code;
149
- return verdictOf(errors, warnings, firstCode);
150
- }
151
- function errorResult(message, errorCode = 'UML_INPUT') {
152
- return { ok: false, ran: false, ...refusalVerdict(message), errorCode, error: message };
153
- }
1
+ import { defineTool } from '@deepseek-ai/dsh-tools';
2
+ import { createHash } from 'node:crypto';
3
+ import { REPORT_SCHEMAS, DEFAULT_HASH_SPEC, modelHash, refusalVerdict, verdictOf, verdictOfFindings } from './engine.js';
4
+ import { renderUml, parseUml, parseFindings, reviewUml, explainLabels, checkUmlIntent, UmlError } from './uml.js';
5
+ import { renderReportText } from './report-view.js';
6
+ export const LOGICPROBE_UML_TOOL_NAME = 'logicprobe_uml';
7
+ /**
8
+ * DSH tool wrapping the UML front end: model a code flow as a UML diagram
9
+ * (`action: "render"`), read a UML diagram back into a LogicModelV1
10
+ * (`action: "parse"`), or review the modelling itself (`action: "review"`).
11
+ *
12
+ * review is the half that makes the feature a check rather than a drawing
13
+ * utility: it reports structural defects the diagram would present as valid
14
+ * flow (unreachable states, dead ends, ambiguous and non-exhaustive branches,
15
+ * self-loops with no exit), documentation gaps (a symbol no reader can map back
16
+ * to code), and — the fidelity check — whether the rendered diagram reads back
17
+ * as the model it was drawn from.
18
+ */
19
+ export const logicProbeUmlTool = defineTool({
20
+ name: LOGICPROBE_UML_TOOL_NAME,
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. Declare `intent` (present, verify, fidelity) to say what the artifact is for: a view that cannot serve it is refused before any work, with errorCode UML_INTENT_UNSERVABLE, UML_VIEW_UNREPRESENTABLE or UML_INTENT_MISMATCH. Omit it and nothing changes. Read `verdict`, not `ok`: error findings mean a failed review.',
22
+ parameters: {
23
+ action: {
24
+ type: 'string',
25
+ required: true,
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.',
28
+ },
29
+ model: {
30
+ type: 'json',
31
+ description: 'LogicModelV1 machine. Required for render; accepted by review (alone, or next to diagram to check the two against each other).',
32
+ },
33
+ diagram: {
34
+ type: 'string',
35
+ description: 'UML source text. Required for parse; accepted by review.',
36
+ },
37
+ notation: {
38
+ type: 'string',
39
+ enum: ['auto', 'mermaid', 'plantuml'],
40
+ description: 'Diagram language. Default auto: detected from the text for parse/review, mermaid for render.',
41
+ },
42
+ kind: {
43
+ type: 'string',
44
+ enum: ['state', 'activity', 'sequence'],
45
+ 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.',
46
+ },
47
+ intent: {
48
+ type: 'string',
49
+ enum: ['present', 'verify', 'fidelity'],
50
+ description: 'What the artifact is for, declared up front: present = a human will read the diagram (any view), verify = it must be parseable into a LogicModelV1 for the engine (state or activity), fidelity = the point is the render-and-reparse proof (review only, state or activity). A view that cannot serve the intent is refused before any work, with errorCode UML_INTENT_UNSERVABLE, UML_VIEW_UNREPRESENTABLE or UML_INTENT_MISMATCH. Omit it and nothing changes: a sequence view is still rendered, and review still reports UML019.',
51
+ },
52
+ roundTrip: {
53
+ type: 'boolean',
54
+ description: 'review only: render and re-parse the model to prove the diagram carries it. Default true.',
55
+ },
56
+ maxSteps: {
57
+ type: 'integer',
58
+ description: 'Cap on the sequence trace length when rendering kind=sequence. Default 60.',
59
+ },
60
+ reportView: {
61
+ type: 'string',
62
+ enum: ['digest', 'full'],
63
+ description: 'digest (default) prints the diagram or report body; full prints the structured result as JSON.',
64
+ },
65
+ },
66
+ output: {
67
+ schema: {
68
+ type: 'json',
69
+ description: 'Render result (diagram source), parse result (LogicModelV1 + labels), or the modelling review report with findings, round-trip diff and next steps.',
70
+ },
71
+ render(args, value) {
72
+ // A rendered diagram is source text: the report body prints it verbatim, once.
73
+ // Echoing the whole result as JSON printed the same diagram a second time, escaped.
74
+ return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full' }) }];
75
+ },
76
+ },
77
+ timeoutMs: 10_000,
78
+ isConcurrencySafe: () => true,
79
+ async execute(args) {
80
+ try {
81
+ if (args.action === 'render') {
82
+ if (args.model === undefined)
83
+ return errorResult('action=render needs `model` (a LogicModelV1 object)');
84
+ const result = renderUml(args.model, (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation), (args.kind ?? 'state'), args.maxSteps, args.intent);
85
+ return { ok: true, ran: true, ...verdictOf(0, result.warnings.length), schema: REPORT_SCHEMAS.umlRender, action: 'render', notation: result.notation, kind: result.diagram, diagram: result.primary, hashes: { diagram: createHash('sha256').update(result.primary).digest('hex') }, warnings: result.warnings };
86
+ }
87
+ if (args.action === 'parse') {
88
+ if (args.diagram === undefined)
89
+ return errorResult('action=parse needs `diagram` (Mermaid or PlantUML text)');
90
+ checkUmlIntent(args.intent, 'parse');
91
+ const result = parseUml(args.diagram, args.notation ?? 'auto');
92
+ // A diagram from another family still parses into *something*. `parseFindings`
93
+ // says so, and the verdict is what stops that something from being trusted:
94
+ // `ok: true` only means the parser ran.
95
+ const findings = parseFindings(result);
96
+ return {
97
+ ok: true,
98
+ ran: true,
99
+ ...parseVerdict(result, findings),
100
+ schema: REPORT_SCHEMAS.umlParse,
101
+ action: 'parse',
102
+ notation: result.notation,
103
+ kind: result.diagram,
104
+ model: result.model,
105
+ labels: result.labels,
106
+ findings,
107
+ discardedConstructs: result.discardedConstructs,
108
+ discardedEdges: result.discardedEdges,
109
+ hashes: { hashSpec: DEFAULT_HASH_SPEC, modelHash: modelHash(result.model), diagram: createHash('sha256').update(args.diagram).digest('hex') },
110
+ warnings: result.warnings,
111
+ };
112
+ }
113
+ // R9: the label contract was documented in the library and in `explainLabels()`, but no
114
+ // tool exposed it, so it was reachable only by reading the source or the docs.
115
+ if (args.action === 'explain-labels') {
116
+ checkUmlIntent(args.intent, 'labels');
117
+ const notation = (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation);
118
+ return {
119
+ ok: true,
120
+ ran: true,
121
+ ...verdictOf(0, 0),
122
+ schema: REPORT_SCHEMAS.umlExplainLabels,
123
+ action: 'explain-labels',
124
+ ...explainLabels(notation),
125
+ };
126
+ }
127
+ const report = reviewUml({
128
+ model: args.model,
129
+ diagram: args.diagram,
130
+ notation: args.notation ?? 'auto',
131
+ diagramKind: (args.kind ?? 'state'),
132
+ roundTrip: args.roundTrip,
133
+ maxSteps: args.maxSteps,
134
+ intent: args.intent,
135
+ });
136
+ return report;
137
+ }
138
+ catch (error) {
139
+ const message = error instanceof Error ? error.message : String(error);
140
+ return errorResult(message, error instanceof UmlError ? error.code : undefined);
141
+ }
142
+ },
143
+ });
144
+ /** The parse verdict counts parser notes as findings: an ignored line is information the model does not carry. */
145
+ function parseVerdict(result, findings) {
146
+ const errors = findings.filter((finding) => finding.severity === 'error').length;
147
+ const warnings = findings.filter((finding) => finding.severity === 'warning').length + result.warnings.length;
148
+ const firstCode = findings.find((finding) => finding.severity === 'error')?.code ?? findings.find((finding) => finding.severity === 'warning')?.code;
149
+ return verdictOf(errors, warnings, firstCode);
150
+ }
151
+ function errorResult(message, errorCode = 'UML_INPUT') {
152
+ return { ok: false, ran: false, ...refusalVerdict(message), errorCode, error: message };
153
+ }