dsh-model-everything 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.en-US.md +104 -76
  2. package/README.md +84 -71
  3. package/lib/automaton-tool.js +70 -0
  4. package/lib/automaton.js +328 -0
  5. package/lib/compose-tool.js +7 -0
  6. package/lib/concurrency-tool.js +7 -0
  7. package/lib/data-tool.js +7 -0
  8. package/lib/design-tool.js +8 -1
  9. package/lib/edit-tool.js +113 -0
  10. package/lib/editor.js +293 -0
  11. package/lib/engine.js +7 -0
  12. package/lib/export-tool.js +8 -0
  13. package/lib/index.js +4 -0
  14. package/lib/presentation.js +309 -0
  15. package/lib/report-diff-tool.js +7 -0
  16. package/lib/report-view.js +62 -10
  17. package/lib/routing.js +85 -0
  18. package/lib/scan.js +547 -0
  19. package/lib/structure-tool.js +35 -1
  20. package/lib/structure.js +28 -3
  21. package/lib/tool.js +7 -0
  22. package/lib/types/automaton-tool.d.ts +2 -0
  23. package/lib/types/automaton.d.ts +57 -0
  24. package/lib/types/edit-tool.d.ts +2 -0
  25. package/lib/types/editor.d.ts +46 -0
  26. package/lib/types/engine.d.ts +1 -0
  27. package/lib/types/presentation.d.ts +164 -0
  28. package/lib/types/routing.d.ts +31 -0
  29. package/lib/types/scan.d.ts +37 -0
  30. package/lib/types/structure.d.ts +16 -0
  31. package/lib/uml-tool.js +21 -6
  32. package/package.json +6 -3
  33. package/skills/model-everything/SKILL.md +6 -2
  34. package/skills/model-everything/references/automata.md +102 -0
  35. package/src/automaton-tool.ts +74 -0
  36. package/src/automaton.ts +376 -0
  37. package/src/compose-tool.ts +13 -5
  38. package/src/concurrency-tool.ts +8 -0
  39. package/src/data-tool.ts +8 -0
  40. package/src/design-tool.ts +76 -68
  41. package/src/edit-tool.ts +121 -0
  42. package/src/editor.ts +297 -0
  43. package/src/engine.ts +7 -0
  44. package/src/export-tool.ts +90 -81
  45. package/src/index.ts +4 -0
  46. package/src/presentation.ts +365 -0
  47. package/src/report-diff-tool.ts +8 -0
  48. package/src/report-view.ts +53 -2
  49. package/src/routing.ts +96 -0
  50. package/src/scan.ts +564 -0
  51. package/src/structure-tool.ts +34 -1
  52. package/src/structure.ts +37 -3
  53. package/src/tool.ts +7 -0
  54. package/src/uml-tool.ts +245 -230
package/src/structure.ts CHANGED
@@ -107,6 +107,10 @@ export interface StructureSummary {
107
107
  errors: number
108
108
  warnings: number
109
109
  rules: number
110
+ /** The most depended-on nodes, by incoming edges. What a reader asks first and counts worst. */
111
+ hubs: { id: string; dependents: number }[]
112
+ /** Modules nothing depends on (no outgoing edges), capped for the report. */
113
+ leaves: string[]
110
114
  }
111
115
 
112
116
  export interface StructureReport {
@@ -503,6 +507,10 @@ export function shortestCycle(nodes: string[], edges: StructureEdge[]): string[]
503
507
  export interface StructureReviewOptions {
504
508
  /** Diagram text to review. */
505
509
  diagram: string
510
+ /** A graph built from source, instead of parsed from a diagram. `diagram` is then ignored. */
511
+ graph?: StructureGraph
512
+ /** How the graph was produced, when it came from a scan rather than a drawing. */
513
+ scan?: { files: number; unresolved: number; packages: number; languages: string[] }
506
514
  notation?: 'auto' | StructureNotation
507
515
  /** Dependency matrix: allowed/denied edges, layers, required edges. */
508
516
  matrix?: unknown
@@ -531,8 +539,9 @@ function requireDiagramText(value: unknown): string {
531
539
  export function reviewStructure(options: StructureReviewOptions): StructureReport {
532
540
  // Not `if (options.diagram !== undefined)`: a missing `diagram` reached the parser as `undefined`
533
541
  // and threw a read of `split` on it - the same defect, one guard narrower.
534
- requireDiagramText(options.diagram)
535
- const graph = parseStructure(options.diagram, options.notation ?? 'auto')
542
+ // A graph built from source arrives already parsed; a caller that passes one has no diagram text to
543
+ // require, so the text guard belongs to the other branch.
544
+ const graph = options.graph ?? parseStructure(requireDiagramText(options.diagram), options.notation ?? 'auto')
536
545
  const warnings = [...graph.warnings]
537
546
  const findings: Finding[] = []
538
547
  const nodes = graph.nodes
@@ -730,6 +739,26 @@ export function reviewStructure(options: StructureReviewOptions): StructureRepor
730
739
  })
731
740
  }
732
741
 
742
+ // Degree is what a reader asks first and counts worst: which module everything depends on, and which
743
+ // depends on nothing. Counting it here means the answer is one line in the report rather than a scan of
744
+ // the graph by hand, which is where a correct graph still produced a wrong answer.
745
+ const inDegree = new Map<string, number>()
746
+ const outDegree = new Map<string, number>()
747
+ for (const edge of graph.edges) {
748
+ inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1)
749
+ outDegree.set(edge.from, (outDegree.get(edge.from) ?? 0) + 1)
750
+ }
751
+ const hubs = [...inDegree.entries()]
752
+ .filter(([, count]) => count > 0)
753
+ .sort((a, b) => (b[1] === a[1] ? a[0].localeCompare(b[0]) : b[1] - a[1]))
754
+ .slice(0, 5)
755
+ .map(([id, dependents]) => ({ id, dependents }))
756
+ const leaves = nodes
757
+ .filter((node) => !node.container && (outDegree.get(node.id) ?? 0) === 0)
758
+ .map((node) => node.id)
759
+ .sort()
760
+ .slice(0, 10)
761
+
733
762
  const errors = findings.filter((finding) => finding.severity === 'error').length
