dsh-model-everything 0.4.1 → 0.5.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.
Files changed (114) hide show
  1. package/README.en-US.md +75 -19
  2. package/README.md +377 -345
  3. package/cordis.patch.yml +9 -0
  4. package/diagram/package.json +5 -0
  5. package/lib/client.js +3494 -0
  6. package/lib/diagram-diff-tool.js +90 -0
  7. package/lib/diagram-render-tool.js +102 -0
  8. package/lib/diagram-review-tool.js +99 -0
  9. package/lib/diagram.js +43 -0
  10. package/lib/gate.js +14 -12
  11. package/lib/presentation.js +10 -0
  12. package/lib/render/budget.js +99 -0
  13. package/lib/render/channels.js +243 -0
  14. package/lib/render/collapse.js +155 -0
  15. package/lib/render/components.js +99 -0
  16. package/lib/render/coords.js +288 -0
  17. package/lib/render/cycle-break.js +147 -0
  18. package/lib/render/diff.js +138 -0
  19. package/lib/render/dummy.js +43 -0
  20. package/lib/render/from-mermaid.js +235 -0
  21. package/lib/render/from-plantuml.js +430 -0
  22. package/lib/render/index.js +42 -0
  23. package/lib/render/layer.js +91 -0
  24. package/lib/render/layout.js +68 -0
  25. package/lib/render/model.js +17 -0
  26. package/lib/render/network-simplex.js +272 -0
  27. package/lib/render/order.js +122 -0
  28. package/lib/render/paint.js +891 -0
  29. package/lib/render/palette.js +127 -0
  30. package/lib/render/place.js +482 -0
  31. package/lib/render/route.js +225 -0
  32. package/lib/render/svg.js +229 -0
  33. package/lib/render/symbol.js +108 -0
  34. package/lib/render/text.js +74 -0
  35. package/lib/render/viewmodel.js +126 -0
  36. package/lib/render/wrap.js +87 -0
  37. package/lib/render/write.js +14 -0
  38. package/lib/scan.js +248 -8
  39. package/lib/structure-tool.js +2 -2
  40. package/lib/types/client.d.ts +83 -0
  41. package/lib/types/diagram-diff-tool.d.ts +8 -0
  42. package/lib/types/diagram-render-tool.d.ts +8 -0
  43. package/lib/types/diagram-review-tool.d.ts +8 -0
  44. package/lib/types/diagram.d.ts +34 -0
  45. package/lib/types/presentation.d.ts +7 -0
  46. package/lib/types/render/budget.d.ts +55 -0
  47. package/lib/types/render/channels.d.ts +63 -0
  48. package/lib/types/render/collapse.d.ts +23 -0
  49. package/lib/types/render/components.d.ts +57 -0
  50. package/lib/types/render/coords.d.ts +64 -0
  51. package/lib/types/render/cycle-break.d.ts +45 -0
  52. package/lib/types/render/diff.d.ts +44 -0
  53. package/lib/types/render/dummy.d.ts +32 -0
  54. package/lib/types/render/from-mermaid.d.ts +27 -0
  55. package/lib/types/render/from-plantuml.d.ts +33 -0
  56. package/lib/types/render/index.d.ts +38 -0
  57. package/lib/types/render/layer.d.ts +28 -0
  58. package/lib/types/render/layout.d.ts +54 -0
  59. package/lib/types/render/model.d.ts +73 -0
  60. package/lib/types/render/network-simplex.d.ts +54 -0
  61. package/lib/types/render/order.d.ts +27 -0
  62. package/lib/types/render/paint.d.ts +127 -0
  63. package/lib/types/render/palette.d.ts +41 -0
  64. package/lib/types/render/place.d.ts +55 -0
  65. package/lib/types/render/route.d.ts +77 -0
  66. package/lib/types/render/svg.d.ts +19 -0
  67. package/lib/types/render/symbol.d.ts +73 -0
  68. package/lib/types/render/text.d.ts +38 -0
  69. package/lib/types/render/viewmodel.d.ts +20 -0
  70. package/lib/types/render/wrap.d.ts +27 -0
  71. package/lib/types/render/write.d.ts +14 -0
  72. package/lib/types/scan.d.ts +10 -2
  73. package/lib/types/structure.d.ts +1 -0
  74. package/package.json +148 -132
  75. package/skills/model-everything/SKILL.md +454 -454
  76. package/skills/model-everything/references/architecture-domains.md +137 -0
  77. package/skills/model-everything/references/architecture-views.md +119 -0
  78. package/skills/model-everything-diagram/SKILL.md +84 -0
  79. package/src/client.ts +307 -0
  80. package/src/diagram-diff-tool.ts +94 -0
  81. package/src/diagram-render-tool.ts +108 -0
  82. package/src/diagram-review-tool.ts +109 -0
  83. package/src/diagram.ts +61 -0
  84. package/src/gate.ts +14 -12
  85. package/src/presentation.ts +12 -0
  86. package/src/render/budget.ts +135 -0
  87. package/src/render/channels.ts +309 -0
  88. package/src/render/collapse.ts +150 -0
  89. package/src/render/components.ts +126 -0
  90. package/src/render/coords.ts +328 -0
  91. package/src/render/cycle-break.ts +164 -0
  92. package/src/render/diff.ts +162 -0
  93. package/src/render/dummy.ts +65 -0
  94. package/src/render/from-mermaid.ts +261 -0
  95. package/src/render/from-plantuml.ts +469 -0
  96. package/src/render/index.ts +73 -0
  97. package/src/render/layer.ts +98 -0
  98. package/src/render/layout.ts +101 -0
  99. package/src/render/model.ts +86 -0
  100. package/src/render/network-simplex.ts +290 -0
  101. package/src/render/order.ts +139 -0
  102. package/src/render/paint.ts +1006 -0
  103. package/src/render/palette.ts +164 -0
  104. package/src/render/place.ts +527 -0
  105. package/src/render/route.ts +269 -0
  106. package/src/render/svg.ts +270 -0
  107. package/src/render/symbol.ts +137 -0
  108. package/src/render/text.ts +86 -0
  109. package/src/render/viewmodel.ts +136 -0
  110. package/src/render/wrap.ts +103 -0
  111. package/src/render/write.ts +17 -0
  112. package/src/scan.ts +807 -564
  113. package/src/structure-tool.ts +2 -2
  114. package/src/structure.ts +1 -1
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The structural diff tool: two drawings, compared by containment.
3
+ *
4
+ * One job per file: this module declares the tool and nothing else, so the documentation generator that walks
5
+ * `*-tool` modules finds it, and so a change to the drawing never touches the reviewing.
6
+ */
7
+
8
+ import { defineTool } from '@deepseek-ai/dsh-tools'
9
+ import { render, type Notation } from './render/index.js'
10
+ import { diffStructure } from './render/diff.js'
11
+
12
+ export const DIAGRAM_DIFF_TOOL_NAME = 'diagram_diff'
13
+
14
+ /** Parse one side of a comparison, reporting which side failed to produce a drawing. */
15
+ function sideOf(source: string, notation: Notation | 'auto', which: 'before' | 'after'): { diagram?: ReturnType<typeof render>['diagram']; refused: string[]; problem?: string } {
16
+ if (source.trim() === '') return { refused: [], problem: which + ' is empty: pass the diagram text for that side' }
17
+ const rendered = render({ source, notation })
18
+ if (rendered.diagram.nodes.length === 0) {
19
+ return { refused: rendered.refused, problem: which + ' produced no nodes (' + String(rendered.refused.length) + ' line(s) refused)' }
20
+ }
21
+ return { diagram: rendered.diagram, refused: rendered.refused }
22
+ }
23
+
24
+ export const diagramDiffTool = defineTool({
25
+ name: DIAGRAM_DIFF_TOOL_NAME,
26
+ description:
27
+ 'Compare the containment structure of two diagrams: which boundaries are new or gone, which were renamed, which moved inside another, and which nodes changed owner. Accepts the same Mermaid and PlantUML subset as diagram_render. This decides what moved, never whether the move is better - read `verdict`, never `ok`. It does not diff report files (report_diff does) and it does not draw (diagram_render does).',
28
+ parameters: {
29
+ before: {
30
+ type: 'string',
31
+ description: 'The earlier diagram source.',
32
+ },
33
+ after: {
34
+ type: 'string',
35
+ description: 'The later diagram source.',
36
+ },
37
+ notation: {
38
+ type: 'string',
39
+ enum: ['auto', 'mermaid', 'plantuml'],
40
+ description: 'Which notation both sides are written in. Default auto: decided per side from its first meaningful line.',
41
+ },
42
+ },
43
+ output: {
44
+ schema: { type: 'json', description: 'Structural diff: the verdict, every change with the id it is about, and the sizes of both sides.' },
45
+ render(_args, value) {
46
+ const report = value as Record<string, unknown>
47
+ const changes = Array.isArray(report.changes) ? (report.changes as Array<Record<string, unknown>>) : []
48
+ const measured = (report.measured ?? {}) as Record<string, Record<string, number>>
49
+ const lines: string[] = []
50
+ lines.push('dsh-diagram/diff/v1 verdict=' + String(report.verdict) + ' ' + String(changes.length) + ' change(s)')
51
+ lines.push(
52
+ 'MEASURED before=' + String(measured.before?.nodes) + ' node(s)/' + String(measured.before?.groups) + ' group(s)' +
53
+ ' after=' + String(measured.after?.nodes) + ' node(s)/' + String(measured.after?.groups) + ' group(s)',
54
+ )
55
+ for (const change of changes) lines.push(' [' + String(change.severity) + '/' + String(change.code) + '] ' + String(change.message))
56
+ if (changes.length === 0) lines.push('NEXT The structure is unchanged. That is all this compares: node text, edges and guards are not part of it.')
57
+ else lines.push('NEXT Read each change against the design intent: a move is a decision, and this tool only reports that it happened.')
58
+ return [{ type: 'text' as const, text: lines.join('\n') }]
59
+ },
60
+ },
61
+ timeoutMs: 10_000,
62
+ isConcurrencySafe: () => true,
63
+ async execute(args: { before?: unknown; after?: unknown; notation?: unknown }) {
64
+ const notation = args.notation === 'mermaid' || args.notation === 'plantuml' ? (args.notation as Notation) : 'auto'
65
+ const before = sideOf(typeof args.before === 'string' ? args.before : '', notation, 'before')
66
+ const after = sideOf(typeof args.after === 'string' ? args.after : '', notation, 'after')
67
+ const problems = [before.problem, after.problem].filter((problem): problem is string => problem !== undefined)
68
+ if (before.diagram === undefined || after.diagram === undefined) {
69
+ return {
70
+ ok: false,
71
+ verdict: 'fail' as const,
72
+ changes: problems.map((problem) => ({ code: 'D1_SIDE_UNREADABLE', severity: 'error', kind: 'removed', subject: '-', message: problem })),
73
+ measured: {
74
+ before: { nodes: before.diagram?.nodes.length ?? 0, groups: before.diagram?.groups?.length ?? 0 },
75
+ after: { nodes: after.diagram?.nodes.length ?? 0, groups: after.diagram?.groups?.length ?? 0 },
76
+ },
77
+ }
78
+ }
79
+ const report = diffStructure(before.diagram, after.diagram)
80
+ return {
81
+ ok: report.verdict !== 'fail',
82
+ verdict: report.verdict,
83
+ changes: report.changes.map((change) => ({
84
+ code: change.code,
85
+ severity: change.severity,
86
+ kind: change.kind,
87
+ subject: change.subject,
88
+ message: change.message,
89
+ })),
90
+ measured: report.measured,
91
+ }
92
+ },
93
+ })
94
+
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The drawing tool: source text in, SVG out.
3
+ *
4
+ * One job per file: this module declares the tool and nothing else, so the documentation generator that walks
5
+ * `*-tool` modules finds it, and so a change to the drawing never touches the reviewing.
6
+ */
7
+
8
+ import { defineTool } from '@deepseek-ai/dsh-tools'
9
+ import { render, type Notation } from './render/index.js'
10
+ import { schemeNames, type PaletteScheme } from './render/palette.js'
11
+ import { writeSvg } from './render/write.js'
12
+
13
+ export const DIAGRAM_TOOL_NAME = 'diagram_render'
14
+
15
+ export const diagramRenderTool = defineTool({
16
+ name: DIAGRAM_TOOL_NAME,
17
+ description:
18
+ 'Draw a diagram from its source text and get back SVG. Accepts Mermaid (flowchart, stateDiagram) and PlantUML (component, package, state) for the subset this renderer draws, lays out boxes and arrows itself, and reports every line it refused with its line number instead of drawing half a diagram. Pass `out` to write the SVG next to a document. This tool draws; it does not parse a diagram back into a model, and it does not review one - uml_parse and uml_review do that.',
19
+ parameters: {
20
+ source: {
21
+ type: 'string',
22
+ description: 'The diagram source text, in Mermaid or PlantUML syntax.',
23
+ },
24
+ notation: {
25
+ type: 'string',
26
+ enum: ['auto', 'mermaid', 'plantuml'],
27
+ description: 'Which notation `source` is written in. Default auto: read from the first meaningful line.',
28
+ },
29
+ title: {
30
+ type: 'string',
31
+ description: 'The heading drawn above the diagram. Defaults to the component configuration.',
32
+ },
33
+ scheme: {
34
+ type: 'string',
35
+ enum: schemeNames(),
36
+ description: 'Colour scheme. Default: the standard light palette. print is greyscale, for a drawing that will be printed or embedded in a plain document.',
37
+ },
38
+ out: {
39
+ type: 'string',
40
+ description: 'Write the SVG here and report the path. Relative paths resolve against the session workspace.',
41
+ },
42
+ },
43
+ output: {
44
+ schema: { type: 'json', description: 'Drawing result: the notation used, the counts, every refused line, and the SVG markup.' },
45
+ render(_args, value) {
46
+ const result = value as Record<string, unknown>
47
+ const lines: string[] = []
48
+ const refused = Array.isArray(result.refused) ? (result.refused as string[]) : []
49
+ lines.push(
50
+ 'dsh-diagram/v1 notation=' + String(result.notation) + ' nodes=' + String(result.nodes) + ' edges=' + String(result.edges) +
51
+ (Number(result.groups) > 0 ? ' groups=' + String(result.groups) : '') +
52
+ (result.path === undefined ? '' : ' written=' + String(result.path)),
53
+ )
54
+ lines.push('CAPTION ' + String(result.caption))
55
+ if (refused.length > 0) {
56
+ lines.push('')
57
+ lines.push('REFUSED ' + String(refused.length) + ' line(s) - the drawing covers the rest')
58
+ for (const line of refused) lines.push(' ' + line)
59
+ lines.push('NEXT Fix or delete the refused lines, or render them in a tool that supports them. The drawing you got does not show them.')
60
+ } else {
61
+ lines.push('NEXT Every line was drawn. Paste the SVG where the diagram belongs, or pass `out` to write it next to the document.')
62
+ }
63
+ return [{ type: 'text' as const, text: lines.join('\n') }]
64
+ },
65
+ },
66
+ timeoutMs: 10_000,
67
+ isConcurrencySafe: () => true,
68
+ async execute(args: { source?: unknown; notation?: unknown; title?: unknown; out?: unknown; scheme?: unknown }) {
69
+ const source = typeof args.source === 'string' ? args.source : ''
70
+ if (source.trim() === '') {
71
+ // One shape for every answer, so the output contract has nothing to special-case.
72
+ return {
73
+ ok: false,
74
+ notation: 'mermaid',
75
+ nodes: 0,
76
+ edges: 0,
77
+ groups: 0,
78
+ caption: 'no source',
79
+ refused: ['source is empty: pass the diagram text to draw'],
80
+ dropped: 0,
81
+ svg: '',
82
+ }
83
+ }
84
+ const notation = args.notation === 'mermaid' || args.notation === 'plantuml' ? (args.notation as Notation) : 'auto'
85
+ const title = typeof args.title === 'string' && args.title.trim() !== '' ? args.title.trim() : undefined
86
+ // The argument validator has already held this to the shipped schemes; an unknown value falls back rather
87
+ // than throwing, because a stale deployment may still send one.
88
+ const scheme = schemeNames().includes(args.scheme as PaletteScheme) ? (args.scheme as PaletteScheme) : 'default'
89
+ const rendered = render({ source, notation, ...(title === undefined ? {} : { title }) }, {}, { scheme })
90
+ const written =
91
+ typeof args.out === 'string' && args.out.trim() !== ''
92
+ ? writeSvg({ source, notation, ...(title === undefined ? {} : { title }) }, args.out.trim(), {}, { scheme })
93
+ : undefined
94
+
95
+ return {
96
+ ok: rendered.refused.length === 0,
97
+ notation: rendered.notation,
98
+ nodes: rendered.diagram.nodes.length,
99
+ edges: rendered.diagram.edges.length,
100
+ groups: rendered.diagram.groups?.length ?? 0,
101
+ caption: rendered.diagram.caption,
102
+ refused: rendered.refused,
103
+ dropped: rendered.dropped,
104
+ ...(written === undefined ? {} : { path: written.path, bytes: written.bytes }),
105
+ svg: rendered.markup,
106
+ }
107
+ },
108
+ })
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The structural review tool: budgets against one drawing.
3
+ *
4
+ * One job per file: this module declares the tool and nothing else, so the documentation generator that walks
5
+ * `*-tool` modules finds it, and so a change to the drawing never touches the reviewing.
6
+ */
7
+
8
+ import { defineTool } from '@deepseek-ai/dsh-tools'
9
+ import { render, type Notation } from './render/index.js'
10
+ import { checkBudget, DEFAULT_LIMITS, type BudgetLimits } from './render/budget.js'
11
+
12
+ export const DIAGRAM_REVIEW_TOOL_NAME = 'diagram_review'
13
+
14
+ /** Read the three caps a caller may override, ignoring anything that is not a positive number. */
15
+ function limitsOf(args: { maxMembers?: unknown; maxDepth?: unknown; maxNodes?: unknown }): BudgetLimits {
16
+ const pick = (value: unknown, fallback: number): number =>
17
+ typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback
18
+ return {
19
+ maxMembers: pick(args.maxMembers, DEFAULT_LIMITS.maxMembers),
20
+ maxDepth: pick(args.maxDepth, DEFAULT_LIMITS.maxDepth),
21
+ maxNodes: pick(args.maxNodes, DEFAULT_LIMITS.maxNodes),
22
+ }
23
+ }
24
+
25
+ export const diagramReviewTool = defineTool({
26
+ name: DIAGRAM_REVIEW_TOOL_NAME,
27
+ description:
28
+ 'Review the structure of a diagram from its source text: how wide each boundary is, how deep they nest, and how many nodes the drawing holds, against budgets you can set. Accepts the same Mermaid and PlantUML subset as diagram_render and reports what it measured, so an empty report cannot be mistaken for a clean one. Read `verdict`, never `ok`. This reviews structure; it does not draw (diagram_render does) and it does not check behaviour (model_verify does).',
29
+ parameters: {
30
+ source: {
31
+ type: 'string',
32
+ description: 'The diagram source text, in Mermaid or PlantUML syntax.',
33
+ },
34
+ notation: {
35
+ type: 'string',
36
+ enum: ['auto', 'mermaid', 'plantuml'],
37
+ description: 'Which notation `source` is written in. Default auto: read from the first meaningful line.',
38
+ },
39
+ maxMembers: {
40
+ type: 'integer',
41
+ description: 'How many direct children a boundary may hold. Default 7: more than a handful of boxes stops reading as a package.',
42
+ },
43
+ maxDepth: {
44
+ type: 'integer',
45
+ description: 'How deep boundaries may nest. Default 3.',
46
+ },
47
+ maxNodes: {
48
+ type: 'integer',
49
+ description: 'How many nodes the whole drawing may hold. Default 40.',
50
+ },
51
+ },
52
+ output: {
53
+ schema: { type: 'json', description: 'Structural review: the verdict, one finding per breached budget, and what was measured.' },
54
+ render(_args, value) {
55
+ const report = value as Record<string, unknown>
56
+ const findings = Array.isArray(report.findings) ? (report.findings as Array<Record<string, unknown>>) : []
57
+ const measured = (report.measured ?? {}) as Record<string, unknown>
58
+ const lines: string[] = []
59
+ lines.push(
60
+ 'dsh-diagram/review/v1 verdict=' + String(report.verdict) + ' ' + String(findings.length) + ' finding(s)',
61
+ )
62
+ lines.push(
63
+ 'MEASURED nodes=' + String(measured.nodes) + ' groups=' + String(measured.groups) + ' deepest=' + String(measured.deepest) +
64
+ (measured.widest === null || measured.widest === undefined ? '' : ' widest=' + JSON.stringify(measured.widest)),
65
+ )
66
+ for (const finding of findings) {
67
+ lines.push(' [' + String(finding.severity) + '/' + String(finding.code) + '] ' + String(finding.message))
68
+ }
69
+ for (const line of Array.isArray(report.refused) ? (report.refused as string[]) : []) {
70
+ lines.push(' refused ' + line)
71
+ }
72
+ if (findings.length === 0) lines.push('NEXT The drawing is inside every budget. That says nothing about whether the system it describes is right.')
73
+ else lines.push('NEXT Split or fold the groups named above, or raise the budget if the medium is larger than a conversation pane.')
74
+ return [{ type: 'text' as const, text: lines.join('\n') }]
75
+ },
76
+ },
77
+ timeoutMs: 10_000,
78
+ isConcurrencySafe: () => true,
79
+ async execute(args: { source?: unknown; notation?: unknown; maxMembers?: unknown; maxDepth?: unknown; maxNodes?: unknown }) {
80
+ const source = typeof args.source === 'string' ? args.source : ''
81
+ const notation = args.notation === 'mermaid' || args.notation === 'plantuml' ? (args.notation as Notation) : 'auto'
82
+ if (source.trim() === '') {
83
+ return {
84
+ ok: false,
85
+ verdict: 'fail' as const,
86
+ findings: [{ code: 'D0_NO_SOURCE', severity: 'error', message: 'source is empty: pass the diagram text to review' }],
87
+ measured: { nodes: 0, groups: 0, deepest: 0, widest: null },
88
+ refused: [],
89
+ }
90
+ }
91
+ const rendered = render({ source, notation })
92
+ const report = checkBudget(rendered.diagram, limitsOf(args))
93
+ return {
94
+ ok: report.verdict !== 'fail',
95
+ verdict: report.verdict,
96
+ // Plain JSON fields: a finding whose `group` is absent must not carry an undefined key.
97
+ findings: report.findings.map((finding) => ({
98
+ code: finding.code,
99
+ severity: finding.severity,
100
+ message: finding.message,
101
+ ...(finding.group === undefined ? {} : { group: finding.group }),
102
+ })),
103
+ measured: report.measured,
104
+ refused: rendered.refused,
105
+ }
106
+ },
107
+ })
108
+
109
+
package/src/diagram.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The diagram component: one row, one switch, three tools.
3
+ *
4
+ * A separate component rather than a feature of the tools above it, because drawing has nothing to do with
5
+ * verifying. It can be on while the tools are off, off while they are on, and it carries no engine: a
6
+ * deployment that only wants pictures of its Mermaid diagrams installs the same package and leaves the rest of
7
+ * it alone.
8
+ *
9
+ * The switch is `config.enabled`, live, exactly like the routing gate: a deployment can turn the drawing off at
10
+ * boot, from the Plugins page, or by disabling this row, and none of those touches the tools or the skills.
11
+ */
12
+
13
+ import z from '@deepseek-ai/schemastery'
14
+ import { diagramDiffTool } from './diagram-diff-tool.js'
15
+ import { diagramRenderTool } from './diagram-render-tool.js'
16
+ import { diagramReviewTool } from './diagram-review-tool.js'
17
+
18
+ /** The default a deployment inherits: on, because a component nobody knows about is a component nobody uses. */
19
+ const DEFAULT_ENABLED = true
20
+
21
+ /**
22
+ * A live boolean, or a plain one on a host that predates the live wrapper.
23
+ *
24
+ * `live` is what makes the Plugins page switch take effect without a restart; the value it returns is read
25
+ * through {@link enabledOf} rather than compared, because both shapes have been seen in the wild.
26
+ */
27
+ const liveBoolean = (z as unknown as { live?: (schema: unknown) => unknown }).live
28
+
29
+ export interface Config {
30
+ /** Whether the diagram tools are registered. Read through {@link enabledOf}. */
31
+ enabled: unknown
32
+ /** The title a drawing carries when the caller did not name one. */
33
+ defaultTitle: string
34
+ }
35
+
36
+ export const Config = z.object({
37
+ enabled:
38
+ liveBoolean === undefined
39
+ ? z.boolean().default(DEFAULT_ENABLED)
40
+ : (liveBoolean(z.boolean().default(DEFAULT_ENABLED)) as never),
41
+ defaultTitle: z.string().default('diagram'),
42
+ })
43
+
44
+ /** Read the switch as a boolean, whichever shape this host produced. */
45
+ export function enabledOf(config: Config): boolean {
46
+ const value = config.enabled as boolean | { get: () => boolean }
47
+ return typeof value === 'boolean' ? value : value.get()
48
+ }
49
+
50
+ export interface ComponentContext {
51
+ tools: { register(definition: unknown): () => void }
52
+ }
53
+
54
+ export function apply(ctx: ComponentContext, config: Config): void {
55
+ if (!enabledOf(config)) return
56
+ ctx.tools.register(diagramRenderTool)
57
+ ctx.tools.register(diagramReviewTool)
58
+ ctx.tools.register(diagramDiffTool)
59
+ }
60
+
61
+ export const inject = ['tools']
package/src/gate.ts CHANGED
@@ -68,6 +68,7 @@ const SKILL_ROWS: Record<string, string> = {
68
68
  'model-everything-design': 'layering, responsibilities, substitutability, interface width',
69
69
  'model-everything-datamodel': 'a schema, a migration, a data guarantee',
70
70
  'model-everything-interleaving': 'thread-safe, lock-free, ISR, async interleaving; a shared location is decided, the rest is routed',
71
+ 'model-everything-diagram': 'drawing, sizing or diffing a diagram from its source text',
71
72
  'model-everything-report-reading': 'reading a report this plugin returned',
72
73
  }
