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,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a tool card shows, reduced to a drawing.
|
|
3
|
+
*
|
|
4
|
+
* The input is what the host hands a settled tool view: a phase and a block whose shape differs per phase.
|
|
5
|
+
* A settled call carries the paired call's arguments as a **lazy view** (`PartialArguments`) and the result
|
|
6
|
+
* as content blocks, so nothing here reads `block.args.model`: the view is asked, and the report text is
|
|
7
|
+
* parsed for the two facts a drawing needs. Every reader is total - an absent field removes a shape rather
|
|
8
|
+
* than throwing, because the same view renders during streaming.
|
|
9
|
+
*
|
|
10
|
+
* Pure. Everything decided here is a fact a test can assert: which nodes exist, which edges, why they are
|
|
11
|
+
* coloured the way they are.
|
|
12
|
+
*/
|
|
13
|
+
const record = (value) => typeof value === 'object' && value !== null && !Array.isArray(value) ? value : undefined;
|
|
14
|
+
const array = (value) => (Array.isArray(value) ? value : []);
|
|
15
|
+
const text = (value) => (typeof value === 'string' && value !== '' ? value : undefined);
|
|
16
|
+
/** The report text a settled call carries, joined: a card reads words, not blocks. */
|
|
17
|
+
function reportText(content) {
|
|
18
|
+
const parts = [];
|
|
19
|
+
for (const entry of array(content)) {
|
|
20
|
+
const block = record(entry);
|
|
21
|
+
const body = text(block?.text);
|
|
22
|
+
if (body !== undefined)
|
|
23
|
+
parts.push(body);
|
|
24
|
+
}
|
|
25
|
+
return parts.join(String.fromCharCode(10));
|
|
26
|
+
}
|
|
27
|
+
/** Two facts out of this plugin's own report format: the verdict, and the first error it named. */
|
|
28
|
+
function readReport(content) {
|
|
29
|
+
const body = reportText(content);
|
|
30
|
+
if (body === '')
|
|
31
|
+
return {};
|
|
32
|
+
const verdict = /verdict=(\w+)/.exec(body)?.[1];
|
|
33
|
+
const named = /\[error\/([A-Z0-9_]+)\]\s*(.*)/.exec(body);
|
|
34
|
+
if (named !== null)
|
|
35
|
+
return { verdict, code: named[1], message: named[2] };
|
|
36
|
+
const first = /first error:\s*([A-Z0-9_]+)/.exec(body);
|
|
37
|
+
return first === null ? { verdict } : { verdict, code: first[1] };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The `model` argument out of the host's lazy arguments view.
|
|
41
|
+
*
|
|
42
|
+
* A settled call exposes `value(key)`; a plain object (a test, or an older shape) is read directly. The
|
|
43
|
+
* view answers `undefined` until the field closes, which is exactly when a drawing should be attempted.
|
|
44
|
+
*/
|
|
45
|
+
function readModel(args) {
|
|
46
|
+
const view = record(args);
|
|
47
|
+
if (view === undefined)
|
|
48
|
+
return undefined;
|
|
49
|
+
const read = view.value;
|
|
50
|
+
if (typeof read === 'function')
|
|
51
|
+
return record(read('model'));
|
|
52
|
+
return record(view.model);
|
|
53
|
+
}
|
|
54
|
+
/** The states of a model: a machine, or nothing when the call carried none. */
|
|
55
|
+
function machineOf(model) {
|
|
56
|
+
if (model === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
const states = array(model.states).map(record).filter((entry) => entry !== undefined && typeof entry.id === 'string');
|
|
59
|
+
if (states.length === 0)
|
|
60
|
+
return undefined;
|
|
61
|
+
const edges = array(model.transitions)
|
|
62
|
+
.map(record)
|
|
63
|
+
.filter((entry) => entry !== undefined && typeof entry.from === 'string' && typeof entry.to === 'string')
|
|
64
|
+
.map((entry) => ({ from: String(entry.from), to: String(entry.to), label: text(entry.event) ?? '' }));
|
|
65
|
+
return {
|
|
66
|
+
nodes: states.map((state) => ({ id: String(state.id), role: 'normal' })),
|
|
67
|
+
edges,
|
|
68
|
+
...(typeof model.init === 'string' ? { init: model.init } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** The caption: the verdict, then the first error's code and message when the report named one. */
|
|
72
|
+
function captionOf(report) {
|
|
73
|
+
const verdict = report.verdict ?? 'no report yet';
|
|
74
|
+
if (report.code === undefined)
|
|
75
|
+
return verdict;
|
|
76
|
+
const message = report.message ?? '';
|
|
77
|
+
return verdict + ' — ' + report.code + (message === '' ? '' : ': ' + message);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Which nodes the report blames.
|
|
81
|
+
*
|
|
82
|
+
* A finding names a state inside its message as `"x"`; matching that is a heuristic, so it only ever *adds*
|
|
83
|
+
* emphasis and never decides the verdict.
|
|
84
|
+
*/
|
|
85
|
+
function blame(nodes, report) {
|
|
86
|
+
if (report.message === undefined)
|
|
87
|
+
return nodes;
|
|
88
|
+
const message = report.message;
|
|
89
|
+
// Whole words only: a report does not quote the state, and a state called `do` must not match `done`.
|
|
90
|
+
const named = nodes
|
|
91
|
+
.filter((node) => {
|
|
92
|
+
const escaped = node.id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
93
|
+
return new RegExp('(?<![A-Za-z0-9_])' + escaped + '(?![A-Za-z0-9_])').test(message);
|
|
94
|
+
})
|
|
95
|
+
.map((node) => node.id);
|
|
96
|
+
if (named.length === 0)
|
|
97
|
+
return nodes;
|
|
98
|
+
return nodes.map((node) => (named.includes(node.id) ? { ...node, role: 'error' } : node));
|
|
99
|
+
}
|
|
100
|
+
export function viewModel(input) {
|
|
101
|
+
const block = record(input.block);
|
|
102
|
+
const title = text(block?.name) ?? text(input.toolName) ?? 'tool';
|
|
103
|
+
const report = readReport(block?.content);
|
|
104
|
+
// A shape that still carries a parsed report object is read from there; it is what older callers pass.
|
|
105
|
+
const parsed = record(block?.result);
|
|
106
|
+
const findings = array(parsed?.findings).map(record);
|
|
107
|
+
const firstError = findings.find((finding) => finding?.severity === 'error');
|
|
108
|
+
const merged = {
|
|
109
|
+
verdict: text(parsed?.verdict) ?? report.verdict,
|
|
110
|
+
...(report.code === undefined && typeof firstError?.code === 'string' ? { code: firstError.code } : { code: report.code }),
|
|
111
|
+
...(report.message === undefined && typeof firstError?.message === 'string' ? { message: firstError.message } : { message: report.message }),
|
|
112
|
+
};
|
|
113
|
+
const caption = captionOf(merged);
|
|
114
|
+
const machine = machineOf(readModel(block?.args));
|
|
115
|
+
if (machine === undefined) {
|
|
116
|
+
return { kind: 'none', title, nodes: [], edges: [], caption };
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
kind: 'machine',
|
|
120
|
+
title,
|
|
121
|
+
...(machine.init === undefined ? {} : { init: machine.init }),
|
|
122
|
+
nodes: blame(machine.nodes, merged),
|
|
123
|
+
edges: machine.edges,
|
|
124
|
+
caption,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrapping: turning one endless column into a page.
|
|
3
|
+
*
|
|
4
|
+
* A chain of twelve boxes has twelve layers, and layering alone draws it as a single column taller than anything a
|
|
5
|
+
* reader can take in - a strip, not a drawing. Graphviz and ELK both answer this with a wrapping pass: cut the layer
|
|
6
|
+
* sequence into bands and stand the bands side by side, so reading order becomes "down the page, then the next
|
|
7
|
+
* column", the way text wraps.
|
|
8
|
+
*
|
|
9
|
+
* The cut is chosen, not guessed: each candidate number of bands is scored by how close the packed page gets to the
|
|
10
|
+
* aspect ratio a page and a landscape screen share, and the best one wins. A drawing that is already close to that
|
|
11
|
+
* shape is left alone - wrapping exists for the strip, not for every tall drawing.
|
|
12
|
+
*
|
|
13
|
+
* Pure: layer sizes in, band ranges and offsets out. The caller applies them.
|
|
14
|
+
*/
|
|
15
|
+
/** The aspect ratio a page and a landscape screen share. */
|
|
16
|
+
const PAGE_ASPECT = 1.6;
|
|
17
|
+
/** A drawing shorter than this many layers is read better stacked, whatever its proportions. */
|
|
18
|
+
const MIN_LAYERS_TO_WRAP = 4;
|
|
19
|
+
/** A drawing that is not this tall relative to its width is not a strip. */
|
|
20
|
+
const STRIP_RATIO = 2;
|
|
21
|
+
/** The height of a run of layers with the gap between them. */
|
|
22
|
+
function runHeight(layerHeight, first, last, gap) {
|
|
23
|
+
let total = 0;
|
|
24
|
+
for (let index = first; index <= last; index += 1) {
|
|
25
|
+
total += layerHeight[index];
|
|
26
|
+
if (index < last)
|
|
27
|
+
total += gap;
|
|
28
|
+
}
|
|
29
|
+
return total;
|
|
30
|
+
}
|
|
31
|
+
/** The width of a run of layers: the widest layer sets it. */
|
|
32
|
+
function runWidth(layerWidth, first, last) {
|
|
33
|
+
let widest = 0;
|
|
34
|
+
for (let index = first; index <= last; index += 1)
|
|
35
|
+
widest = Math.max(widest, layerWidth[index]);
|
|
36
|
+
return widest;
|
|
37
|
+
}
|
|
38
|
+
/** How far the packed page is from the target shape, in log space so half and double count the same. */
|
|
39
|
+
function shapeError(width, height, aspect) {
|
|
40
|
+
if (width <= 0 || height <= 0)
|
|
41
|
+
return Infinity;
|
|
42
|
+
const ratio = Math.max(width, height) / Math.min(width, height);
|
|
43
|
+
return Math.abs(Math.log(ratio / aspect));
|
|
44
|
+
}
|
|
45
|
+
export function planBands(layerWidth, layerHeight, gap, aspect = PAGE_ASPECT) {
|
|
46
|
+
const count = layerWidth.length;
|
|
47
|
+
if (count === 0)
|
|
48
|
+
return { bands: [], width: 0, height: 0 };
|
|
49
|
+
const single = {
|
|
50
|
+
bands: [{ first: 0, last: count - 1, offsetX: 0 }],
|
|
51
|
+
width: runWidth(layerWidth, 0, count - 1),
|
|
52
|
+
height: runHeight(layerHeight, 0, count - 1, gap),
|
|
53
|
+
};
|
|
54
|
+
if (count < MIN_LAYERS_TO_WRAP || single.height <= single.width * STRIP_RATIO)
|
|
55
|
+
return single;
|
|
56
|
+
const columnGap = gap * 2;
|
|
57
|
+
let best = single;
|
|
58
|
+
let bestError = shapeError(single.width, single.height, aspect);
|
|
59
|
+
for (let bands = 2; bands < count; bands += 1) {
|
|
60
|
+
const base = Math.floor(count / bands);
|
|
61
|
+
const extra = count % bands;
|
|
62
|
+
const ranges = [];
|
|
63
|
+
let cursor = 0;
|
|
64
|
+
for (let band = 0; band < bands; band += 1) {
|
|
65
|
+
const size = base + (band < extra ? 1 : 0);
|
|
66
|
+
ranges.push({ first: cursor, last: cursor + size - 1 });
|
|
67
|
+
cursor += size;
|
|
68
|
+
}
|
|
69
|
+
let width = columnGap * (bands - 1);
|
|
70
|
+
let height = 0;
|
|
71
|
+
for (const range of ranges) {
|
|
72
|
+
width += runWidth(layerWidth, range.first, range.last);
|
|
73
|
+
height = Math.max(height, runHeight(layerHeight, range.first, range.last, gap));
|
|
74
|
+
}
|
|
75
|
+
if (shapeError(width, height, aspect) >= bestError)
|
|
76
|
+
continue;
|
|
77
|
+
const planned = [];
|
|
78
|
+
let offsetX = 0;
|
|
79
|
+
for (const range of ranges) {
|
|
80
|
+
planned.push({ first: range.first, last: range.last, offsetX });
|
|
81
|
+
offsetX += runWidth(layerWidth, range.first, range.last) + columnGap;
|
|
82
|
+
}
|
|
83
|
+
best = { bands: planned, width, height };
|
|
84
|
+
bestError = shapeError(width, height, aspect);
|
|
85
|
+
}
|
|
86
|
+
return best;
|
|
87
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing a drawing to disk: the CLI's job, kept out of the renderer.
|
|
3
|
+
*
|
|
4
|
+
* `render/index.ts` is bundled into the page as well as run in Node, so it may not import a filesystem;
|
|
5
|
+
* everything that touches one lives here, where only the host reaches it.
|
|
6
|
+
*/
|
|
7
|
+
import { writeFileSync } from 'node:fs';
|
|
8
|
+
import { render } from './index.js';
|
|
9
|
+
/** Write the drawing where a document can link to it. Returns the path, the byte count and the render. */
|
|
10
|
+
export function writeSvg(input, path, overrides = {}, options = {}) {
|
|
11
|
+
const rendered = render(input, overrides, options);
|
|
12
|
+
writeFileSync(path, rendered.markup, 'utf8');
|
|
13
|
+
return { path, bytes: rendered.markup.length, rendered };
|
|
14
|
+
}
|
package/lib/scan.js
CHANGED
|
@@ -18,6 +18,55 @@
|
|
|
18
18
|
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
19
19
|
import { extname, relative, resolve } from 'node:path';
|
|
20
20
|
import { OptionShapeError } from './option-shape.js';
|
|
21
|
+
/** `tsconfig.json` allows comments and trailing commas; this reads the subset it needs without a parser. */
|
|
22
|
+
function parseJsonc(text) {
|
|
23
|
+
const withoutComments = stripCComments(text);
|
|
24
|
+
const withoutTrailingCommas = withoutComments.replaceAll(/,\s*([}\]])/g, '$1');
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(withoutTrailingCommas);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The aliases a TypeScript or JavaScript tree declares in its own `tsconfig.json`/`jsconfig.json`.
|
|
34
|
+
*
|
|
35
|
+
* Read rather than asked for: the project already states how it resolves `@/x`, and asking a caller to
|
|
36
|
+
* repeat it is how a scan silently loses half its edges.
|
|
37
|
+
*/
|
|
38
|
+
function readAliases(root) {
|
|
39
|
+
for (const name of ['tsconfig.json', 'jsconfig.json']) {
|
|
40
|
+
let text;
|
|
41
|
+
try {
|
|
42
|
+
text = readFileSync(resolve(root, name), 'utf8');
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const parsed = parseJsonc(text);
|
|
48
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
49
|
+
continue;
|
|
50
|
+
const options = parsed.compilerOptions;
|
|
51
|
+
if (typeof options !== 'object' || options === null)
|
|
52
|
+
continue;
|
|
53
|
+
const compiler = options;
|
|
54
|
+
const baseUrl = typeof compiler.baseUrl === 'string' ? compiler.baseUrl.replaceAll(String.fromCharCode(92), '/').replace(/^\.\//, '') : undefined;
|
|
55
|
+
const aliases = [];
|
|
56
|
+
if (typeof compiler.paths === 'object' && compiler.paths !== null) {
|
|
57
|
+
for (const [pattern, targets] of Object.entries(compiler.paths)) {
|
|
58
|
+
if (!Array.isArray(targets))
|
|
59
|
+
continue;
|
|
60
|
+
const cleaned = targets.filter((target) => typeof target === 'string').map((target) => (baseUrl === undefined ? target : normalise([baseUrl, target])));
|
|
61
|
+
if (cleaned.length === 0)
|
|
62
|
+
continue;
|
|
63
|
+
aliases.push({ prefix: pattern.replace(/\*$/, ''), targets: cleaned.map((target) => target.replace(/\*$/, '')) });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { aliases, ...(baseUrl === undefined ? {} : { baseUrl }) };
|
|
67
|
+
}
|
|
68
|
+
return { aliases: [] };
|
|
69
|
+
}
|
|
21
70
|
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', 'out', 'coverage', '.next', '.cache', 'target', 'vendor', '__pycache__', '.venv', 'venv']);
|
|
22
71
|
const LIMITS = { files: 5000, bytes: 2_000_000 };
|
|
23
72
|
/** Strip comments so a commented-out include is not an edge. */
|
|
@@ -64,7 +113,14 @@ const JS_LIKE = {
|
|
|
64
113
|
extract(text) {
|
|
65
114
|
const source = stripCComments(text);
|
|
66
115
|
const { local, external } = specifiers(source, [/(?:^|[^.\w])from\s*['"]([^'"]+)['"]/g, /(?:^|[^.\w])import\s*\(\s*['"]([^'"]+)['"]\s*\)/g, /(?:^|[^.\w])require\s*\(\s*['"]([^'"]+)['"]\s*\)/g, /(?:^|[^.\w])import\s*['"]([^'"]+)['"]/g]);
|
|
67
|
-
|
|
116
|
+
// `@/x`, `~/x` and `#x` are project-internal by convention: a path alias, a subpath import. Counting
|
|
117
|
+
// them as packages made a tree full of aliases look like it had one external dependency and almost no
|
|
118
|
+
// internal edges - the quietest way for a scan to be wrong.
|
|
119
|
+
const aliasShaped = (name) => name.startsWith('@/') || name.startsWith('~/') || name.startsWith('#');
|
|
120
|
+
// A call whose target is not a literal cannot be resolved from the source at all. Counting it is the
|
|
121
|
+
// honest move: the graph is missing those edges and the report says how many.
|
|
122
|
+
const dynamic = [...source.matchAll(/(?:^|[^.\w])import\s*\(\s*[^'"\s)]|(?:^|[^.\w])require\s*\(\s*[^'"\s)]/g)].length;
|
|
123
|
+
return { local: [...local, ...external.filter(aliasShaped)], external: external.filter((name) => !name.startsWith('node:') && !aliasShaped(name)), dynamic };
|
|
68
124
|
},
|
|
69
125
|
};
|
|
70
126
|
const C_LIKE = {
|
|
@@ -418,6 +474,23 @@ function resolveSpecifier(language, fromId, specifier, index) {
|
|
|
418
474
|
}
|
|
419
475
|
if (specifier.startsWith('/'))
|
|
420
476
|
return undefined;
|
|
477
|
+
if (!specifier.startsWith('.')) {
|
|
478
|
+
// A bare specifier is resolved the way the project declares it: a path alias first, then `baseUrl`.
|
|
479
|
+
// That is also the only way a bare specifier can be project-internal, so the same helper decides
|
|
480
|
+
// whether one is an edge or a package.
|
|
481
|
+
const declared = resolveDeclared(language, specifier, index);
|
|
482
|
+
if (declared !== undefined)
|
|
483
|
+
return declared;
|
|
484
|
+
}
|
|
485
|
+
// A C/C++ include that is not beside the importing file is usually found through an include directory
|
|
486
|
+
// the build already knows about, so `compile_commands.json` is read rather than asked for.
|
|
487
|
+
if (language.name === 'c' && !specifier.startsWith('.')) {
|
|
488
|
+
for (const directory of index.includeDirs) {
|
|
489
|
+
const hit = tryFiles(normalise([directory, specifier]), language.extensions, index);
|
|
490
|
+
if (hit !== undefined)
|
|
491
|
+
return hit;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
421
494
|
const base = joinPath(fromId, specifier);
|
|
422
495
|
const extension = extname(base);
|
|
423
496
|
const stem = extension === '' ? base : base.slice(0, -extension.length);
|
|
@@ -429,6 +502,149 @@ function resolveSpecifier(language, fromId, specifier, index) {
|
|
|
429
502
|
return candidates.find((candidate) => index.files.has(candidate));
|
|
430
503
|
}
|
|
431
504
|
// ------------------------------------------------------------------------------------------------ scan --
|
|
505
|
+
/**
|
|
506
|
+
* A bare specifier that the project itself declares: a `paths` alias, or a file under `baseUrl`.
|
|
507
|
+
*
|
|
508
|
+
* `@/lib/a` is not a package, and counting it as one dropped the edge without saying so. Returns
|
|
509
|
+
* undefined when nothing is declared for it, which leaves the caller free to treat it as external.
|
|
510
|
+
*/
|
|
511
|
+
function resolveDeclared(language, specifier, index) {
|
|
512
|
+
// A workspace package is the project too: `import { x } from '@acme/ui'` in a monorepo is an edge, not
|
|
513
|
+
// an external dependency. The name comes from the package's own manifest.
|
|
514
|
+
for (const [name, directory] of index.workspaces) {
|
|
515
|
+
if (specifier !== name && !specifier.startsWith(name + '/'))
|
|
516
|
+
continue;
|
|
517
|
+
const rest = specifier === name ? '' : specifier.slice(name.length + 1);
|
|
518
|
+
const hit = tryFiles(normalise([directory, rest]), language.extensions, index)
|
|
519
|
+
?? tryFiles(normalise([directory]), language.extensions, index)
|
|
520
|
+
?? tryFiles(normalise([directory, 'src']), language.extensions, index);
|
|
521
|
+
if (hit !== undefined)
|
|
522
|
+
return hit;
|
|
523
|
+
}
|
|
524
|
+
for (const alias of index.aliases) {
|
|
525
|
+
if (!specifier.startsWith(alias.prefix))
|
|
526
|
+
continue;
|
|
527
|
+
const rest = specifier.slice(alias.prefix.length);
|
|
528
|
+
for (const target of alias.targets) {
|
|
529
|
+
const hit = tryFiles(normalise([target + rest]), language.extensions, index);
|
|
530
|
+
if (hit !== undefined)
|
|
531
|
+
return hit;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (index.baseUrl !== undefined) {
|
|
535
|
+
return tryFiles(normalise([index.baseUrl, specifier]), language.extensions, index);
|
|
536
|
+
}
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Does this package-language specifier name something this tree owns?
|
|
541
|
+
*
|
|
542
|
+
* `com.acme.b.B` in a tree whose top level is `com/`, and `example.com/proj/pkg/b` in a module whose
|
|
543
|
+
* `go.mod` says `module example.com/proj`, are both internal: if they do not resolve, the reference is
|
|
544
|
+
* broken. `java.util.List` and `lodash` are not.
|
|
545
|
+
*/
|
|
546
|
+
function isInternalSpecifier(specifier, roots) {
|
|
547
|
+
const head = specifier.replaceAll('\\', '/').split(/[/.]/).filter(Boolean)[0];
|
|
548
|
+
if (head === undefined)
|
|
549
|
+
return false;
|
|
550
|
+
return roots.some((root) => root === head || specifier.startsWith(root + '/') || specifier.startsWith(root + '.'));
|
|
551
|
+
}
|
|
552
|
+
/** Workspace packages declared by the root manifest, so a monorepo's own names resolve inside the tree. */
|
|
553
|
+
function readWorkspaces(root) {
|
|
554
|
+
const names = new Map();
|
|
555
|
+
let manifest;
|
|
556
|
+
try {
|
|
557
|
+
manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'));
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
return names;
|
|
561
|
+
}
|
|
562
|
+
if (typeof manifest !== 'object' || manifest === null)
|
|
563
|
+
return names;
|
|
564
|
+
const declared = manifest.workspaces;
|
|
565
|
+
const patterns = Array.isArray(declared)
|
|
566
|
+
? declared
|
|
567
|
+
: typeof declared === 'object' && declared !== null && Array.isArray(declared.packages)
|
|
568
|
+
? (declared.packages)
|
|
569
|
+
: [];
|
|
570
|
+
for (const pattern of patterns) {
|
|
571
|
+
if (typeof pattern !== 'string' || pattern.includes('*'))
|
|
572
|
+
continue;
|
|
573
|
+
const directory = pattern.replace(/^\.\//, '').replace(/\/$/, '');
|
|
574
|
+
try {
|
|
575
|
+
const entry = JSON.parse(readFileSync(resolve(root, directory, 'package.json'), 'utf8'));
|
|
576
|
+
if (typeof entry.name === 'string' && entry.name !== '')
|
|
577
|
+
names.set(entry.name, directory);
|
|
578
|
+
}
|
|
579
|
+
catch {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return names;
|
|
584
|
+
}
|
|
585
|
+
/** `-I`/`-isystem` directories from `compile_commands.json`, kept inside the scanned tree. */
|
|
586
|
+
function readIncludeDirs(root) {
|
|
587
|
+
let entries;
|
|
588
|
+
try {
|
|
589
|
+
entries = JSON.parse(readFileSync(resolve(root, 'compile_commands.json'), 'utf8'));
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
return [];
|
|
593
|
+
}
|
|
594
|
+
if (!Array.isArray(entries))
|
|
595
|
+
return [];
|
|
596
|
+
const dirs = new Set();
|
|
597
|
+
for (const entry of entries) {
|
|
598
|
+
if (typeof entry !== 'object' || entry === null)
|
|
599
|
+
continue;
|
|
600
|
+
const record = entry;
|
|
601
|
+
const declared = typeof record.directory === 'string' ? record.directory.replaceAll(String.fromCharCode(92), '/') : '.';
|
|
602
|
+
const base = declared.startsWith('/') || /^[A-Za-z]:/.test(declared) ? declared : resolve(root, declared).replaceAll(String.fromCharCode(92), '/');
|
|
603
|
+
const args = Array.isArray(record.arguments)
|
|
604
|
+
? record.arguments.filter((argument) => typeof argument === 'string')
|
|
605
|
+
: typeof record.command === 'string'
|
|
606
|
+
? record.command.split(/\s+/)
|
|
607
|
+
: [];
|
|
608
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
609
|
+
const argument = args[index];
|
|
610
|
+
let value;
|
|
611
|
+
if (argument === '-I' || argument === '-isystem')
|
|
612
|
+
value = args[index + 1];
|
|
613
|
+
else if (argument.startsWith('-I'))
|
|
614
|
+
value = argument.slice(2);
|
|
615
|
+
else if (argument.startsWith('-isystem'))
|
|
616
|
+
value = argument.slice(8);
|
|
617
|
+
if (value === undefined || value === '')
|
|
618
|
+
continue;
|
|
619
|
+
const absolute = value.startsWith('/') || /^[A-Za-z]:/.test(value) ? value : base + '/' + value;
|
|
620
|
+
const local = relative(root, absolute).replaceAll(String.fromCharCode(92), '/');
|
|
621
|
+
if (local.startsWith('..'))
|
|
622
|
+
continue;
|
|
623
|
+
dirs.add(local === '' ? '.' : local);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return [...dirs].sort();
|
|
627
|
+
}
|
|
628
|
+
/** The package roots this tree owns: `module` from `go.mod`, and the top-level names on disk. */
|
|
629
|
+
function internalRootsOf(root, directories) {
|
|
630
|
+
const roots = new Set();
|
|
631
|
+
for (const directory of directories) {
|
|
632
|
+
const head = directory.split('/')[0];
|
|
633
|
+
if (head !== undefined && head !== '')
|
|
634
|
+
roots.add(head);
|
|
635
|
+
}
|
|
636
|
+
try {
|
|
637
|
+
for (const line of readFileSync(resolve(root, 'go.mod'), 'utf8').split(String.fromCharCode(10)).map((entry) => entry.trim())) {
|
|
638
|
+
const match = /^module\s+(\S+)/.exec(line);
|
|
639
|
+
if (match?.[1] !== undefined)
|
|
640
|
+
roots.add(match[1]);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
// No go.mod: the top-level names are the whole set.
|
|
645
|
+
}
|
|
646
|
+
return [...roots].sort();
|
|
647
|
+
}
|
|
432
648
|
export function scanDependencyGraph(root) {
|
|
433
649
|
if (typeof root !== 'string' || root.trim() === '') {
|
|
434
650
|
throw new OptionShapeError('STRUCTURE_OPTION_INVALID', 'repoPath must be a non-empty path to a directory or a single source file.');
|
|
@@ -444,7 +660,7 @@ export function scanDependencyGraph(root) {
|
|
|
444
660
|
}
|
|
445
661
|
if (stat.isFile()) {
|
|
446
662
|
const id = relative(resolve('/'), absolute).slice(1).replaceAll('\\', '/');
|
|
447
|
-
return { nodes: [id], edges: [], filesRead: 1, unresolved: [], packages: [], languages: [] };
|
|
663
|
+
return { nodes: [id], edges: [], filesRead: 1, unresolved: [], packages: [], dynamic: 0, languages: [] };
|
|
448
664
|
}
|
|
449
665
|
if (!stat.isDirectory()) {
|
|
450
666
|
throw new OptionShapeError('STRUCTURE_OPTION_INVALID', 'repoPath must be a directory or a source file.');
|
|
@@ -494,24 +710,35 @@ export function scanDependencyGraph(root) {
|
|
|
494
710
|
const byDotted = new Map();
|
|
495
711
|
for (const directory of directories)
|
|
496
712
|
byDotted.set(directory.replaceAll('/', '.'), directory);
|
|
497
|
-
const index = {
|
|
713
|
+
const index = {
|
|
714
|
+
files: fileSet,
|
|
715
|
+
directories,
|
|
716
|
+
byDotted,
|
|
717
|
+
...readAliases(absolute),
|
|
718
|
+
workspaces: readWorkspaces(absolute),
|
|
719
|
+
includeDirs: readIncludeDirs(absolute),
|
|
720
|
+
internalRoots: internalRootsOf(absolute, directories),
|
|
721
|
+
};
|
|
498
722
|
const edges = [];
|
|
499
723
|
const seen = new Set();
|
|
500
724
|
const unresolved = [];
|
|
501
725
|
const packages = new Set();
|
|
502
726
|
const nodeSet = new Set();
|
|
727
|
+
let dynamic = 0;
|
|
503
728
|
for (const id of files) {
|
|
504
729
|
const language = languageOf(id);
|
|
505
|
-
const { local, external } = language.extract(contents.get(id) ?? '');
|
|
730
|
+
const { local, external, dynamic: dynamicHere } = language.extract(contents.get(id) ?? '');
|
|
731
|
+
dynamic += dynamicHere ?? 0;
|
|
506
732
|
const nodeOf = (fileId) => (language.granularity === 'package' ? (dirOf(fileId) === '' ? '.' : dirOf(fileId)) : fileId);
|
|
507
733
|
const from = nodeOf(id);
|
|
508
734
|
nodeSet.add(from);
|
|
509
735
|
for (const specifier of local) {
|
|
510
736
|
const target = resolveSpecifier(language, id, specifier, index);
|
|
511
737
|
if (target === undefined) {
|
|
512
|
-
// A package language names modules by a path this tree may not contain.
|
|
513
|
-
//
|
|
514
|
-
|
|
738
|
+
// A package language names modules by a path this tree may or may not contain. When the path
|
|
739
|
+
// starts at a root this tree owns - the `go.mod` module, a top-level package directory - it is a
|
|
740
|
+
// broken reference and has to be reported; anything else is an external dependency.
|
|
741
|
+
if (language.granularity === 'package' && !isInternalSpecifier(specifier, index.internalRoots)) {
|
|
515
742
|
const name = specifier.split(/[/.]/).filter(Boolean)[0];
|
|
516
743
|
if (name !== undefined)
|
|
517
744
|
packages.add(name);
|
|
@@ -533,6 +760,19 @@ export function scanDependencyGraph(root) {
|
|
|
533
760
|
edges.push({ from, to });
|
|
534
761
|
}
|
|
535
762
|
for (const specifier of external) {
|
|
763
|
+
const internal = language.granularity === 'file' ? resolveDeclared(language, specifier, index) : undefined;
|
|
764
|
+
if (internal !== undefined) {
|
|
765
|
+
const target = nodeOf(internal);
|
|
766
|
+
nodeSet.add(target);
|
|
767
|
+
if (target !== from) {
|
|
768
|
+
const key = from + '' + target;
|
|
769
|
+
if (!seen.has(key)) {
|
|
770
|
+
seen.add(key);
|
|
771
|
+
edges.push({ from, to: target });
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
536
776
|
const name = specifier.startsWith('@') ? specifier.split('/').slice(0, 2).join('/') : specifier.split(/[/.:]/)[0];
|
|
537
777
|
if (name !== undefined && name !== '')
|
|
538
778
|
packages.add(name);
|
|
@@ -543,5 +783,5 @@ export function scanDependencyGraph(root) {
|
|
|
543
783
|
.sort((a, b) => b.files - a.files);
|
|
544
784
|
edges.sort((a, b) => (a.from === b.from ? a.to.localeCompare(b.to) : a.from.localeCompare(b.to)));
|
|
545
785
|
unresolved.sort();
|
|
546
|
-
return { nodes: [...nodeSet].sort(), edges, filesRead: files.length, unresolved, packages: [...packages].sort(), languages };
|
|
786
|
+
return { nodes: [...nodeSet].sort(), edges, filesRead: files.length, unresolved, packages: [...packages].sort(), dynamic, languages };
|
|
547
787
|
}
|
package/lib/structure-tool.js
CHANGED
|
@@ -34,7 +34,7 @@ export const umlStructureTool = defineTool({
|
|
|
34
34
|
},
|
|
35
35
|
repoPath: {
|
|
36
36
|
type: 'string',
|
|
37
|
-
description: 'Directory or single source file to scan instead of drawing the diagram yourself. Reads the import construct of C/C++, Python, TypeScript/JavaScript, Go, Rust, Java/Kotlin/Scala, C#, PHP, Ruby, Lua, Zig, Dart and Elixir, building a file graph for file languages and a package graph for package languages (Go, Java, C#). The graph is reviewed by the same checks; `diagram` and `diagrams` are then ignored.',
|
|
37
|
+
description: 'Directory or single source file to scan instead of drawing the diagram yourself. Reads the import construct of C/C++, Python, TypeScript/JavaScript, Go, Rust, Java/Kotlin/Scala, C#, PHP, Ruby, Lua, Zig, Dart and Elixir, building a file graph for file languages and a package graph for package languages (Go, Java, C#). Path aliases come from the project own `tsconfig.json`/`jsconfig.json` (`baseUrl` and `paths`), a monorepo package name from the root `package.json` workspaces, and C include directories from `compile_commands.json`. A project-internal specifier that resolves to nothing is listed as unresolved rather than counted as an external package, and an import whose target is computed at run time is counted rather than guessed. The graph is reviewed by the same checks; `diagram` and `diagrams` are then ignored.',
|
|
38
38
|
},
|
|
39
39
|
matrix: {
|
|
40
40
|
type: 'json',
|
|
@@ -105,7 +105,7 @@ export const umlStructureTool = defineTool({
|
|
|
105
105
|
return reviewStructure({
|
|
106
106
|
diagram: '',
|
|
107
107
|
graph,
|
|
108
|
-
scan: { files: scanned.filesRead, unresolved: scanned.unresolved.length, packages: scanned.packages.length, languages: scanned.languages.map((entry) => entry.language) },
|
|
108
|
+
scan: { files: scanned.filesRead, unresolved: scanned.unresolved.length, packages: scanned.packages.length, dynamic: scanned.dynamic, languages: scanned.languages.map((entry) => entry.language) },
|
|
109
109
|
...(args.matrix === undefined ? {} : { matrix: args.matrix }),
|
|
110
110
|
});
|
|
111
111
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Client half: a lazy factory the page binds to this package's name.
|
|
3
|
+
*
|
|
4
|
+
* Everything that decides anything lives in `render/`; this file only turns shapes into React elements and
|
|
5
|
+
* registers them where the page offers a seat. Two seats are used, and they fail independently:
|
|
6
|
+
*
|
|
7
|
+
* - `tool.call.toolview` replaces the card of a tool whose result this package can draw;
|
|
8
|
+
* - `sidebar.right.pane.tab` is the whiteboard: a tab that renders whatever the reader pastes, with no tool
|
|
9
|
+
* and no host data at all, so it keeps working when the card seat does not.
|
|
10
|
+
*
|
|
11
|
+
* This module is not a page script: it exports the factory, and `scripts/build-client.mjs` wraps the
|
|
12
|
+
* compiled output in `window.__ModuleLoader__.load(...)`, which is the loader contract itself.
|
|
13
|
+
*/
|
|
14
|
+
import { type Palette, type PaletteMode } from './render/palette.js';
|
|
15
|
+
export interface ReactLike {
|
|
16
|
+
createElement: (type: unknown, props?: Record<string, unknown> | null, ...children: unknown[]) => unknown;
|
|
17
|
+
useState?: <T>(initial: T) => [T, (next: T) => void];
|
|
18
|
+
}
|
|
19
|
+
export interface SlotContext {
|
|
20
|
+
slots: {
|
|
21
|
+
inject: (slot: string, callback: () => unknown) => unknown;
|
|
22
|
+
register: (options: Record<string, unknown>, component: unknown) => unknown;
|
|
23
|
+
};
|
|
24
|
+
get?: (service: string) => unknown;
|
|
25
|
+
}
|
|
26
|
+
export interface PluginShape {
|
|
27
|
+
inject: string[];
|
|
28
|
+
apply: (ctx: SlotContext) => void;
|
|
29
|
+
/** The whiteboard mount, with its scheduler injectable. `apply` calls this with the default. */
|
|
30
|
+
mount: (ctx: SlotContext, options?: MountOptions) => unknown;
|
|
31
|
+
}
|
|
32
|
+
/** A settled tool call, as the host hands it to a view. */
|
|
33
|
+
export interface ToolCallProps {
|
|
34
|
+
phase?: string;
|
|
35
|
+
toolName?: string;
|
|
36
|
+
block?: {
|
|
37
|
+
args?: unknown;
|
|
38
|
+
result?: unknown;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Tool names this package answers. A key that never dispatches simply never renders. */
|
|
42
|
+
export declare const TOOL_NAMES: string[];
|
|
43
|
+
/**
|
|
44
|
+
* Which surface the page is painting the drawing on.
|
|
45
|
+
*
|
|
46
|
+
* The host publishes its palette as private CSS variables, so naming them here would break on the next theme.
|
|
47
|
+
* The one fact that cannot drift is the rendered background: read it, and fall back to the media query, then
|
|
48
|
+
* to light. Never throws - a view that cannot read the document still draws.
|
|
49
|
+
*/
|
|
50
|
+
export declare function themeMode(): PaletteMode;
|
|
51
|
+
/** The whiteboard's seat key, in both the tab registry and the body slot. */
|
|
52
|
+
export declare const WHITEBOARD_ID = "model-everything-diagram";
|
|
53
|
+
/** What the whiteboard opens with: a diagram the reader can edit rather than an empty box. */
|
|
54
|
+
export declare const WHITEBOARD_SAMPLE: string;
|
|
55
|
+
/**
|
|
56
|
+
* The view for one tool name.
|
|
57
|
+
*
|
|
58
|
+
* The drawing is emitted as markup, so React receives it through `dangerouslySetInnerHTML`. That is safe
|
|
59
|
+
* only because the writer escapes every value that came from a report or a model, which the render suite
|
|
60
|
+
* pins with an injection case.
|
|
61
|
+
*/
|
|
62
|
+
export declare function viewFor(React: ReactLike, overrides?: Partial<Palette>): (props: ToolCallProps) => unknown;
|
|
63
|
+
/**
|
|
64
|
+
* The whiteboard: paste a diagram, get a drawing, zoom it.
|
|
65
|
+
*
|
|
66
|
+
* It holds no host data, which is the point: it is a place to think about a diagram while writing one, and
|
|
67
|
+
* it cannot be broken by a change to any tool's result shape. The source it draws is whatever is in its own
|
|
68
|
+
* box.
|
|
69
|
+
*/
|
|
70
|
+
export declare function whiteboardFor(React: ReactLike, overrides?: Partial<Palette>): () => unknown;
|
|
71
|
+
export interface MountOptions {
|
|
72
|
+
/** Injected so the suite can drive the poll without waiting. Defaults to `setTimeout`. */
|
|
73
|
+
schedule?: (run: () => void, delay: number) => unknown;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Register the whiteboard tab once the sidebar's optional services are ready.
|
|
77
|
+
*
|
|
78
|
+
* The slot can be declared before `sidebarRightTabs` exists - the host composes services and seats in an
|
|
79
|
+
* order this plugin does not control - and a single `ctx.get` there is a race that loses the tab silently.
|
|
80
|
+
* So it polls, like the reference implementation does, and stops after a bounded number of attempts.
|
|
81
|
+
*/
|
|
82
|
+
export declare function mountWhiteboard(React: ReactLike, ctx: SlotContext, options?: MountOptions): unknown;
|
|
83
|
+
export default function factory(require: (name: string) => unknown): PluginShape;
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
export declare const DIAGRAM_DIFF_TOOL_NAME = "diagram_diff";
|
|
8
|
+
export declare const diagramDiffTool: import("@deepseek-ai/dsh-tools").ToolDefinition;
|