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.
- package/README.en-US.md +75 -19
- package/README.md +377 -345
- package/cordis.patch.yml +9 -0
- package/diagram/package.json +5 -0
- package/lib/client.js +3494 -0
- package/lib/diagram-diff-tool.js +90 -0
- package/lib/diagram-render-tool.js +102 -0
- package/lib/diagram-review-tool.js +99 -0
- package/lib/diagram.js +43 -0
- package/lib/gate.js +14 -12
- package/lib/presentation.js +10 -0
- package/lib/render/budget.js +99 -0
- package/lib/render/channels.js +243 -0
- package/lib/render/collapse.js +155 -0
- package/lib/render/components.js +99 -0
- package/lib/render/coords.js +288 -0
- package/lib/render/cycle-break.js +147 -0
- package/lib/render/diff.js +138 -0
- package/lib/render/dummy.js +43 -0
- package/lib/render/from-mermaid.js +235 -0
- package/lib/render/from-plantuml.js +430 -0
- package/lib/render/index.js +42 -0
- package/lib/render/layer.js +91 -0
- package/lib/render/layout.js +68 -0
- package/lib/render/model.js +17 -0
- package/lib/render/network-simplex.js +272 -0
- package/lib/render/order.js +122 -0
- package/lib/render/paint.js +891 -0
- package/lib/render/palette.js +127 -0
- package/lib/render/place.js +482 -0
- package/lib/render/route.js +225 -0
- package/lib/render/svg.js +229 -0
- package/lib/render/symbol.js +108 -0
- package/lib/render/text.js +74 -0
- package/lib/render/viewmodel.js +126 -0
- package/lib/render/wrap.js +87 -0
- package/lib/render/write.js +14 -0
- package/lib/scan.js +248 -8
- package/lib/structure-tool.js +2 -2
- package/lib/types/client.d.ts +83 -0
- package/lib/types/diagram-diff-tool.d.ts +8 -0
- package/lib/types/diagram-render-tool.d.ts +8 -0
- package/lib/types/diagram-review-tool.d.ts +8 -0
- package/lib/types/diagram.d.ts +34 -0
- package/lib/types/presentation.d.ts +7 -0
- package/lib/types/render/budget.d.ts +55 -0
- package/lib/types/render/channels.d.ts +63 -0
- package/lib/types/render/collapse.d.ts +23 -0
- package/lib/types/render/components.d.ts +57 -0
- package/lib/types/render/coords.d.ts +64 -0
- package/lib/types/render/cycle-break.d.ts +45 -0
- package/lib/types/render/diff.d.ts +44 -0
- package/lib/types/render/dummy.d.ts +32 -0
- package/lib/types/render/from-mermaid.d.ts +27 -0
- package/lib/types/render/from-plantuml.d.ts +33 -0
- package/lib/types/render/index.d.ts +38 -0
- package/lib/types/render/layer.d.ts +28 -0
- package/lib/types/render/layout.d.ts +54 -0
- package/lib/types/render/model.d.ts +73 -0
- package/lib/types/render/network-simplex.d.ts +54 -0
- package/lib/types/render/order.d.ts +27 -0
- package/lib/types/render/paint.d.ts +127 -0
- package/lib/types/render/palette.d.ts +41 -0
- package/lib/types/render/place.d.ts +55 -0
- package/lib/types/render/route.d.ts +77 -0
- package/lib/types/render/svg.d.ts +19 -0
- package/lib/types/render/symbol.d.ts +73 -0
- package/lib/types/render/text.d.ts +38 -0
- package/lib/types/render/viewmodel.d.ts +20 -0
- package/lib/types/render/wrap.d.ts +27 -0
- package/lib/types/render/write.d.ts +14 -0
- package/lib/types/scan.d.ts +10 -2
- package/lib/types/structure.d.ts +1 -0
- package/package.json +148 -132
- package/skills/model-everything/SKILL.md +454 -454
- package/skills/model-everything/references/architecture-domains.md +137 -0
- package/skills/model-everything/references/architecture-views.md +119 -0
- package/skills/model-everything-diagram/SKILL.md +84 -0
- package/src/client.ts +307 -0
- package/src/diagram-diff-tool.ts +94 -0
- package/src/diagram-render-tool.ts +108 -0
- package/src/diagram-review-tool.ts +109 -0
- package/src/diagram.ts +61 -0
- package/src/gate.ts +14 -12
- package/src/presentation.ts +12 -0
- package/src/render/budget.ts +135 -0
- package/src/render/channels.ts +309 -0
- package/src/render/collapse.ts +150 -0
- package/src/render/components.ts +126 -0
- package/src/render/coords.ts +328 -0
- package/src/render/cycle-break.ts +164 -0
- package/src/render/diff.ts +162 -0
- package/src/render/dummy.ts +65 -0
- package/src/render/from-mermaid.ts +261 -0
- package/src/render/from-plantuml.ts +469 -0
- package/src/render/index.ts +73 -0
- package/src/render/layer.ts +98 -0
- package/src/render/layout.ts +101 -0
- package/src/render/model.ts +86 -0
- package/src/render/network-simplex.ts +290 -0
- package/src/render/order.ts +139 -0
- package/src/render/paint.ts +1006 -0
- package/src/render/palette.ts +164 -0
- package/src/render/place.ts +527 -0
- package/src/render/route.ts +269 -0
- package/src/render/svg.ts +270 -0
- package/src/render/symbol.ts +137 -0
- package/src/render/text.ts +86 -0
- package/src/render/viewmodel.ts +136 -0
- package/src/render/wrap.ts +103 -0
- package/src/render/write.ts +17 -0
- package/src/scan.ts +807 -564
- package/src/structure-tool.ts +2 -2
- package/src/structure.ts +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
8
|
+
import { render } from './render/index.js';
|
|
9
|
+
import { diffStructure } from './render/diff.js';
|
|
10
|
+
export const DIAGRAM_DIFF_TOOL_NAME = 'diagram_diff';
|
|
11
|
+
/** Parse one side of a comparison, reporting which side failed to produce a drawing. */
|
|
12
|
+
function sideOf(source, notation, which) {
|
|
13
|
+
if (source.trim() === '')
|
|
14
|
+
return { refused: [], problem: which + ' is empty: pass the diagram text for that side' };
|
|
15
|
+
const rendered = render({ source, notation });
|
|
16
|
+
if (rendered.diagram.nodes.length === 0) {
|
|
17
|
+
return { refused: rendered.refused, problem: which + ' produced no nodes (' + String(rendered.refused.length) + ' line(s) refused)' };
|
|
18
|
+
}
|
|
19
|
+
return { diagram: rendered.diagram, refused: rendered.refused };
|
|
20
|
+
}
|
|
21
|
+
export const diagramDiffTool = defineTool({
|
|
22
|
+
name: DIAGRAM_DIFF_TOOL_NAME,
|
|
23
|
+
description: '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).',
|
|
24
|
+
parameters: {
|
|
25
|
+
before: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'The earlier diagram source.',
|
|
28
|
+
},
|
|
29
|
+
after: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'The later diagram source.',
|
|
32
|
+
},
|
|
33
|
+
notation: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
enum: ['auto', 'mermaid', 'plantuml'],
|
|
36
|
+
description: 'Which notation both sides are written in. Default auto: decided per side from its first meaningful line.',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
output: {
|
|
40
|
+
schema: { type: 'json', description: 'Structural diff: the verdict, every change with the id it is about, and the sizes of both sides.' },
|
|
41
|
+
render(_args, value) {
|
|
42
|
+
const report = value;
|
|
43
|
+
const changes = Array.isArray(report.changes) ? report.changes : [];
|
|
44
|
+
const measured = (report.measured ?? {});
|
|
45
|
+
const lines = [];
|
|
46
|
+
lines.push('dsh-diagram/diff/v1 verdict=' + String(report.verdict) + ' ' + String(changes.length) + ' change(s)');
|
|
47
|
+
lines.push('MEASURED before=' + String(measured.before?.nodes) + ' node(s)/' + String(measured.before?.groups) + ' group(s)' +
|
|
48
|
+
' after=' + String(measured.after?.nodes) + ' node(s)/' + String(measured.after?.groups) + ' group(s)');
|
|
49
|
+
for (const change of changes)
|
|
50
|
+
lines.push(' [' + String(change.severity) + '/' + String(change.code) + '] ' + String(change.message));
|
|
51
|
+
if (changes.length === 0)
|
|
52
|
+
lines.push('NEXT The structure is unchanged. That is all this compares: node text, edges and guards are not part of it.');
|
|
53
|
+
else
|
|
54
|
+
lines.push('NEXT Read each change against the design intent: a move is a decision, and this tool only reports that it happened.');
|
|
55
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
timeoutMs: 10_000,
|
|
59
|
+
isConcurrencySafe: () => true,
|
|
60
|
+
async execute(args) {
|
|
61
|
+
const notation = args.notation === 'mermaid' || args.notation === 'plantuml' ? args.notation : 'auto';
|
|
62
|
+
const before = sideOf(typeof args.before === 'string' ? args.before : '', notation, 'before');
|
|
63
|
+
const after = sideOf(typeof args.after === 'string' ? args.after : '', notation, 'after');
|
|
64
|
+
const problems = [before.problem, after.problem].filter((problem) => problem !== undefined);
|
|
65
|
+
if (before.diagram === undefined || after.diagram === undefined) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
verdict: 'fail',
|
|
69
|
+
changes: problems.map((problem) => ({ code: 'D1_SIDE_UNREADABLE', severity: 'error', kind: 'removed', subject: '-', message: problem })),
|
|
70
|
+
measured: {
|
|
71
|
+
before: { nodes: before.diagram?.nodes.length ?? 0, groups: before.diagram?.groups?.length ?? 0 },
|
|
72
|
+
after: { nodes: after.diagram?.nodes.length ?? 0, groups: after.diagram?.groups?.length ?? 0 },
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const report = diffStructure(before.diagram, after.diagram);
|
|
77
|
+
return {
|
|
78
|
+
ok: report.verdict !== 'fail',
|
|
79
|
+
verdict: report.verdict,
|
|
80
|
+
changes: report.changes.map((change) => ({
|
|
81
|
+
code: change.code,
|
|
82
|
+
severity: change.severity,
|
|
83
|
+
kind: change.kind,
|
|
84
|
+
subject: change.subject,
|
|
85
|
+
message: change.message,
|
|
86
|
+
})),
|
|
87
|
+
measured: report.measured,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
8
|
+
import { render } from './render/index.js';
|
|
9
|
+
import { schemeNames } from './render/palette.js';
|
|
10
|
+
import { writeSvg } from './render/write.js';
|
|
11
|
+
export const DIAGRAM_TOOL_NAME = 'diagram_render';
|
|
12
|
+
export const diagramRenderTool = defineTool({
|
|
13
|
+
name: DIAGRAM_TOOL_NAME,
|
|
14
|
+
description: '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.',
|
|
15
|
+
parameters: {
|
|
16
|
+
source: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'The diagram source text, in Mermaid or PlantUML syntax.',
|
|
19
|
+
},
|
|
20
|
+
notation: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
enum: ['auto', 'mermaid', 'plantuml'],
|
|
23
|
+
description: 'Which notation `source` is written in. Default auto: read from the first meaningful line.',
|
|
24
|
+
},
|
|
25
|
+
title: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'The heading drawn above the diagram. Defaults to the component configuration.',
|
|
28
|
+
},
|
|
29
|
+
scheme: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
enum: schemeNames(),
|
|
32
|
+
description: 'Colour scheme. Default: the standard light palette. print is greyscale, for a drawing that will be printed or embedded in a plain document.',
|
|
33
|
+
},
|
|
34
|
+
out: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: 'Write the SVG here and report the path. Relative paths resolve against the session workspace.',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
output: {
|
|
40
|
+
schema: { type: 'json', description: 'Drawing result: the notation used, the counts, every refused line, and the SVG markup.' },
|
|
41
|
+
render(_args, value) {
|
|
42
|
+
const result = value;
|
|
43
|
+
const lines = [];
|
|
44
|
+
const refused = Array.isArray(result.refused) ? result.refused : [];
|
|
45
|
+
lines.push('dsh-diagram/v1 notation=' + String(result.notation) + ' nodes=' + String(result.nodes) + ' edges=' + String(result.edges) +
|
|
46
|
+
(Number(result.groups) > 0 ? ' groups=' + String(result.groups) : '') +
|
|
47
|
+
(result.path === undefined ? '' : ' written=' + String(result.path)));
|
|
48
|
+
lines.push('CAPTION ' + String(result.caption));
|
|
49
|
+
if (refused.length > 0) {
|
|
50
|
+
lines.push('');
|
|
51
|
+
lines.push('REFUSED ' + String(refused.length) + ' line(s) - the drawing covers the rest');
|
|
52
|
+
for (const line of refused)
|
|
53
|
+
lines.push(' ' + line);
|
|
54
|
+
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.');
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
lines.push('NEXT Every line was drawn. Paste the SVG where the diagram belongs, or pass `out` to write it next to the document.');
|
|
58
|
+
}
|
|
59
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
timeoutMs: 10_000,
|
|
63
|
+
isConcurrencySafe: () => true,
|
|
64
|
+
async execute(args) {
|
|
65
|
+
const source = typeof args.source === 'string' ? args.source : '';
|
|
66
|
+
if (source.trim() === '') {
|
|
67
|
+
// One shape for every answer, so the output contract has nothing to special-case.
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
notation: 'mermaid',
|
|
71
|
+
nodes: 0,
|
|
72
|
+
edges: 0,
|
|
73
|
+
groups: 0,
|
|
74
|
+
caption: 'no source',
|
|
75
|
+
refused: ['source is empty: pass the diagram text to draw'],
|
|
76
|
+
dropped: 0,
|
|
77
|
+
svg: '',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const notation = args.notation === 'mermaid' || args.notation === 'plantuml' ? args.notation : 'auto';
|
|
81
|
+
const title = typeof args.title === 'string' && args.title.trim() !== '' ? args.title.trim() : undefined;
|
|
82
|
+
// The argument validator has already held this to the shipped schemes; an unknown value falls back rather
|
|
83
|
+
// than throwing, because a stale deployment may still send one.
|
|
84
|
+
const scheme = schemeNames().includes(args.scheme) ? args.scheme : 'default';
|
|
85
|
+
const rendered = render({ source, notation, ...(title === undefined ? {} : { title }) }, {}, { scheme });
|
|
86
|
+
const written = typeof args.out === 'string' && args.out.trim() !== ''
|
|
87
|
+
? writeSvg({ source, notation, ...(title === undefined ? {} : { title }) }, args.out.trim(), {}, { scheme })
|
|
88
|
+
: undefined;
|
|
89
|
+
return {
|
|
90
|
+
ok: rendered.refused.length === 0,
|
|
91
|
+
notation: rendered.notation,
|
|
92
|
+
nodes: rendered.diagram.nodes.length,
|
|
93
|
+
edges: rendered.diagram.edges.length,
|
|
94
|
+
groups: rendered.diagram.groups?.length ?? 0,
|
|
95
|
+
caption: rendered.diagram.caption,
|
|
96
|
+
refused: rendered.refused,
|
|
97
|
+
dropped: rendered.dropped,
|
|
98
|
+
...(written === undefined ? {} : { path: written.path, bytes: written.bytes }),
|
|
99
|
+
svg: rendered.markup,
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
8
|
+
import { render } from './render/index.js';
|
|
9
|
+
import { checkBudget, DEFAULT_LIMITS } from './render/budget.js';
|
|
10
|
+
export const DIAGRAM_REVIEW_TOOL_NAME = 'diagram_review';
|
|
11
|
+
/** Read the three caps a caller may override, ignoring anything that is not a positive number. */
|
|
12
|
+
function limitsOf(args) {
|
|
13
|
+
const pick = (value, fallback) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
14
|
+
return {
|
|
15
|
+
maxMembers: pick(args.maxMembers, DEFAULT_LIMITS.maxMembers),
|
|
16
|
+
maxDepth: pick(args.maxDepth, DEFAULT_LIMITS.maxDepth),
|
|
17
|
+
maxNodes: pick(args.maxNodes, DEFAULT_LIMITS.maxNodes),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export const diagramReviewTool = defineTool({
|
|
21
|
+
name: DIAGRAM_REVIEW_TOOL_NAME,
|
|
22
|
+
description: '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).',
|
|
23
|
+
parameters: {
|
|
24
|
+
source: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'The diagram source text, in Mermaid or PlantUML syntax.',
|
|
27
|
+
},
|
|
28
|
+
notation: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
enum: ['auto', 'mermaid', 'plantuml'],
|
|
31
|
+
description: 'Which notation `source` is written in. Default auto: read from the first meaningful line.',
|
|
32
|
+
},
|
|
33
|
+
maxMembers: {
|
|
34
|
+
type: 'integer',
|
|
35
|
+
description: 'How many direct children a boundary may hold. Default 7: more than a handful of boxes stops reading as a package.',
|
|
36
|
+
},
|
|
37
|
+
maxDepth: {
|
|
38
|
+
type: 'integer',
|
|
39
|
+
description: 'How deep boundaries may nest. Default 3.',
|
|
40
|
+
},
|
|
41
|
+
maxNodes: {
|
|
42
|
+
type: 'integer',
|
|
43
|
+
description: 'How many nodes the whole drawing may hold. Default 40.',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
output: {
|
|
47
|
+
schema: { type: 'json', description: 'Structural review: the verdict, one finding per breached budget, and what was measured.' },
|
|
48
|
+
render(_args, value) {
|
|
49
|
+
const report = value;
|
|
50
|
+
const findings = Array.isArray(report.findings) ? report.findings : [];
|
|
51
|
+
const measured = (report.measured ?? {});
|
|
52
|
+
const lines = [];
|
|
53
|
+
lines.push('dsh-diagram/review/v1 verdict=' + String(report.verdict) + ' ' + String(findings.length) + ' finding(s)');
|
|
54
|
+
lines.push('MEASURED nodes=' + String(measured.nodes) + ' groups=' + String(measured.groups) + ' deepest=' + String(measured.deepest) +
|
|
55
|
+
(measured.widest === null || measured.widest === undefined ? '' : ' widest=' + JSON.stringify(measured.widest)));
|
|
56
|
+
for (const finding of findings) {
|
|
57
|
+
lines.push(' [' + String(finding.severity) + '/' + String(finding.code) + '] ' + String(finding.message));
|
|
58
|
+
}
|
|
59
|
+
for (const line of Array.isArray(report.refused) ? report.refused : []) {
|
|
60
|
+
lines.push(' refused ' + line);
|
|
61
|
+
}
|
|
62
|
+
if (findings.length === 0)
|
|
63
|
+
lines.push('NEXT The drawing is inside every budget. That says nothing about whether the system it describes is right.');
|
|
64
|
+
else
|
|
65
|
+
lines.push('NEXT Split or fold the groups named above, or raise the budget if the medium is larger than a conversation pane.');
|
|
66
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
timeoutMs: 10_000,
|
|
70
|
+
isConcurrencySafe: () => true,
|
|
71
|
+
async execute(args) {
|
|
72
|
+
const source = typeof args.source === 'string' ? args.source : '';
|
|
73
|
+
const notation = args.notation === 'mermaid' || args.notation === 'plantuml' ? args.notation : 'auto';
|
|
74
|
+
if (source.trim() === '') {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
verdict: 'fail',
|
|
78
|
+
findings: [{ code: 'D0_NO_SOURCE', severity: 'error', message: 'source is empty: pass the diagram text to review' }],
|
|
79
|
+
measured: { nodes: 0, groups: 0, deepest: 0, widest: null },
|
|
80
|
+
refused: [],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const rendered = render({ source, notation });
|
|
84
|
+
const report = checkBudget(rendered.diagram, limitsOf(args));
|
|
85
|
+
return {
|
|
86
|
+
ok: report.verdict !== 'fail',
|
|
87
|
+
verdict: report.verdict,
|
|
88
|
+
// Plain JSON fields: a finding whose `group` is absent must not carry an undefined key.
|
|
89
|
+
findings: report.findings.map((finding) => ({
|
|
90
|
+
code: finding.code,
|
|
91
|
+
severity: finding.severity,
|
|
92
|
+
message: finding.message,
|
|
93
|
+
...(finding.group === undefined ? {} : { group: finding.group }),
|
|
94
|
+
})),
|
|
95
|
+
measured: report.measured,
|
|
96
|
+
refused: rendered.refused,
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
});
|
package/lib/diagram.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
import z from '@deepseek-ai/schemastery';
|
|
13
|
+
import { diagramDiffTool } from './diagram-diff-tool.js';
|
|
14
|
+
import { diagramRenderTool } from './diagram-render-tool.js';
|
|
15
|
+
import { diagramReviewTool } from './diagram-review-tool.js';
|
|
16
|
+
/** The default a deployment inherits: on, because a component nobody knows about is a component nobody uses. */
|
|
17
|
+
const DEFAULT_ENABLED = true;
|
|
18
|
+
/**
|
|
19
|
+
* A live boolean, or a plain one on a host that predates the live wrapper.
|
|
20
|
+
*
|
|
21
|
+
* `live` is what makes the Plugins page switch take effect without a restart; the value it returns is read
|
|
22
|
+
* through {@link enabledOf} rather than compared, because both shapes have been seen in the wild.
|
|
23
|
+
*/
|
|
24
|
+
const liveBoolean = z.live;
|
|
25
|
+
export const Config = z.object({
|
|
26
|
+
enabled: liveBoolean === undefined
|
|
27
|
+
? z.boolean().default(DEFAULT_ENABLED)
|
|
28
|
+
: liveBoolean(z.boolean().default(DEFAULT_ENABLED)),
|
|
29
|
+
defaultTitle: z.string().default('diagram'),
|
|
30
|
+
});
|
|
31
|
+
/** Read the switch as a boolean, whichever shape this host produced. */
|
|
32
|
+
export function enabledOf(config) {
|
|
33
|
+
const value = config.enabled;
|
|
34
|
+
return typeof value === 'boolean' ? value : value.get();
|
|
35
|
+
}
|
|
36
|
+
export function apply(ctx, config) {
|
|
37
|
+
if (!enabledOf(config))
|
|
38
|
+
return;
|
|
39
|
+
ctx.tools.register(diagramRenderTool);
|
|
40
|
+
ctx.tools.register(diagramReviewTool);
|
|
41
|
+
ctx.tools.register(diagramDiffTool);
|
|
42
|
+
}
|
|
43
|
+
export const inject = ['tools'];
|
package/lib/gate.js
CHANGED
|
@@ -51,6 +51,7 @@ const SKILL_ROWS = {
|
|
|
51
51
|
'model-everything-design': 'layering, responsibilities, substitutability, interface width',
|
|
52
52
|
'model-everything-datamodel': 'a schema, a migration, a data guarantee',
|
|
53
53
|
'model-everything-interleaving': 'thread-safe, lock-free, ISR, async interleaving; a shared location is decided, the rest is routed',
|
|
54
|
+
'model-everything-diagram': 'drawing, sizing or diffing a diagram from its source text',
|
|
54
55
|
'model-everything-report-reading': 'reading a report this plugin returned',
|
|
55
56
|
};
|
|
56
57
|
/**
|
|
@@ -90,11 +91,11 @@ export function renderGateText(skills = shippedSkills(), extraGuidance = '') {
|
|
|
90
91
|
const rows = skills.map((skill) => '| ' + (SKILL_ROWS[skill] ?? 'see its catalog line') + ' | `' + skill + '` |');
|
|
91
92
|
const extra = extraGuidance.trim();
|
|
92
93
|
return `<EXTREMELY_IMPORTANT>
|
|
93
|
-
Before answering anything about code, a design or a diagram: load the skill whose row matches
|
|
94
|
+
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.
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
dsh-model-everything is active: it checks claims about designs and code - facts against the source, behaviour as a model, structure as a graph.
|
|
96
97
|
|
|
97
|
-
**Unsure which row applies?** Load \`model-everything\` first: it owns the doctrine, the
|
|
98
|
+
**Unsure which row applies?** Load \`model-everything\` first: it owns the doctrine, the schema and the routing to its siblings.
|
|
98
99
|
|
|
99
100
|
| The request | The skill |
|
|
100
101
|
| --- | --- |
|
|
@@ -102,22 +103,23 @@ ${rows.join('\n')}
|
|
|
102
103
|
|
|
103
104
|
**Tools** (dsh):
|
|
104
105
|
|
|
105
|
-
- \`model_verify\` — one state machine: structural checks, adversarial probes, budgets, deadlines,
|
|
106
|
+
- \`model_verify\` — one state machine: structural checks, adversarial probes, budgets, deadlines, regression.
|
|
106
107
|
- \`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).
|
|
107
|
-
- \`model_datamodel_verify\` — a data model and its migration: keys, domains,
|
|
108
|
-
- \`model_design_review\` — a design against its own declarations: layering, boundaries, substitutability, interface width.
|
|
109
|
-
- \`model_interleaving_scan\` — finds claims about threads,
|
|
110
|
-
- \`model_export\` — UPPAAL, TLA+, PRISM or SPIN input from a
|
|
111
|
-
- \`uml_render\`, \`uml_parse\`, \`uml_review\` — draw a model, read a diagram into one, audit one. Round-trip fidelity is measured
|
|
108
|
+
- \`model_datamodel_verify\` — a data model and its migration: keys, domains, coverage, copy consistency, rollback symmetry.
|
|
109
|
+
- \`model_design_review\` — a design against its own declarations: layering, boundaries, substitutability, interface width. Graph shape is reported, not judged.
|
|
110
|
+
- \`model_interleaving_scan\` — finds claims about threads, async and interrupts, names the family and routes each to what decides it. It proves nothing itself.
|
|
111
|
+
- \`model_export\` — UPPAAL, TLA+, PRISM or SPIN input from a model. It exports; it does not check the target.
|
|
112
|
+
- \`uml_render\`, \`uml_parse\`, \`uml_review\` — draw a model, read a diagram into one, audit one. Round-trip fidelity is measured.
|
|
112
113
|
- \`uml_explain_labels\` — the accepted label and comment forms for one notation.
|
|
113
114
|
- \`uml_structure_verify\` — dependency review of a structure drawing, one level or several. It audits the drawing, not the code.
|
|
115
|
+
- \`diagram_render\`, \`diagram_review\`, \`diagram_diff\` — draw, size and compare a Mermaid or PlantUML source. Undrawable lines are refused by number.
|
|
114
116
|
- \`report_diff\` — baseline against current: what a change added, removed or changed.
|
|
115
117
|
|
|
116
|
-
**Read a report**: gate on \`verdict
|
|
118
|
+
**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.
|
|
117
119
|
|
|
118
|
-
**When a tool refuses
|
|
120
|
+
**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}
|
|
119
121
|
|
|
120
|
-
The rule again, because it is the one that gets skipped:
|
|
122
|
+
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.
|
|
121
123
|
</EXTREMELY_IMPORTANT>`;
|
|
122
124
|
}
|
|
123
125
|
/** The shipped default: every row the package can advertise, no deployment guidance. */
|
package/lib/presentation.js
CHANGED
|
@@ -50,6 +50,7 @@ export function reportCard(value, family) {
|
|
|
50
50
|
const errors = findings.filter((entry) => severity(entry) === 'error');
|
|
51
51
|
const warnings = findings.filter((entry) => severity(entry) === 'warning');
|
|
52
52
|
const summary = record(report.summary);
|
|
53
|
+
const scan = record(report.scan);
|
|
53
54
|
const summaryErrors = typeof summary?.errors === 'number' ? summary.errors : undefined;
|
|
54
55
|
const summaryWarnings = typeof summary?.warnings === 'number' ? summary.warnings : undefined;
|
|
55
56
|
const checksRun = typeof summary?.checksRun === 'number'
|
|
@@ -97,6 +98,9 @@ export function reportCard(value, family) {
|
|
|
97
98
|
...(violations.length === 0 && signals.length === 0 && facts.length === 0
|
|
98
99
|
? {}
|
|
99
100
|
: { levels: { violations: violations.length, signals: signals.length, facts: facts.length, skipped: array(report.checksSkipped).length } }),
|
|
101
|
+
...(typeof scan?.files === 'number'
|
|
102
|
+
? { scan: { files: scan.files, unresolved: Number(scan.unresolved ?? 0), packages: Number(scan.packages ?? 0), dynamic: Number(scan.dynamic ?? 0) } }
|
|
103
|
+
: {}),
|
|
100
104
|
...(typeof summary?.absoluteClaims === 'number'
|
|
101
105
|
? { claims: { absolute: summary.absoluteClaims, keywords: Number(summary.keywords ?? 0), families: claimFamilies(findings) } }
|
|
102
106
|
: {}),
|
|
@@ -250,6 +254,12 @@ export function structureCardBlocks(card) {
|
|
|
250
254
|
if (card.graph !== undefined) {
|
|
251
255
|
lines.push('graph: ' + String(card.graph.nodes) + ' nodes · ' + String(card.graph.edges) + ' edges · ' + String(card.graph.cycles) + ' cycle(s) · ' + String(card.graph.isolated) + ' isolated');
|
|
252
256
|
}
|
|
257
|
+
if (card.scan !== undefined) {
|
|
258
|
+
// A scan that resolved half its imports reads the same as one that resolved all of them unless the
|
|
259
|
+
// counts are on the card.
|
|
260
|
+
const dynamic = card.scan.dynamic > 0 ? ' · ' + String(card.scan.dynamic) + ' computed' : '';
|
|
261
|
+
lines.push('scan: ' + String(card.scan.files) + ' file(s) · ' + String(card.scan.unresolved) + ' unresolved · ' + String(card.scan.packages) + ' package(s)' + dynamic);
|
|
262
|
+
}
|
|
253
263
|
if (card.first != null)
|
|
254
264
|
lines.push((card.first.severity === 'error' ? '✗ ' : '⚠ ') + card.first.code + ': ' + card.first.message);
|
|
255
265
|
if (card.next != null)
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
* Defaults, chosen for a drawing read in a conversation pane: a package with more than a handful of boxes stops
|
|
14
|
+
* being a package and becomes a diagram of its own, and three levels is what fits on one screen before a reader
|
|
15
|
+
* needs the source.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_LIMITS = { maxMembers: 7, maxDepth: 3, maxNodes: 40 };
|
|
18
|
+
/** The direct children of a boundary: its own nodes plus the groups that name it as parent. */
|
|
19
|
+
function childrenOf(groups, group) {
|
|
20
|
+
const nested = groups.filter((entry) => entry.parent === group.id).length;
|
|
21
|
+
return group.members.length + nested;
|
|
22
|
+
}
|
|
23
|
+
/** How deep the nesting goes, guarding against a parent cycle in the data. */
|
|
24
|
+
function depthOf(groups, groupId) {
|
|
25
|
+
const byId = new Map(groups.map((group) => [group.id, group]));
|
|
26
|
+
let depth = 1;
|
|
27
|
+
let cursor = byId.get(groupId)?.parent;
|
|
28
|
+
const seen = new Set([groupId]);
|
|
29
|
+
while (cursor !== undefined && !seen.has(cursor)) {
|
|
30
|
+
seen.add(cursor);
|
|
31
|
+
depth += 1;
|
|
32
|
+
cursor = byId.get(cursor)?.parent;
|
|
33
|
+
}
|
|
34
|
+
return depth;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Measure a drawing against its limits.
|
|
38
|
+
*
|
|
39
|
+
* Every finding names the group it is about, because a budget report that says "a group is too wide" without
|
|
40
|
+
* saying which one is a report nobody can act on.
|
|
41
|
+
*/
|
|
42
|
+
export function checkBudget(diagram, limits = {}) {
|
|
43
|
+
const caps = { ...DEFAULT_LIMITS, ...limits };
|
|
44
|
+
const groups = diagram.groups ?? [];
|
|
45
|
+
const findings = [];
|
|
46
|
+
const nodeIds = new Set(diagram.nodes.map((node) => node.id));
|
|
47
|
+
for (const group of groups) {
|
|
48
|
+
const size = childrenOf(groups, group);
|
|
49
|
+
const depth = depthOf(groups, group.id);
|
|
50
|
+
if (size > caps.maxMembers) {
|
|
51
|
+
findings.push({
|
|
52
|
+
code: 'B1_GROUP_TOO_WIDE',
|
|
53
|
+
severity: 'error',
|
|
54
|
+
group: group.id,
|
|
55
|
+
message: 'group "' + group.label + '" holds ' + String(size) + ' direct children; the budget is ' + String(caps.maxMembers),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (depth > caps.maxDepth) {
|
|
59
|
+
findings.push({
|
|
60
|
+
code: 'B2_TOO_DEEP',
|
|
61
|
+
severity: 'error',
|
|
62
|
+
group: group.id,
|
|
63
|
+
message: 'group "' + group.label + '" nests ' + String(depth) + ' levels deep; the budget is ' + String(caps.maxDepth),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const realMembers = group.members.filter((member) => nodeIds.has(member));
|
|
67
|
+
if (realMembers.length === 0 && groups.every((entry) => entry.parent !== group.id)) {
|
|
68
|
+
findings.push({
|
|
69
|
+
code: 'B3_EMPTY_GROUP',
|
|
70
|
+
severity: 'error',
|
|
71
|
+
group: group.id,
|
|
72
|
+
message: 'group "' + group.label + '" holds nothing that is drawn, so the boundary claims a containment the drawing does not show',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (size === 1) {
|
|
76
|
+
findings.push({
|
|
77
|
+
code: 'B4_SINGLE_MEMBER',
|
|
78
|
+
severity: 'warning',
|
|
79
|
+
group: group.id,
|
|
80
|
+
message: 'group "' + group.label + '" holds one child; a boundary around one thing usually means a layer that was never split',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (diagram.nodes.length > caps.maxNodes) {
|
|
85
|
+
findings.push({
|
|
86
|
+
code: 'B5_TOO_MANY_NODES',
|
|
87
|
+
severity: 'error',
|
|
88
|
+
message: 'the drawing holds ' + String(diagram.nodes.length) + ' nodes; the budget is ' + String(caps.maxNodes),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const deepest = groups.reduce((max, group) => Math.max(max, depthOf(groups, group.id)), 0);
|
|
92
|
+
const widest = groups.reduce((best, group) => {
|
|
93
|
+
const size = childrenOf(groups, group);
|
|
94
|
+
return best === null || size > best.size ? { group: group.label, size } : best;
|
|
95
|
+
}, null);
|
|
96
|
+
const errors = findings.filter((finding) => finding.severity === 'error').length;
|
|
97
|
+
const verdict = errors > 0 ? 'fail' : findings.length > 0 ? 'pass_with_findings' : 'pass';
|
|
98
|
+
return { verdict, findings, measured: { nodes: diagram.nodes.length, groups: groups.length, deepest, widest } };
|
|
99
|
+
}
|