73
74
 
@@ -108,11 +109,11 @@ export function renderGateText(skills: string[] = shippedSkills(), extraGuidance
108
109
  const rows = skills.map((skill) => '| ' + (SKILL_ROWS[skill] ?? 'see its catalog line') + ' | `' + skill + '` |')
109
110
  const extra = extraGuidance.trim()
110
111
  return `<EXTREMELY_IMPORTANT>
111
- Before answering anything about code, a design or a diagram: load the skill whose row matches, and let its tool decide. Do not answer from memory first.
112
+ Before answering anything about code, a design or a diagram: load the skill whose row matches and let its tool decide. Do not answer from memory.
112
113
 
113
- Plugin dsh-model-everything is active. It checks claims about designs and code: facts against the source, behaviour as an executable model, structure as a graph.
114
+ dsh-model-everything is active: it checks claims about designs and code - facts against the source, behaviour as a model, structure as a graph.
114
115
 
115
- **Unsure which row applies?** Load \`model-everything\` first: it owns the doctrine, the model schema and the routing to its siblings.
116
+ **Unsure which row applies?** Load \`model-everything\` first: it owns the doctrine, the schema and the routing to its siblings.
116
117
 
117
118
  | The request | The skill |
118
119
  | --- | --- |
@@ -120,22 +121,23 @@ ${rows.join('\n')}
120
121
 
121
122
  **Tools** (dsh):
122
123
 
123
- - \`model_verify\` — one state machine: structural checks, adversarial probes, budgets, deadlines, before/after regression.
124
+ - \`model_verify\` — one state machine: structural checks, adversarial probes, budgets, deadlines, regression.
124
125
  - \`model_compose_verify\` — two or more machines: deadlock, a rendezvous that never fires, and - with \`shared\` + \`accesses\` - an unordered write (C4) or a non-atomic update (C5).
125
- - \`model_datamodel_verify\` — a data model and its migration: keys, domains, migration coverage, copy consistency, rollback symmetry.
126
- - \`model_design_review\` — a design against its own declarations: layering, boundaries, substitutability, interface width. The graph's shape is reported, not judged.
127
- - \`model_interleaving_scan\` — finds claims about threads, synchronization, async and interrupts, names the family, and routes each to the tool that decides it. It proves nothing about any of them.
128
- - \`model_export\` — UPPAAL, TLA+, PRISM or SPIN input from a verified model. It exports; it does not check the target tool.
129
- - \`uml_render\`, \`uml_parse\`, \`uml_review\` — draw a model, read a diagram into one, audit one. Round-trip fidelity is measured, never assumed.
126
+ - \`model_datamodel_verify\` — a data model and its migration: keys, domains, coverage, copy consistency, rollback symmetry.
127
+ - \`model_design_review\` — a design against its own declarations: layering, boundaries, substitutability, interface width. Graph shape is reported, not judged.
128
+ - \`model_interleaving_scan\` — finds claims about threads, async and interrupts, names the family and routes each to what decides it. It proves nothing itself.
129
+ - \`model_export\` — UPPAAL, TLA+, PRISM or SPIN input from a model. It exports; it does not check the target.
130
+ - \`uml_render\`, \`uml_parse\`, \`uml_review\` — draw a model, read a diagram into one, audit one. Round-trip fidelity is measured.
130
131
  - \`uml_explain_labels\` — the accepted label and comment forms for one notation.
131
132
  - \`uml_structure_verify\` — dependency review of a structure drawing, one level or several. It audits the drawing, not the code.
133
+ - \`diagram_render\`, \`diagram_review\`, \`diagram_diff\` — draw, size and compare a Mermaid or PlantUML source. Undrawable lines are refused by number.
132
134
  - \`report_diff\` — baseline against current: what a change added, removed or changed.
133
135
 
134
- **Read a report**: gate on \`verdict\` (\`pass\`, \`pass_with_findings\`, \`fail\`), never on \`ok\`. \`verdictReason\` says why and \`nextSteps\` says what to do. \`ran\` says whether it executed, \`checksRun\` how much it examined: a \`skip\` is not a \`pass\`, and a code ending \`_UNDECIDED\` means the search hit its limit and decided nothing. Keys starting with \`_\` are notes: ignored, and left out of the model hash.
136
+ **Read a report**: gate on \`verdict\`, never on \`ok\`. \`verdictReason\` says why and \`nextSteps\` says what to do. \`ran\` says whether it executed, \`checksRun\` how much it examined: a \`skip\` is not a \`pass\`, and a code ending \`_UNDECIDED\` means the search hit its limit and decided nothing. Keys starting with \`_\` are notes: ignored, and left out of the model hash.
135
137
 
136
- **When a tool refuses, or is not there.** Read its \`errorCode\` and fix the input before retrying; if the tool is not registered in this deployment, say so and use the general method. A check that nothing ran is not a passing check.${extra === '' ? '' : '\n\n' + extra}
138
+ **When a tool refuses or is absent.** Read its \`errorCode\` and fix the input; if it is not registered here, say so and use the general method. A check that nothing ran is not a passing check.${extra === '' ? '' : '\n\n' + extra}
137
139
 
138
- The rule again, because it is the one that gets skipped: before answering anything about code, a design or a diagram, load the skill whose routing applies and let its tool decide - a claim you can check is not a claim you may answer from memory.
140
+ The rule again, because it is the one that gets skipped: load the skill whose routing applies and let its tool decide - a claim you can check is not one you may answer from memory.
139
141
  </EXTREMELY_IMPORTANT>`
140
142
  }