734
763
  const warningCount = findings.filter((finding) => finding.severity === 'warning').length
735
764
  const summary: StructureSummary = {
@@ -745,6 +774,8 @@ export function reviewStructure(options: StructureReviewOptions): StructureRepor
745
774
  errors,
746
775
  warnings: warningCount,
747
776
  rules: rules.length,
777
+ hubs,
778
+ leaves,
748
779
  }
749
780
 
750
781
  const nextSteps: string[] = []
@@ -761,10 +792,13 @@ export function reviewStructure(options: StructureReviewOptions): StructureRepor
761
792
  notation: graph.notation,
762
793
  graph,
763
794
  ...(options.matrix === undefined ? {} : { edgeVerdicts }),
795
+ ...(options.scan === undefined ? {} : { scan: options.scan }),
764
796
  findings,
765
797
  summary,
766
798
  hashes: {
767
- diagram: createHash('sha256').update(options.diagram).digest('hex'),
799
+ // A graph built from source has no diagram text to hash; hashing the text would hash the empty
800
+ // string, and every scan would then carry the same input hash.
801
+ diagram: createHash('sha256').update(options.graph === undefined ? options.diagram : canonicalJson({ nodes: options.graph.nodes.map((node) => node.id), edges: options.graph.edges.map((edge) => ({ from: edge.from, to: edge.to })) })).digest('hex'),
768
802
  ...(options.matrix === undefined ? {} : { matrix: createHash('sha256').update(canonicalJson(options.matrix)).digest('hex') }),
769
803
  },
770
804
  warnings,
package/src/tool.ts CHANGED
@@ -2,6 +2,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
2
2
  import type { JsonValue } from './json-value.js'
3
3
  import { runVerification } from './engine.js'
4
4
  import { renderReportText } from './report-view.js'
5
+ import { reportCallView, reportCard, reportResultView } from './presentation.js'
5
6
  import { OptionShapeError, optionRefusal, requireObjectOption } from './option-shape.js'
6
7
  import { oneOf } from './model-file.js'
7
8
 