141
143
 
@@ -53,6 +53,8 @@ export interface ReportCard {
53
53
  graph?: { nodes: number; edges: number; cycles: number; isolated: number; dangling: number }
54
54
  /** `model_design_review`: three levels, because a signal is not a violation and a fact is neither. */
55
55
  levels?: { violations: number; signals: number; facts: number; skipped: number }
56
+ /** `uml_structure_verify` with `repoPath`: what the scan itself resolved, and what it could not. */
57
+ scan?: { files: number; unresolved: number; packages: number; dynamic: number }
56
58
  /** `model_interleaving_scan`: how many claims were found, and in which families. */
57
59
  claims?: { absolute: number; keywords: number; families: Array<{ family: string; count: number }> }
58
60
  }
@@ -109,6 +111,7 @@ export function reportCard(value: unknown, family: string): ReportCard {
109
111
  const errors = findings.filter((entry) => severity(entry) === 'error')
110
112
  const warnings = findings.filter((entry) => severity(entry) === 'warning')
111
113
  const summary = record(report.summary)
114
+ const scan = record(report.scan)
112
115
  const summaryErrors = typeof summary?.errors === 'number' ? summary.errors : undefined
113
116
  const summaryWarnings = typeof summary?.warnings === 'number' ? summary.warnings : undefined
114
117
  const checksRun = typeof summary?.checksRun === 'number'
@@ -156,6 +159,9 @@ export function reportCard(value: unknown, family: string): ReportCard {
156
159
  ...(violations.length === 0 && signals.length === 0 && facts.length === 0
157
160
  ? {}
158
161
  : { levels: { violations: violations.length, signals: signals.length, facts: facts.length, skipped: array(report.checksSkipped).length } }),
162
+ ...(typeof scan?.files === 'number'
163
+ ? { scan: { files: scan.files, unresolved: Number(scan.unresolved ?? 0), packages: Number(scan.packages ?? 0), dynamic: Number(scan.dynamic ?? 0) } }
164
+ : {}),
159
165
  ...(typeof summary?.absoluteClaims === 'number'
160
166
  ? { claims: { absolute: summary.absoluteClaims, keywords: Number(summary.keywords ?? 0), families: claimFamilies(findings) } }
161
167
  : {}),
@@ -309,6 +315,12 @@ export function structureCardBlocks(card: ReportCard): ContentBlock[] {
309
315
  if (card.graph !== undefined) {
310
316
  lines.push('graph: ' + String(card.graph.nodes) + ' nodes · ' + String(card.graph.edges) + ' edges · ' + String(card.graph.cycles) + ' cycle(s) · ' + String(card.graph.isolated) + ' isolated')
311
317
  }
318
+ if (card.scan !== undefined) {
319
+ // A scan that resolved half its imports reads the same as one that resolved all of them unless the
320
+ // counts are on the card.
321
+ const dynamic = card.scan.dynamic > 0 ? ' · ' + String(card.scan.dynamic) + ' computed' : ''
322
+ lines.push('scan: ' + String(card.scan.files) + ' file(s) · ' + String(card.scan.unresolved) + ' unresolved · ' + String(card.scan.packages) + ' package(s)' + dynamic)
323
+ }
312
324
  if (card.first != null) lines.push((card.first.severity === 'error' ? '✗ ' : '⚠ ') + card.first.code + ': ' + card.first.message)
313
325
  if (card.next != null) lines.push('next: ' + card.next)
314
326
  return [{ type: 'text', text: lines.join(String.fromCharCode(10)) }]
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Structural budgets for a diagram: the checks a drawing can fail without being wrong.
3
+ *
4
+ * A diagram is not verified the way a state machine is - there is no deadlock to find - but an architecture
5
+ * picture has properties a reader relies on, and they are checkable: a package that holds twenty things is not
6
+ * communicating anything, a boundary around one box is a layer the source did not mean, a group nothing points
7
+ * into and nothing leaves is a group that belongs on another page.
8
+ *
9
+ * Pure: a diagram and a set of limits in, findings out. The limits are arguments, not constants, because what
10
+ * counts as too wide depends on the medium - a slide holds less than a scrollable tab.
11
+ */
12
+
13
+ import type { Diagram, DiagramGroup } from './model.js'
14
+
15
+ export type Severity = 'error' | 'warning'
16
+
17
+ export interface BudgetFinding {
18
+ code: string
19
+ severity: Severity
20
+ message: string
21
+ /** The group the finding is about, when it is about one. */
22
+ group?: string
23
+ }
24
+
25
+ export interface BudgetLimits {
26
+ /** How many direct children a boundary may hold: its own nodes plus the groups directly inside it. */
27
+ maxMembers: number
28
+ /** How deep boundaries may nest. */
29
+ maxDepth: number
30
+ /** How many nodes the whole drawing may hold. */
31
+ maxNodes: number
32
+ }
33
+
34
+ /**
35
+ * Defaults, chosen for a drawing read in a conversation pane: a package with more than a handful of boxes stops
36
+ * being a package and becomes a diagram of its own, and three levels is what fits on one screen before a reader
37
+ * needs the source.
38
+ */
39
+ export const DEFAULT_LIMITS: BudgetLimits = { maxMembers: 7, maxDepth: 3, maxNodes: 40 }
40
+
41
+ export interface BudgetReport {
42
+ verdict: 'pass' | 'pass_with_findings' | 'fail'
43
+ findings: BudgetFinding[]
44
+ /** What was measured, so a reader can tell "nothing to check" from "checked and clean". */
45
+ measured: { nodes: number; groups: number; deepest: number; widest: { group: string; size: number } | null }
46
+ }
47
+
48
+ /** The direct children of a boundary: its own nodes plus the groups that name it as parent. */
49
+ function childrenOf(groups: DiagramGroup[], group: DiagramGroup): number {
50
+ const nested = groups.filter((entry) => entry.parent === group.id).length
51
+ return group.members.length + nested
52
+ }
53
+
54
+ /** How deep the nesting goes, guarding against a parent cycle in the data. */
55
+ function depthOf(groups: DiagramGroup[], groupId: string): number {
56
+ const byId = new Map(groups.map((group) => [group.id, group] as const))
57
+ let depth = 1
58
+ let cursor = byId.get(groupId)?.parent
59
+ const seen = new Set<string>([groupId])
60
+ while (cursor !== undefined && !seen.has(cursor)) {
61
+ seen.add(cursor)
62
+ depth += 1
63
+ cursor = byId.get(cursor)?.parent
64
+ }
65
+ return depth
66
+ }
67
+
68
+ /**
69
+ * Measure a drawing against its limits.
70
+ *
71
+ * Every finding names the group it is about, because a budget report that says "a group is too wide" without
72
+ * saying which one is a report nobody can act on.
73
+ */
74
+ export function checkBudget(diagram: Diagram, limits: Partial<BudgetLimits> = {}): BudgetReport {
75
+ const caps: BudgetLimits = { ...DEFAULT_LIMITS, ...limits }
76
+ const groups = diagram.groups ?? []
77
+ const findings: BudgetFinding[] = []
78
+
79
+ const nodeIds = new Set(diagram.nodes.map((node) => node.id))
80
+ for (const group of groups) {
81
+ const size = childrenOf(groups, group)
82
+ const depth = depthOf(groups, group.id)
83
+ if (size > caps.maxMembers) {
84
+ findings.push({
85
+ code: 'B1_GROUP_TOO_WIDE',
86
+ severity: 'error',
87
+ group: group.id,
88
+ message: 'group "' + group.label + '" holds ' + String(size) + ' direct children; the budget is ' + String(caps.maxMembers),
89
+ })
90
+ }
91
+ if (depth > caps.maxDepth) {
92
+ findings.push({
93
+ code: 'B2_TOO_DEEP',
94
+ severity: 'error',
95
+ group: group.id,
96
+ message: 'group "' + group.label + '" nests ' + String(depth) + ' levels deep; the budget is ' + String(caps.maxDepth),
97
+ })
98
+ }
99
+ const realMembers = group.members.filter((member) => nodeIds.has(member))
100
+ if (realMembers.length === 0 && groups.every((entry) => entry.parent !== group.id)) {
101
+ findings.push({
102
+ code: 'B3_EMPTY_GROUP',
103
+ severity: 'error',
104
+ group: group.id,
105
+ message: 'group "' + group.label + '" holds nothing that is drawn, so the boundary claims a containment the drawing does not show',
106
+ })
107
+ }
108
+ if (size === 1) {
109
+ findings.push({
110
+ code: 'B4_SINGLE_MEMBER',
111
+ severity: 'warning',
112
+ group: group.id,
113
+ message: 'group "' + group.label + '" holds one child; a boundary around one thing usually means a layer that was never split',
114
+ })
115
+ }
116
+ }
117
+
118
+ if (diagram.nodes.length > caps.maxNodes) {
119
+ findings.push({
120
+ code: 'B5_TOO_MANY_NODES',
121
+ severity: 'error',
122
+ message: 'the drawing holds ' + String(diagram.nodes.length) + ' nodes; the budget is ' + String(caps.maxNodes),
123
+ })
124
+ }
125
+
126
+ const deepest = groups.reduce((max, group) => Math.max(max, depthOf(groups, group.id)), 0)
127
+ const widest = groups.reduce<{ group: string; size: number } | null>((best, group) => {
128
+ const size = childrenOf(groups, group)
129
+ return best === null || size > best.size ? { group: group.label, size } : best
130
+ }, null)
131
+
132
+ const errors = findings.filter((finding) => finding.severity === 'error').length
133
+ const verdict: BudgetReport['verdict'] = errors > 0 ? 'fail' : findings.length > 0 ? 'pass_with_findings' : 'pass'
134
+ return { verdict, findings, measured: { nodes: diagram.nodes.length, groups: groups.length, deepest, widest } }
135
+ }