@@ -63,7 +64,13 @@ export const modelVerifyTool = defineTool({
63
64
  render(args, value) {
64
65
  return [{ type: 'text' as const, text: renderReportText(value, { full: args.reportView === 'full', check: args.check }) }]
65
66
  },
67
+ // The card a UI shows, separated from the text the model reads.
68
+ presentationMeta(args, value) {
69
+ return reportCard(value, MODEL_VERIFY_TOOL_NAME) as unknown as JsonValue
70
+ },
66
71
  },
72
+ presentCall: reportCallView(MODEL_VERIFY_TOOL_NAME),
73
+ presentResult: reportResultView(MODEL_VERIFY_TOOL_NAME),
67
74
  timeoutMs: 10_000,
68
75
  isConcurrencySafe: () => true,
69
76
  async execute(args) {
package/src/uml-tool.ts CHANGED
@@ -1,230 +1,245 @@
1
- import { defineTool } from '@deepseek-ai/dsh-tools'
2
- import { createHash } from 'node:crypto'
3
- import type { JsonValue } from './json-value.js'
4
- import { REPORT_SCHEMAS, DEFAULT_HASH_SPEC, valueHash, refusalVerdict, verdictOf, verdictOfFindings, type VerdictSummary } from './engine.js'
5
- import {
6
- renderUml, parseUml, parseFindings, reviewUml, explainLabels, checkUmlIntent, UmlError, UML_DIAGRAMS,
7
- type UmlDiagram, type UmlFinding, type UmlIntent, type UmlNotation, type UmlParseResult,
8
- } from './uml.js'
9
- import { renderReportText } from './report-view.js'
10
-
11
- /**
12
- * The UML front end as four tools, one per job.
13
- *
14
- * The split follows the jobs a caller actually has: draw a diagram from a model, read a
15
- * diagram into a model, audit a diagram or a model, and read the label contract the parser
16
- * accepts. Bundling them behind one action parameter made every caller read four jobs to
17
- * find one, and made the tool description a summary of all four.
18
- *
19
- * The family list is not written here. It is read from the engine (`UML_DIAGRAMS`), so adding
20
- * a UML diagram type is a change in the engine and its family module and touches no tool:
21
- * the schema, the enum and every description follow from the table.
22
- */
23
-
24
- /** Every family the front end knows, in the engine's order. A new family is one edit there. */
25
- const FAMILIES = [...UML_DIAGRAMS]
26
-
27
- export const UML_RENDER_TOOL_NAME = 'uml_render'
28
- export const UML_PARSE_TOOL_NAME = 'uml_parse'
29
- export const UML_REVIEW_TOOL_NAME = 'uml_review'
30
- export const UML_LABELS_TOOL_NAME = 'uml_explain_labels'
31
-
32
- const reportViewParameter = {
33
- type: 'string',
34
- enum: ['digest', 'full'],
35
- description: 'digest (default) prints the diagram or the report body; full prints the structured result as JSON.',
36
- } as const
37
-
38
- const notationParameter = {
39
- type: 'string',
40
- enum: ['auto', 'mermaid', 'plantuml'],
41
- description: 'Diagram language. Default auto: detected from the text where there is text, mermaid where a model is drawn.',
42
- } as const
43
-
44
- const kindParameter = {
45
- type: 'string',
46
- enum: FAMILIES,
47
- description: 'UML diagram family. Default state. A family the chosen notation cannot express is refused with the reason, never approximated silently.',
48
- } as const
49
-
50
- const intentParameter = {
51
- type: 'string',
52
- enum: ['present', 'verify', 'fidelity'],
53
- description: 'What the artifact is for, declared up front: present = a human will read it; verify = it must be readable as a model by the verification engine; fidelity = the render-and-reparse proof is the point. A view that cannot serve the declared intent is refused before any work, with errorCode UML_INTENT_UNSERVABLE, UML_VIEW_UNREPRESENTABLE or UML_INTENT_MISMATCH. Omit it and nothing changes.',
54
- } as const
55
-
56
- const reportOutput = {
57
- schema: { type: 'json', description: 'The result: diagram source, a family model, or a modelling review report with findings, round-trip fidelity and next steps.' },
58
- render(args: Record<string, unknown>, value: unknown) {
59
- // A rendered diagram is source text: the body prints it verbatim, once.
60
- return [{ type: 'text' as const, text: renderReportText(value, { full: args.reportView === 'full' }) }]
61
- },
62
- } as const
63
-
64
- /** Draw a model as diagram source. */
65
- export const umlRenderTool = defineTool({
66
- name: UML_RENDER_TOOL_NAME,
67
- description:
68
- 'Turn a model into UML diagram source a human can read, in the notation that can draw that family. It does not judge the model: `uml_review` audits the modelling, `uml_parse` reads a diagram someone else drew. Takes a LogicModelV1 state machine, or the model of another family (use case, class, object, package, component, deployment), and returns Mermaid or PlantUML text that draws it. Use it to turn a verified machine into a diagram a reader can check, or to produce a diagram someone can edit. It never invents structure the model does not have: a construct the notation cannot carry becomes a warning. PlantUML activity is refused instead of approximated, and a family the notation has no view for is refused with the reason. Declare `intent` to have a view that cannot serve the declared purpose refused before any work.',
69
- parameters: {
70
- model: { type: 'json', required: true, description: 'The model to draw: a LogicModelV1 (state machine) or a family model such as a use case or structure model.' },
71
- notation: notationParameter,
72
- kind: kindParameter,
73
- maxSteps: { type: 'integer', description: 'Cap on the sequence trace length when rendering kind=sequence. Default 60.' },
74
- intent: intentParameter,
75
- reportView: reportViewParameter,
76
- },
77
- output: reportOutput,
78
- timeoutMs: 10_000,
79
- isConcurrencySafe: () => true,
80
- async execute(args) {
81
- try {
82
- if (args.model === undefined) return errorResult('uml_render needs a model: a LogicModelV1 or a family model object')
83
- const notation = (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation) as UmlNotation
84
- const result = renderUml(args.model, notation, (args.kind ?? 'state') as UmlDiagram, args.maxSteps as number | undefined, args.intent as UmlIntent | undefined)
85
- return {
86
- ok: true,
87
- ran: true,
88
- ...verdictOf(0, result.warnings.length),
89
- schema: REPORT_SCHEMAS.umlRender,
90
- action: 'render',
91
- notation: result.notation,
92
- kind: result.diagram,
93
- diagram: result.primary,
94
- hashes: { diagram: createHash('sha256').update(result.primary).digest('hex') },
95
- warnings: result.warnings,
96
- } as unknown as JsonValue
97
- } catch (error) {
98
- return refusal(error)
99
- }
100
- },
101
- })
102
-
103
- /** Read diagram text into the model of the family it declares. */
104
- export const umlParseTool = defineTool({
105
- name: UML_PARSE_TOOL_NAME,
106
- description:
107
- 'Read UML diagram text into the model of the family it declares, so a hand-drawn diagram can be checked or diffed. It only reads what the notation states: `uml_review` judges the modelling, and a family the notation cannot carry is refused by name. Mermaid and PlantUML are both accepted, and the family is detected from the text: a state machine or activity diagram becomes a LogicModelV1 the verification engine can run, while a use case, class, object, package, component or deployment diagram becomes that family model. Read the verdict, not ok: a diagram whose family has no module in this build is refused with errorCode UML_NOT_A_STATE_DIAGRAM, and the refusal names the family it found. A sequence diagram is refused as well, because a trace cannot reconstruct a machine, and so is text that is not a UML diagram at all.',
108
- parameters: {
109
- diagram: { type: 'string', required: true, description: 'The UML source text to read.' },
110
- notation: notationParameter,
111
- intent: intentParameter,
112
- reportView: reportViewParameter,
113
- },
114
- output: reportOutput,
115
- timeoutMs: 10_000,
116
- isConcurrencySafe: () => true,
117
- async execute(args) {
118
- try {
119
- if (args.diagram === undefined) return errorResult('uml_parse needs diagram text (Mermaid or PlantUML)')
120
- checkUmlIntent(args.intent as UmlIntent | undefined, 'parse')
121
- const result = parseUml(args.diagram, (args.notation ?? 'auto') as UmlNotation | 'auto')
122
- // The parser notes are the parse verdict: an ignored line is information the model does not carry, so
123
- // ok: true only ever means the parser ran. They stay in `warnings` - the channel the schema reference names and
124
- // the render side uses - and the body prints them, which is where the old contradiction lived: the verdict
125
- // counted them while the roster said `FINDINGS none`.
126
- const findings = parseFindings(result)
127
- return {
128
- ok: true,
129
- ran: true,
130
- ...parseVerdict(result, findings),
131
- schema: REPORT_SCHEMAS.umlParse,
132
- action: 'parse',
133
- notation: result.notation,
134
- kind: result.diagram,
135
- family: result.family,
136
- model: result.model,
137
- labels: result.labels,
138
- findings,
139
- discardedConstructs: result.discardedConstructs,
140
- discardedEdges: result.discardedEdges,
141
- hashes: { hashSpec: DEFAULT_HASH_SPEC, modelHash: valueHash(result.model), diagram: createHash('sha256').update(args.diagram).digest('hex') },
142
- warnings: result.warnings,
143
- } as unknown as JsonValue
144
- } catch (error) {
145
- return refusal(error)
146
- }
147
- },
148
- })
149
-
150
- /** Audit the modelling: the diagram, the model, or the two against each other. */
151
- export const umlReviewTool = defineTool({
152
- name: UML_REVIEW_TOOL_NAME,
153
- description:
154
- 'Audit UML modelling, and report what a reader would get wrong. Give it a model (is this machine well modelled?), a diagram (what does this diagram actually say?), or both (does the diagram carry the model it is presented with?). It reports structural defects such as unreachable states, dead ends, ambiguous or non-exhaustive branches, isolated nodes and cycles, plus documentation gaps and the fidelity check that re-renders the model and reads it back. Read the verdict, not ok. A family diagram gets its own family checks: use case defects, or the dependency-graph checks for a structure diagram. This audits the modelling; behaviour needs model_verify.',
155
- parameters: {
156
- model: { type: 'json', description: 'The model to review: a LogicModelV1 or a family model. Provide it, the diagram, or both.' },
157
- diagram: { type: 'string', description: 'UML source text to review. Provide it, the model, or both.' },
158
- notation: notationParameter,
159
- kind: kindParameter,
160
- roundTrip: { type: 'boolean', description: 'Render and re-read the model to prove the diagram carries it. Default true.' },
161
- maxSteps: { type: 'integer', description: 'Cap on the sequence trace used for the fidelity check.' },
162
- intent: intentParameter,
163
- reportView: reportViewParameter,
164
- },
165
- output: reportOutput,
166
- timeoutMs: 10_000,
167
- isConcurrencySafe: () => true,
168
- async execute(args) {
169
- try {
170
- if (args.model === undefined && args.diagram === undefined) return errorResult('uml_review needs a model, diagram text, or both')
171
- const report = reviewUml({
172
- model: args.model,
173
- diagram: args.diagram,
174
- notation: (args.notation ?? 'auto') as UmlNotation | 'auto',
175
- diagramKind: (args.kind ?? 'state') as UmlDiagram,
176
- roundTrip: args.roundTrip as boolean | undefined,
177
- maxSteps: args.maxSteps as number | undefined,
178
- intent: args.intent as UmlIntent | undefined,
179
- })
180
- return report as unknown as JsonValue
181
- } catch (error) {
182
- return refusal(error)
183
- }
184
- },
185
- })
186
-
187
- /** Print the label and comment contract the parser accepts. */
188
- export const umlExplainLabelsTool = defineTool({
189
- name: UML_LABELS_TOOL_NAME,
190
- description:
191
- 'Print the label and comment conventions the UML parser accepts, for one notation: the accepted label spellings, the form the renderer writes, the directives that pin what the notation cannot spell, and which lines are ignored. No diagram is needed. Use it when a hand-written diagram reports label drift or ignored lines and the reason is not obvious from the source. It reads no diagram and judges no modelling: `uml_parse` reads a diagram, and `uml_review` audits one.',
192
- parameters: {
193
- notation: notationParameter,
194
- reportView: reportViewParameter,
195
- },
196
- output: reportOutput,
197
- timeoutMs: 10_000,
198
- isConcurrencySafe: () => true,
199
- async execute(args) {
200
- const notation = (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation) as UmlNotation
201
- return {
202
- ok: true,
203
- ran: true,
204
- ...verdictOf(0, 0),
205
- schema: REPORT_SCHEMAS.umlExplainLabels,
206
- action: 'explain-labels',
207
- ...explainLabels(notation),
208
- } as unknown as JsonValue
209
- },
210
- })
211
-
212
- /** The parse verdict counts parser notes as warnings: an ignored line is information the model does not carry. */
213
- function parseVerdict(result: UmlParseResult, findings: UmlFinding[]): VerdictSummary {
214
- const errors = findings.filter((finding) => finding.severity === 'error').length
215
- const warnings = findings.filter((finding) => finding.severity === 'warning').length + result.warnings.length
216
- const firstCode = findings.find((finding) => finding.severity === 'error')?.code ?? findings.find((finding) => finding.severity === 'warning')?.code
217
- return verdictOf(errors, warnings, firstCode)
218
- }
219
-
220
- /** A front-end refusal travels as a failed result, with the code that says why. */
221
- function refusal(error: unknown): JsonValue {
222
- const message = error instanceof Error ? error.message : String(error)
223
- return errorResult(message, error instanceof UmlError ? error.code : undefined)
224
- }
225
-
226
- function errorResult(message: string, errorCode = 'UML_INPUT'): JsonValue {
227
- // Carries the refusal schema. Every other family's refusal carries one, and a reader who decides "what am I
228
- // holding" from `schema` got nothing from this one - it was the only refusal shaped differently.
229
- return { ok: false, ran: false, ...refusalVerdict(message), schema: REPORT_SCHEMAS.umlRefusal, errorCode, error: message } as unknown as JsonValue
230
- }
1
+ import { defineTool } from '@deepseek-ai/dsh-tools'
2
+ import { createHash } from 'node:crypto'
3
+ import type { JsonValue } from './json-value.js'
4
+ import { REPORT_SCHEMAS, DEFAULT_HASH_SPEC, valueHash, refusalVerdict, verdictOf, verdictOfFindings, type VerdictSummary } from './engine.js'
5
+ import {
6
+ renderUml, parseUml, parseFindings, reviewUml, explainLabels, checkUmlIntent, UmlError, UML_DIAGRAMS,
7
+ type UmlDiagram, type UmlFinding, type UmlIntent, type UmlNotation, type UmlParseResult,
8
+ } from './uml.js'
9
+ import { renderReportText } from './report-view.js'
10
+ import { cardWithSource, reportCallView, reportCard, reportResultView } from './presentation.js'
11
+
12
+ /**
13
+ * The UML front end as four tools, one per job.
14
+ *
15
+ * The split follows the jobs a caller actually has: draw a diagram from a model, read a
16
+ * diagram into a model, audit a diagram or a model, and read the label contract the parser
17
+ * accepts. Bundling them behind one action parameter made every caller read four jobs to
18
+ * find one, and made the tool description a summary of all four.
19
+ *
20
+ * The family list is not written here. It is read from the engine (`UML_DIAGRAMS`), so adding
21
+ * a UML diagram type is a change in the engine and its family module and touches no tool:
22
+ * the schema, the enum and every description follow from the table.
23
+ */
24
+
25
+ /** Every family the front end knows, in the engine's order. A new family is one edit there. */
26
+ const FAMILIES = [...UML_DIAGRAMS]
27
+
28
+ export const UML_RENDER_TOOL_NAME = 'uml_render'
29
+ export const UML_PARSE_TOOL_NAME = 'uml_parse'
30
+ export const UML_REVIEW_TOOL_NAME = 'uml_review'
31
+ export const UML_LABELS_TOOL_NAME = 'uml_explain_labels'
32
+
33
+ const reportViewParameter = {
34
+ type: 'string',
35
+ enum: ['digest', 'full'],
36
+ description: 'digest (default) prints the diagram or the report body; full prints the structured result as JSON.',
37
+ } as const
38
+
39
+ const notationParameter = {
40
+ type: 'string',
41
+ enum: ['auto', 'mermaid', 'plantuml'],
42
+ description: 'Diagram language. Default auto: detected from the text where there is text, mermaid where a model is drawn.',
43
+ } as const
44
+
45
+ const kindParameter = {
46
+ type: 'string',
47
+ enum: FAMILIES,
48
+ description: 'UML diagram family. Default state. A family the chosen notation cannot express is refused with the reason, never approximated silently.',
49
+ } as const
50
+
51
+ const intentParameter = {
52
+ type: 'string',
53
+ enum: ['present', 'verify', 'fidelity'],
54
+ description: 'What the artifact is for, declared up front: present = a human will read it; verify = it must be readable as a model by the verification engine; fidelity = the render-and-reparse proof is the point. A view that cannot serve the declared intent is refused before any work, with errorCode UML_INTENT_UNSERVABLE, UML_VIEW_UNREPRESENTABLE or UML_INTENT_MISMATCH. Omit it and nothing changes.',
55
+ } as const
56
+
57
+ const reportOutput = (family: string) => ({
58
+ schema: { type: 'json' as const, description: 'The result: diagram source, a family model, or a modelling review report with findings, round-trip fidelity and next steps.' },
59
+ render(args: Record<string, unknown>, value: unknown) {
60
+ // A rendered diagram is source text: the body prints it verbatim, once.
61
+ return [{ type: 'text' as const, text: renderReportText(value, { full: args.reportView === 'full' }) }]
62
+ },
63
+ // The card a UI shows, separated from the text the model reads. The family is the tool's own name, so
64
+ // one shared output does not have to guess which tool is calling it.
65
+ presentationMeta(_args: unknown, value: unknown) {
66
+ const report = value as Record<string, unknown>
67
+ return cardWithSource(value, family, typeof report.diagram === 'string' ? report.diagram : undefined, 'diagram source') as unknown as JsonValue
68
+ },
69
+ })
70
+
71
+ /** Draw a model as diagram source. */
72
+ export const umlRenderTool = defineTool({
73
+ name: UML_RENDER_TOOL_NAME,
74
+ description:
75
+ 'Turn a model into UML diagram source a human can read, in the notation that can draw that family. It does not judge the model: `uml_review` audits the modelling, `uml_parse` reads a diagram someone else drew. Takes a LogicModelV1 state machine, or the model of another family (use case, class, object, package, component, deployment), and returns Mermaid or PlantUML text that draws it. Use it to turn a verified machine into a diagram a reader can check, or to produce a diagram someone can edit. It never invents structure the model does not have: a construct the notation cannot carry becomes a warning. PlantUML activity is refused instead of approximated, and a family the notation has no view for is refused with the reason. Declare `intent` to have a view that cannot serve the declared purpose refused before any work.',
76
+ parameters: {
77
+ model: { type: 'json', required: true, description: 'The model to draw: a LogicModelV1 (state machine) or a family model such as a use case or structure model.' },
78
+ notation: notationParameter,
79
+ kind: kindParameter,
80
+ maxSteps: { type: 'integer', description: 'Cap on the sequence trace length when rendering kind=sequence. Default 60.' },
81
+ intent: intentParameter,
82
+ reportView: reportViewParameter,
83
+ },
84
+ output: reportOutput(UML_RENDER_TOOL_NAME),
85
+ presentCall: reportCallView(UML_RENDER_TOOL_NAME),
86
+ presentResult: reportResultView(UML_RENDER_TOOL_NAME),
87
+ timeoutMs: 10_000,
88
+ isConcurrencySafe: () => true,
89
+ async execute(args) {
90
+ try {
91
+ if (args.model === undefined) return errorResult('uml_render needs a model: a LogicModelV1 or a family model object')
92
+ const notation = (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation) as UmlNotation
93
+ const result = renderUml(args.model, notation, (args.kind ?? 'state') as UmlDiagram, args.maxSteps as number | undefined, args.intent as UmlIntent | undefined)
94
+ return {
95
+ ok: true,
96
+ ran: true,
97
+ ...verdictOf(0, result.warnings.length),
98
+ schema: REPORT_SCHEMAS.umlRender,
99
+ action: 'render',
100
+ notation: result.notation,
101
+ kind: result.diagram,
102
+ diagram: result.primary,
103
+ hashes: { diagram: createHash('sha256').update(result.primary).digest('hex') },
104
+ warnings: result.warnings,
105
+ } as unknown as JsonValue
106
+ } catch (error) {
107
+ return refusal(error)
108
+ }
109
+ },
110
+ })
111
+
112
+ /** Read diagram text into the model of the family it declares. */
113
+ export const umlParseTool = defineTool({
114
+ name: UML_PARSE_TOOL_NAME,
115
+ description:
116
+ 'Read UML diagram text into the model of the family it declares, so a hand-drawn diagram can be checked or diffed. It only reads what the notation states: `uml_review` judges the modelling, and a family the notation cannot carry is refused by name. Mermaid and PlantUML are both accepted, and the family is detected from the text: a state machine or activity diagram becomes a LogicModelV1 the verification engine can run, while a use case, class, object, package, component or deployment diagram becomes that family model. Read the verdict, not ok: a diagram whose family has no module in this build is refused with errorCode UML_NOT_A_STATE_DIAGRAM, and the refusal names the family it found. A sequence diagram is refused as well, because a trace cannot reconstruct a machine, and so is text that is not a UML diagram at all.',
117
+ parameters: {
118
+ diagram: { type: 'string', required: true, description: 'The UML source text to read.' },
119
+ notation: notationParameter,
120
+ intent: intentParameter,
121
+ reportView: reportViewParameter,
122
+ },
123
+ output: reportOutput(UML_PARSE_TOOL_NAME),
124
+ presentCall: reportCallView(UML_PARSE_TOOL_NAME),
125
+ presentResult: reportResultView(UML_PARSE_TOOL_NAME),
126
+ timeoutMs: 10_000,
127
+ isConcurrencySafe: () => true,
128
+ async execute(args) {
129
+ try {
130
+ if (args.diagram === undefined) return errorResult('uml_parse needs diagram text (Mermaid or PlantUML)')
131
+ checkUmlIntent(args.intent as UmlIntent | undefined, 'parse')
132
+ const result = parseUml(args.diagram, (args.notation ?? 'auto') as UmlNotation | 'auto')
133
+ // The parser notes are the parse verdict: an ignored line is information the model does not carry, so
134
+ // ok: true only ever means the parser ran. They stay in `warnings` - the channel the schema reference names and
135
+ // the render side uses - and the body prints them, which is where the old contradiction lived: the verdict
136
+ // counted them while the roster said `FINDINGS none`.
137
+ const findings = parseFindings(result)
138
+ return {
139
+ ok: true,
140
+ ran: true,
141
+ ...parseVerdict(result, findings),
142
+ schema: REPORT_SCHEMAS.umlParse,
143
+ action: 'parse',
144
+ notation: result.notation,
145
+ kind: result.diagram,
146
+ family: result.family,
147
+ model: result.model,
148
+ labels: result.labels,
149
+ findings,
150
+ discardedConstructs: result.discardedConstructs,
151
+ discardedEdges: result.discardedEdges,
152
+ hashes: { hashSpec: DEFAULT_HASH_SPEC, modelHash: valueHash(result.model), diagram: createHash('sha256').update(args.diagram).digest('hex') },
153
+ warnings: result.warnings,
154
+ } as unknown as JsonValue
155
+ } catch (error) {
156
+ return refusal(error)
157
+ }
158
+ },
159
+ })
160
+
161
+ /** Audit the modelling: the diagram, the model, or the two against each other. */
162
+ export const umlReviewTool = defineTool({
163
+ name: UML_REVIEW_TOOL_NAME,
164
+ description:
165
+ 'Audit UML modelling, and report what a reader would get wrong. Give it a model (is this machine well modelled?), a diagram (what does this diagram actually say?), or both (does the diagram carry the model it is presented with?). It reports structural defects such as unreachable states, dead ends, ambiguous or non-exhaustive branches, isolated nodes and cycles, plus documentation gaps and the fidelity check that re-renders the model and reads it back. Read the verdict, not ok. A family diagram gets its own family checks: use case defects, or the dependency-graph checks for a structure diagram. This audits the modelling; behaviour needs model_verify.',
166
+ parameters: {
167
+ model: { type: 'json', description: 'The model to review: a LogicModelV1 or a family model. Provide it, the diagram, or both.' },
168
+ diagram: { type: 'string', description: 'UML source text to review. Provide it, the model, or both.' },
169
+ notation: notationParameter,
170
+ kind: kindParameter,
171
+ roundTrip: { type: 'boolean', description: 'Render and re-read the model to prove the diagram carries it. Default true.' },
172
+ maxSteps: { type: 'integer', description: 'Cap on the sequence trace used for the fidelity check.' },
173
+ intent: intentParameter,
174
+ reportView: reportViewParameter,
175
+ },
176
+ output: reportOutput(UML_REVIEW_TOOL_NAME),
177
+ presentCall: reportCallView(UML_REVIEW_TOOL_NAME),
178
+ presentResult: reportResultView(UML_REVIEW_TOOL_NAME),
179
+ timeoutMs: 10_000,
180
+ isConcurrencySafe: () => true,
181
+ async execute(args) {
182
+ try {
183
+ if (args.model === undefined && args.diagram === undefined) return errorResult('uml_review needs a model, diagram text, or both')
184
+ const report = reviewUml({
185
+ model: args.model,
186
+ diagram: args.diagram,
187
+ notation: (args.notation ?? 'auto') as UmlNotation | 'auto',
188
+ diagramKind: (args.kind ?? 'state') as UmlDiagram,
189
+ roundTrip: args.roundTrip as boolean | undefined,
190
+ maxSteps: args.maxSteps as number | undefined,
191
+ intent: args.intent as UmlIntent | undefined,
192
+ })
193
+ return report as unknown as JsonValue
194
+ } catch (error) {
195
+ return refusal(error)
196
+ }
197
+ },
198
+ })
199
+
200
+ /** Print the label and comment contract the parser accepts. */
201
+ export const umlExplainLabelsTool = defineTool({
202
+ name: UML_LABELS_TOOL_NAME,
203
+ description:
204
+ 'Print the label and comment conventions the UML parser accepts, for one notation: the accepted label spellings, the form the renderer writes, the directives that pin what the notation cannot spell, and which lines are ignored. No diagram is needed. Use it when a hand-written diagram reports label drift or ignored lines and the reason is not obvious from the source. It reads no diagram and judges no modelling: `uml_parse` reads a diagram, and `uml_review` audits one.',
205
+ parameters: {
206
+ notation: notationParameter,
207
+ reportView: reportViewParameter,
208
+ },
209
+ output: reportOutput(UML_LABELS_TOOL_NAME),
210
+ presentCall: reportCallView(UML_LABELS_TOOL_NAME),
211
+ presentResult: reportResultView(UML_LABELS_TOOL_NAME),
212
+ timeoutMs: 10_000,
213
+ isConcurrencySafe: () => true,
214
+ async execute(args) {
215
+ const notation = (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation) as UmlNotation
216
+ return {
217
+ ok: true,
218
+ ran: true,
219
+ ...verdictOf(0, 0),
220
+ schema: REPORT_SCHEMAS.umlExplainLabels,
221
+ action: 'explain-labels',
222
+ ...explainLabels(notation),
223
+ } as unknown as JsonValue
224
+ },
225
+ })
226
+
227
+ /** The parse verdict counts parser notes as warnings: an ignored line is information the model does not carry. */
228
+ function parseVerdict(result: UmlParseResult, findings: UmlFinding[]): VerdictSummary {
229
+ const errors = findings.filter((finding) => finding.severity === 'error').length
230
+ const warnings = findings.filter((finding) => finding.severity === 'warning').length + result.warnings.length
231
+ const firstCode = findings.find((finding) => finding.severity === 'error')?.code ?? findings.find((finding) => finding.severity === 'warning')?.code
232
+ return verdictOf(errors, warnings, firstCode)
233
+ }
234
+
235
+ /** A front-end refusal travels as a failed result, with the code that says why. */
236
+ function refusal(error: unknown): JsonValue {
237
+ const message = error instanceof Error ? error.message : String(error)
238
+ return errorResult(message, error instanceof UmlError ? error.code : undefined)
239
+ }
240
+
241
+ function errorResult(message: string, errorCode = 'UML_INPUT'): JsonValue {
242
+ // Carries the refusal schema. Every other family's refusal carries one, and a reader who decides "what am I
243
+ // holding" from `schema` got nothing from this one - it was the only refusal shaped differently.
244
+ return { ok: false, ran: false, ...refusalVerdict(message), schema: REPORT_SCHEMAS.umlRefusal, errorCode, error: message } as unknown as JsonValue
245
+ }