@sleetdrop/dsh-plugin-topology 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NEXT-STEPS.md +32 -0
- package/README.md +120 -0
- package/cordis.patch.yml +17 -0
- package/lib/client/TopologyGraphView.d.ts +27 -0
- package/lib/client/TopologyGraphView.js +246 -0
- package/lib/client/TopologyView.d.ts +26 -0
- package/lib/client/TopologyView.js +96 -0
- package/lib/client/icons.d.ts +34 -0
- package/lib/client/icons.js +37 -0
- package/lib/client/index.d.ts +29 -0
- package/lib/client/index.js +76 -0
- package/lib/client/locales.d.ts +4 -0
- package/lib/client/locales.js +55 -0
- package/lib/client/panel.d.ts +19 -0
- package/lib/client/panel.js +41 -0
- package/lib/client/remote-client.d.ts +12 -0
- package/lib/client/remote-client.js +148 -0
- package/lib/client/stores.d.ts +25 -0
- package/lib/client/stores.js +15 -0
- package/lib/client/useDismissOnOutsidePointer.d.ts +9 -0
- package/lib/client/useDismissOnOutsidePointer.js +23 -0
- package/lib/client.js +5566 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +54 -0
- package/lib/index.js +198 -0
- package/lib/invariant.d.ts +15 -0
- package/lib/invariant.js +17 -0
- package/lib/metrics.d.ts +26 -0
- package/lib/metrics.js +331 -0
- package/lib/render.d.ts +72 -0
- package/lib/render.js +262 -0
- package/lib/tool.d.ts +14 -0
- package/lib/tool.js +57 -0
- package/lib/typert.d.ts +139 -0
- package/lib/typert.js +142 -0
- package/lib/types.d.ts +126 -0
- package/lib/types.js +5 -0
- package/overlay.example.yml +19 -0
- package/package.json +114 -0
package/lib/render.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export renderers for the plugin topology analysis.
|
|
3
|
+
*
|
|
4
|
+
* `nodeLink` merges the collapsed graph and its metrics into a
|
|
5
|
+
* NetworkX-compatible node-link document; `renderJson` and `renderDot`
|
|
6
|
+
* serialize it for graph tools and Graphviz. The Graphviz SVG rendering is
|
|
7
|
+
* owned by the service (it needs the async WASM `dot` layout), which calls
|
|
8
|
+
* `renderDot` for its source.
|
|
9
|
+
*
|
|
10
|
+
* @module @sleetdrop/dsh-plugin-topology/render
|
|
11
|
+
*/
|
|
12
|
+
import type { GraphMetrics, PluginNode, PluginState, Rankdir, TopologyAnalysis } from './types.ts';
|
|
13
|
+
/** One plugin node with its metrics in the node-link document. */
|
|
14
|
+
export interface NodeLinkNode {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly label: string;
|
|
17
|
+
readonly state: PluginState;
|
|
18
|
+
/** Fiber-tree parent node id; `null` for the root. */
|
|
19
|
+
readonly parent: string | null;
|
|
20
|
+
readonly inDegree: number;
|
|
21
|
+
readonly outDegree: number;
|
|
22
|
+
readonly degreeCentrality: number;
|
|
23
|
+
readonly betweennessCentrality: number;
|
|
24
|
+
readonly eigenvectorCentrality: number;
|
|
25
|
+
readonly pagerank: number;
|
|
26
|
+
}
|
|
27
|
+
/** One dependency edge in the node-link document. */
|
|
28
|
+
export interface NodeLinkEdge {
|
|
29
|
+
readonly source: string;
|
|
30
|
+
readonly target: string;
|
|
31
|
+
readonly kind: 'depends-on';
|
|
32
|
+
readonly service: string;
|
|
33
|
+
}
|
|
34
|
+
/** NetworkX `node_link_graph`-compatible export document. */
|
|
35
|
+
export interface NodeLinkGraph {
|
|
36
|
+
readonly kind: 'node-link';
|
|
37
|
+
readonly generatedAt: string;
|
|
38
|
+
readonly nodes: NodeLinkNode[];
|
|
39
|
+
readonly edges: NodeLinkEdge[];
|
|
40
|
+
readonly graph: GraphMetrics;
|
|
41
|
+
}
|
|
42
|
+
/** Merge the collapsed graph and its metrics into a node-link document. */
|
|
43
|
+
export declare function nodeLink(analysis: TopologyAnalysis): NodeLinkGraph;
|
|
44
|
+
/** Serialize the node-link document as indented JSON. */
|
|
45
|
+
export declare function renderJson(analysis: TopologyAnalysis): string;
|
|
46
|
+
/**
|
|
47
|
+
* Serialize the collapsed graph as Graphviz DOT source.
|
|
48
|
+
*
|
|
49
|
+
* Every plugin label carries its short id in brackets (same-named plugins stay
|
|
50
|
+
* distinguishable). The SVG renders only the connected ("live") plugins; plugins
|
|
51
|
+
* with no dependency edges are surfaced by the client as a side list so they
|
|
52
|
+
* never stretch the layout. Plugins with an unresolved dependency get a red
|
|
53
|
+
* stroke.
|
|
54
|
+
*/
|
|
55
|
+
export declare function renderDot(analysis: TopologyAnalysis, rankdir: Rankdir): string;
|
|
56
|
+
/** Plugins with no incident dependency edge (and not the synthetic root). */
|
|
57
|
+
export declare function isolatedNodes(analysis: TopologyAnalysis): readonly PluginNode[];
|
|
58
|
+
/** Serialize the isolated plugins as a standalone dashed-cluster DOT graph. */
|
|
59
|
+
export declare function renderIsolatedDot(analysis: TopologyAnalysis, rankdir: Rankdir): string;
|
|
60
|
+
/**
|
|
61
|
+
* The complete DOT source: every plugin instance (connected and isolated) with
|
|
62
|
+
* all dependency edges, isolated ones listed in a dashed cluster. Aligned with
|
|
63
|
+
* the JSON export; used for the downloadable DOT. The SVG split (main/isolated)
|
|
64
|
+
* is an internal layout tradeoff and is not exposed here.
|
|
65
|
+
*/
|
|
66
|
+
export declare function renderCompleteDot(analysis: TopologyAnalysis, rankdir: Rankdir): string;
|
|
67
|
+
/**
|
|
68
|
+
* Compose two Graphviz SVGs into one document. The isolated graph is placed to
|
|
69
|
+
* the side (LR) or below (TB) of the main graph with a fixed gap, and centered
|
|
70
|
+
* along the graph's perpendicular axis, so the exported SVG is one image.
|
|
71
|
+
*/
|
|
72
|
+
export declare function composeGraphvizSvgs(main: string, isolated: string, rankdir: Rankdir): string;
|
package/lib/render.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export renderers for the plugin topology analysis.
|
|
3
|
+
*
|
|
4
|
+
* `nodeLink` merges the collapsed graph and its metrics into a
|
|
5
|
+
* NetworkX-compatible node-link document; `renderJson` and `renderDot`
|
|
6
|
+
* serialize it for graph tools and Graphviz. The Graphviz SVG rendering is
|
|
7
|
+
* owned by the service (it needs the async WASM `dot` layout), which calls
|
|
8
|
+
* `renderDot` for its source.
|
|
9
|
+
*
|
|
10
|
+
* @module @sleetdrop/dsh-plugin-topology/render
|
|
11
|
+
*/
|
|
12
|
+
/** Merge the collapsed graph and its metrics into a node-link document. */
|
|
13
|
+
export function nodeLink(analysis) {
|
|
14
|
+
const nodes = analysis.collapsed.nodes.map((node) => {
|
|
15
|
+
const metrics = analysis.nodeMetrics[node.id];
|
|
16
|
+
return {
|
|
17
|
+
id: node.id,
|
|
18
|
+
label: node.label,
|
|
19
|
+
state: node.state,
|
|
20
|
+
parent: node.parent,
|
|
21
|
+
inDegree: metrics?.inDegree ?? 0,
|
|
22
|
+
outDegree: metrics?.outDegree ?? 0,
|
|
23
|
+
degreeCentrality: metrics?.degreeCentrality ?? 0,
|
|
24
|
+
betweennessCentrality: metrics?.betweennessCentrality ?? 0,
|
|
25
|
+
eigenvectorCentrality: metrics?.eigenvectorCentrality ?? 0,
|
|
26
|
+
pagerank: metrics?.pagerank ?? 0,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
const edges = analysis.collapsed.edges.map(edge => ({
|
|
30
|
+
source: edge.source,
|
|
31
|
+
target: edge.target,
|
|
32
|
+
kind: 'depends-on',
|
|
33
|
+
service: edge.service,
|
|
34
|
+
}));
|
|
35
|
+
return {
|
|
36
|
+
kind: 'node-link',
|
|
37
|
+
generatedAt: analysis.collapsed.generatedAt,
|
|
38
|
+
nodes,
|
|
39
|
+
edges,
|
|
40
|
+
graph: analysis.graphMetrics,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Serialize the node-link document as indented JSON. */
|
|
44
|
+
export function renderJson(analysis) {
|
|
45
|
+
return `${JSON.stringify(nodeLink(analysis), null, 2)}\n`;
|
|
46
|
+
}
|
|
47
|
+
/** Escape a string for a DOT quoted attribute. */
|
|
48
|
+
function escapeDot(value) {
|
|
49
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Serialize the collapsed graph as Graphviz DOT source.
|
|
53
|
+
*
|
|
54
|
+
* Every plugin label carries its short id in brackets (same-named plugins stay
|
|
55
|
+
* distinguishable). The SVG renders only the connected ("live") plugins; plugins
|
|
56
|
+
* with no dependency edges are surfaced by the client as a side list so they
|
|
57
|
+
* never stretch the layout. Plugins with an unresolved dependency get a red
|
|
58
|
+
* stroke.
|
|
59
|
+
*/
|
|
60
|
+
export function renderDot(analysis, rankdir) {
|
|
61
|
+
const unresolvedIds = new Set(analysis.graph.unresolved.map(dep => dep.plugin));
|
|
62
|
+
const nodes = analysis.collapsed.nodes;
|
|
63
|
+
const ROOT_ID = 'p:0';
|
|
64
|
+
// Degree in the plugin-to-plugin projection; degree-0 (and not root) plugins
|
|
65
|
+
// are isolated from the dependency network and rendered as a separate graph.
|
|
66
|
+
const degree = new Map();
|
|
67
|
+
for (const edge of analysis.collapsed.edges) {
|
|
68
|
+
degree.set(edge.source, (degree.get(edge.source) ?? 0) + 1);
|
|
69
|
+
degree.set(edge.target, (degree.get(edge.target) ?? 0) + 1);
|
|
70
|
+
}
|
|
71
|
+
const live = nodes.filter(node => node.id === ROOT_ID || (degree.get(node.id) ?? 0) > 0);
|
|
72
|
+
const merged = mergeByName(live, unresolvedIds);
|
|
73
|
+
const lines = [
|
|
74
|
+
'digraph plugin_topology {',
|
|
75
|
+
` rankdir=${rankdir};`,
|
|
76
|
+
' nodesep=0.3;',
|
|
77
|
+
' ranksep=0.7;',
|
|
78
|
+
' splines=true;',
|
|
79
|
+
' node [shape=oval, style="filled", fillcolor="#ffffff", color="#cbd5e1", fontcolor="#64748b", penwidth=0.8, fontname="Helvetica", fontsize=8, margin="0.16,0.11"];',
|
|
80
|
+
' edge [color="#cbd5e1", penwidth=0.8, arrowsize=0.55, fontname="Helvetica", fontsize=7, fontcolor="#94a3b8"];',
|
|
81
|
+
];
|
|
82
|
+
for (const node of merged.nodes) {
|
|
83
|
+
const stroke = node.unresolved ? ', color="#dc2626", fontcolor="#dc2626", penwidth=1.5' : '';
|
|
84
|
+
lines.push(` "${node.id}" [label="${mergedLabel(node)}"${stroke}];`);
|
|
85
|
+
}
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
for (const edge of analysis.collapsed.edges) {
|
|
88
|
+
const source = merged.byOriginalId.get(edge.source) ?? edge.source;
|
|
89
|
+
const target = merged.byOriginalId.get(edge.target) ?? edge.target;
|
|
90
|
+
const key = `${source}\u0000${target}\u0000${edge.service}`;
|
|
91
|
+
if (seen.has(key))
|
|
92
|
+
continue;
|
|
93
|
+
seen.add(key);
|
|
94
|
+
lines.push(` "${source}" -> "${target}" [label="${escapeDot(edge.service)}"];`);
|
|
95
|
+
}
|
|
96
|
+
lines.push('}');
|
|
97
|
+
return `${lines.join('\n')}\n`;
|
|
98
|
+
}
|
|
99
|
+
/** Group plugins by display name so same-named instances become one node. */
|
|
100
|
+
function mergeByName(nodes, unresolvedIds) {
|
|
101
|
+
const groups = new Map();
|
|
102
|
+
for (const node of nodes) {
|
|
103
|
+
let group = groups.get(node.label);
|
|
104
|
+
if (group === undefined) {
|
|
105
|
+
group = [];
|
|
106
|
+
groups.set(node.label, group);
|
|
107
|
+
}
|
|
108
|
+
group.push(node);
|
|
109
|
+
}
|
|
110
|
+
const out = [];
|
|
111
|
+
const byOriginalId = new Map();
|
|
112
|
+
let groupIndex = 0;
|
|
113
|
+
for (const [label, group] of groups) {
|
|
114
|
+
const ids = group.map(node => node.id.replace(/^p:/, ''));
|
|
115
|
+
if (group.length === 1) {
|
|
116
|
+
const single = group[0];
|
|
117
|
+
if (single === undefined)
|
|
118
|
+
continue;
|
|
119
|
+
byOriginalId.set(single.id, single.id);
|
|
120
|
+
out.push({ id: single.id, label, ids, unresolved: unresolvedIds.has(single.id) });
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const id = `m:${groupIndex++}`;
|
|
124
|
+
for (const node of group)
|
|
125
|
+
byOriginalId.set(node.id, id);
|
|
126
|
+
out.push({ id, label, ids, unresolved: group.some(node => unresolvedIds.has(node.id)) });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { nodes: out, byOriginalId };
|
|
130
|
+
}
|
|
131
|
+
/** Display label for a (possibly merged) node: `name [1,9,23]`, capped for many ids. */
|
|
132
|
+
function mergedLabel(node) {
|
|
133
|
+
const ids = node.ids.length <= 6 ? node.ids.join(',') : `${node.ids.slice(0, 6).join(',')},…`;
|
|
134
|
+
return `${escapeDot(node.label)} [${ids}]`;
|
|
135
|
+
}
|
|
136
|
+
/** Plugins with no incident dependency edge (and not the synthetic root). */
|
|
137
|
+
export function isolatedNodes(analysis) {
|
|
138
|
+
const connected = new Set();
|
|
139
|
+
for (const edge of analysis.collapsed.edges) {
|
|
140
|
+
connected.add(edge.source);
|
|
141
|
+
connected.add(edge.target);
|
|
142
|
+
}
|
|
143
|
+
return analysis.collapsed.nodes.filter(node => node.id !== 'p:0' && !connected.has(node.id));
|
|
144
|
+
}
|
|
145
|
+
/** Serialize the isolated plugins as a standalone dashed-cluster DOT graph. */
|
|
146
|
+
export function renderIsolatedDot(analysis, rankdir) {
|
|
147
|
+
const isolated = isolatedNodes(analysis);
|
|
148
|
+
const merged = mergeByName(isolated, new Set());
|
|
149
|
+
const lines = [
|
|
150
|
+
'digraph plugin_topology_isolated {',
|
|
151
|
+
` rankdir=${rankdir};`,
|
|
152
|
+
' nodesep=0.18;',
|
|
153
|
+
' ranksep=0.4;',
|
|
154
|
+
' node [shape=oval, style="filled", fillcolor="#ffffff", color="#cbd5e1", fontcolor="#64748b", penwidth=0.8, fontname="Helvetica", fontsize=8, margin="0.16,0.11"];',
|
|
155
|
+
' subgraph "cluster_isolated" {',
|
|
156
|
+
' label="isolated";',
|
|
157
|
+
' style="rounded,dashed"; fillcolor="#f8fafc"; color="#94a3b8"; fontsize=9; fontcolor="#64748b";',
|
|
158
|
+
' rank=same;',
|
|
159
|
+
];
|
|
160
|
+
for (const node of merged.nodes) {
|
|
161
|
+
lines.push(` "${node.id}" [label="${mergedLabel(node)}"];`);
|
|
162
|
+
}
|
|
163
|
+
lines.push(' }');
|
|
164
|
+
lines.push('}');
|
|
165
|
+
return `${lines.join('\n')}\n`;
|
|
166
|
+
}
|
|
167
|
+
/** Per-instance label: `name [id]` (no merging — used by the complete DOT). */
|
|
168
|
+
function instanceLabel(node) {
|
|
169
|
+
return `${escapeDot(node.label)} [${escapeDot(node.id.replace(/^p:/, ''))}]`;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The complete DOT source: every plugin instance (connected and isolated) with
|
|
173
|
+
* all dependency edges, isolated ones listed in a dashed cluster. Aligned with
|
|
174
|
+
* the JSON export; used for the downloadable DOT. The SVG split (main/isolated)
|
|
175
|
+
* is an internal layout tradeoff and is not exposed here.
|
|
176
|
+
*/
|
|
177
|
+
export function renderCompleteDot(analysis, rankdir) {
|
|
178
|
+
const unresolvedIds = new Set(analysis.graph.unresolved.map(dep => dep.plugin));
|
|
179
|
+
const isolated = isolatedNodes(analysis);
|
|
180
|
+
const isolatedIds = new Set(isolated.map(node => node.id));
|
|
181
|
+
const lines = [
|
|
182
|
+
'digraph plugin_topology {',
|
|
183
|
+
` rankdir=${rankdir};`,
|
|
184
|
+
' nodesep=0.3;',
|
|
185
|
+
' ranksep=0.7;',
|
|
186
|
+
' splines=true;',
|
|
187
|
+
' node [shape=oval, style="filled", fillcolor="#ffffff", color="#cbd5e1", fontcolor="#64748b", penwidth=0.8, fontname="Helvetica", fontsize=8, margin="0.16,0.11"];',
|
|
188
|
+
' edge [color="#cbd5e1", penwidth=0.8, arrowsize=0.55, fontname="Helvetica", fontsize=7, fontcolor="#94a3b8"];',
|
|
189
|
+
];
|
|
190
|
+
for (const node of analysis.collapsed.nodes) {
|
|
191
|
+
if (isolatedIds.has(node.id))
|
|
192
|
+
continue;
|
|
193
|
+
const stroke = unresolvedIds.has(node.id) ? ', color="#dc2626", fontcolor="#dc2626", penwidth=1.5' : '';
|
|
194
|
+
lines.push(` "${node.id}" [label="${instanceLabel(node)}"${stroke}];`);
|
|
195
|
+
}
|
|
196
|
+
for (const edge of analysis.collapsed.edges) {
|
|
197
|
+
lines.push(` "${edge.source}" -> "${edge.target}" [label="${escapeDot(edge.service)}"];`);
|
|
198
|
+
}
|
|
199
|
+
if (isolated.length > 0) {
|
|
200
|
+
lines.push(' subgraph "cluster_isolated" {');
|
|
201
|
+
lines.push(' label="isolated";');
|
|
202
|
+
lines.push(' style="rounded,dashed"; fillcolor="#f8fafc"; color="#94a3b8"; fontsize=9; fontcolor="#64748b";');
|
|
203
|
+
lines.push(' rank=same;');
|
|
204
|
+
for (const node of isolated) {
|
|
205
|
+
lines.push(` "${node.id}" [label="${instanceLabel(node)}"];`);
|
|
206
|
+
}
|
|
207
|
+
lines.push(' }');
|
|
208
|
+
}
|
|
209
|
+
lines.push('}');
|
|
210
|
+
return `${lines.join('\n')}\n`;
|
|
211
|
+
}
|
|
212
|
+
function splitSvg(svg) {
|
|
213
|
+
const openStart = svg.indexOf('<svg');
|
|
214
|
+
const openEnd = svg.indexOf('>', openStart) + 1;
|
|
215
|
+
const close = svg.lastIndexOf('</svg>');
|
|
216
|
+
const openTag = svg.slice(openStart, openEnd);
|
|
217
|
+
const viewBox = openTag.match(/viewBox="[0-9.]+ [0-9.]+ ([0-9.]+) ([0-9.]+)"/);
|
|
218
|
+
if (viewBox === null)
|
|
219
|
+
return { width: 0, height: 0, inner: '' };
|
|
220
|
+
return {
|
|
221
|
+
width: parseFloat((viewBox[1] ?? '0')),
|
|
222
|
+
height: parseFloat((viewBox[2] ?? '0')),
|
|
223
|
+
inner: svg.slice(openEnd, close),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Compose two Graphviz SVGs into one document. The isolated graph is placed to
|
|
228
|
+
* the side (LR) or below (TB) of the main graph with a fixed gap, and centered
|
|
229
|
+
* along the graph's perpendicular axis, so the exported SVG is one image.
|
|
230
|
+
*/
|
|
231
|
+
export function composeGraphvizSvgs(main, isolated, rankdir) {
|
|
232
|
+
const m = splitSvg(main);
|
|
233
|
+
const iso = splitSvg(isolated);
|
|
234
|
+
if (iso.width === 0 || iso.height === 0)
|
|
235
|
+
return main;
|
|
236
|
+
// Namespace the isolated copy so marker/clip ids do not collide with the main.
|
|
237
|
+
const isoInner = iso.inner
|
|
238
|
+
.replace(/\bid="([^"]+)"/g, 'id="iso-$1"')
|
|
239
|
+
.replace(/url\(#([^)]+)\)/g, 'url(#iso-$1)')
|
|
240
|
+
.replace(/xlink:href="#([^"]+)"/g, 'xlink:href="#iso-$1"')
|
|
241
|
+
.replace(/(?<!:)\bhref="#([^"]+)"/g, 'href="#iso-$1"');
|
|
242
|
+
const gap = 48;
|
|
243
|
+
let width;
|
|
244
|
+
let height;
|
|
245
|
+
const mx = 0;
|
|
246
|
+
const my = 0;
|
|
247
|
+
let ix = 0;
|
|
248
|
+
let iy = 0;
|
|
249
|
+
if (rankdir === 'LR') {
|
|
250
|
+
ix = m.width + gap;
|
|
251
|
+
iy = Math.max(0, (m.height - iso.height) / 2);
|
|
252
|
+
width = m.width + gap + iso.width;
|
|
253
|
+
height = Math.max(m.height, iy + iso.height);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
iy = m.height + gap;
|
|
257
|
+
ix = Math.max(0, (m.width - iso.width) / 2);
|
|
258
|
+
width = Math.max(m.width, ix + iso.width);
|
|
259
|
+
height = m.height + gap + iso.height;
|
|
260
|
+
}
|
|
261
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img"><g transform="translate(${mx},${my})">${m.inner}</g><g transform="translate(${ix},${iy})">${isoInner}</g></svg>`;
|
|
262
|
+
}
|
package/lib/tool.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing Consumer of the `pluginTopology` capability seam: the
|
|
3
|
+
* `graph.render` tool serializes the runtime plugin dependency graph for the
|
|
4
|
+
* agent to inspect or write out as a shareable artifact.
|
|
5
|
+
* @module @sleetdrop/dsh-plugin-topology/tool
|
|
6
|
+
*/
|
|
7
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
8
|
+
export declare const name = "tool-plugin-topology";
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register the `graph.render` tool on `ctx.tools`.
|
|
12
|
+
* @param ctx - registrant context carrying the tool registry and the topology service.
|
|
13
|
+
*/
|
|
14
|
+
export declare function apply(ctx: Context): void;
|
package/lib/tool.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing Consumer of the `pluginTopology` capability seam: the
|
|
3
|
+
* `graph.render` tool serializes the runtime plugin dependency graph for the
|
|
4
|
+
* agent to inspect or write out as a shareable artifact.
|
|
5
|
+
* @module @sleetdrop/dsh-plugin-topology/tool
|
|
6
|
+
*/
|
|
7
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
8
|
+
export const name = 'tool-plugin-topology';
|
|
9
|
+
export const inject = ['tools', 'pluginTopology'];
|
|
10
|
+
const FORMATS = ['json', 'dot', 'svg'];
|
|
11
|
+
/** Model-facing description of the serialized topology formats. */
|
|
12
|
+
const DESCRIPTION = 'Render the dependency graph of the running DSH instance: '
|
|
13
|
+
+ 'the runtime Cordis plugin topology (which plugin depends on which service, '
|
|
14
|
+
+ 'and therefore on which other plugin). Returns one complete document. '
|
|
15
|
+
+ 'Use `json` to inspect or analyze the assembly — it is a NetworkX-compatible '
|
|
16
|
+
+ 'node-link graph whose nodes carry graph-theory metrics (in/out degree, '
|
|
17
|
+
+ 'centrality, pagerank) and whose graph object carries global metrics '
|
|
18
|
+
+ '(density, strongly connected components, isDag, diameter). '
|
|
19
|
+
+ 'Use `dot` for Graphviz source, or `svg` for a Graphviz-rendered SVG. '
|
|
20
|
+
+ 'For `dot` or `svg`, write the returned content to a file with the write '
|
|
21
|
+
+ 'tool to produce a shareable artifact.';
|
|
22
|
+
/**
|
|
23
|
+
* Register the `graph.render` tool on `ctx.tools`.
|
|
24
|
+
* @param ctx - registrant context carrying the tool registry and the topology service.
|
|
25
|
+
*/
|
|
26
|
+
export function apply(ctx) {
|
|
27
|
+
ctx.tools.register(defineTool({
|
|
28
|
+
name: 'graph.render',
|
|
29
|
+
description: DESCRIPTION,
|
|
30
|
+
parameters: {
|
|
31
|
+
format: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
required: true,
|
|
34
|
+
enum: [...FORMATS],
|
|
35
|
+
description: 'Output format: json (analysis) | dot (Graphviz source) | svg (Graphviz SVG).',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
output: {
|
|
39
|
+
schema: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
properties: {
|
|
43
|
+
format: { type: 'string', required: true, enum: [...FORMATS] },
|
|
44
|
+
content: { type: 'string', required: true },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
render: (_args, value) => [{ type: 'text', text: value.content }],
|
|
48
|
+
},
|
|
49
|
+
async execute(args) {
|
|
50
|
+
return {
|
|
51
|
+
format: args.format,
|
|
52
|
+
content: await ctx.pluginTopology.render(args.format, 'LR'),
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
presentCall: args => ({ card: 'generic', title: 'Render plugin topology', kind: 'other', rawInput: args.format }),
|
|
56
|
+
}));
|
|
57
|
+
}
|
package/lib/typert.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-face Typert manifest.
|
|
3
|
+
*
|
|
4
|
+
* The 0.1.1-rc.2 typert-loader resolves each loader entry's `exports["./typert"]`
|
|
5
|
+
* and registers its TYPERT manifest into `ctx.typert`; packages without the
|
|
6
|
+
* export are skipped silently, which leaves their `@Remote` methods
|
|
7
|
+
* undiscoverable. This module mirrors the artifact the
|
|
8
|
+
* `@deepseek-ai/dsh-typert-generator` would emit for this package, so the host
|
|
9
|
+
* gateway can dispatch `pluginTopology/analyze` and `pluginTopology/render`.
|
|
10
|
+
* @module @sleetdrop/dsh-plugin-topology/typert
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
export declare const TYPERT: {
|
|
14
|
+
package: string;
|
|
15
|
+
face: string;
|
|
16
|
+
schemas: never[];
|
|
17
|
+
invocations: ({
|
|
18
|
+
id: string;
|
|
19
|
+
service: string;
|
|
20
|
+
namespace: string;
|
|
21
|
+
method: string;
|
|
22
|
+
invocation: {
|
|
23
|
+
kind: string;
|
|
24
|
+
};
|
|
25
|
+
parameters: never[];
|
|
26
|
+
result: {
|
|
27
|
+
mode: string;
|
|
28
|
+
typeSymbol: string;
|
|
29
|
+
schema: z.ZodObject<{
|
|
30
|
+
graph: z.ZodReadonly<z.ZodObject<{
|
|
31
|
+
kind: z.ZodReadonly<z.ZodLiteral<"bipartite">>;
|
|
32
|
+
generatedAt: z.ZodReadonly<z.ZodString>;
|
|
33
|
+
nodes: z.ZodReadonly<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
34
|
+
id: z.ZodReadonly<z.ZodString>;
|
|
35
|
+
kind: z.ZodReadonly<z.ZodLiteral<"plugin">>;
|
|
36
|
+
label: z.ZodReadonly<z.ZodString>;
|
|
37
|
+
state: z.ZodReadonly<z.ZodUnion<readonly [z.ZodLiteral<"PENDING">, z.ZodLiteral<"LOADING">, z.ZodLiteral<"ACTIVE">, z.ZodLiteral<"FAILED">, z.ZodLiteral<"DISPOSED">, z.ZodLiteral<"UNLOADING">]>>;
|
|
38
|
+
parent: z.ZodReadonly<z.ZodUnion<readonly [z.ZodLiteral<null>, z.ZodString]>>;
|
|
39
|
+
inject: z.ZodReadonly<z.ZodArray<z.ZodString>>;
|
|
40
|
+
source: z.ZodOptional<z.ZodReadonly<z.ZodString>>;
|
|
41
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
42
|
+
id: z.ZodReadonly<z.ZodString>;
|
|
43
|
+
kind: z.ZodReadonly<z.ZodLiteral<"service">>;
|
|
44
|
+
label: z.ZodReadonly<z.ZodString>;
|
|
45
|
+
scope: z.ZodReadonly<z.ZodString>;
|
|
46
|
+
}, z.core.$strip>]>>>;
|
|
47
|
+
edges: z.ZodReadonly<z.ZodArray<z.ZodObject<{
|
|
48
|
+
source: z.ZodReadonly<z.ZodString>;
|
|
49
|
+
target: z.ZodReadonly<z.ZodString>;
|
|
50
|
+
kind: z.ZodReadonly<z.ZodUnion<readonly [z.ZodLiteral<"provides">, z.ZodLiteral<"injects">]>>;
|
|
51
|
+
service: z.ZodReadonly<z.ZodString>;
|
|
52
|
+
}, z.core.$strip>>>;
|
|
53
|
+
unresolved: z.ZodReadonly<z.ZodArray<z.ZodObject<{
|
|
54
|
+
plugin: z.ZodReadonly<z.ZodString>;
|
|
55
|
+
service: z.ZodReadonly<z.ZodString>;
|
|
56
|
+
state: z.ZodReadonly<z.ZodUnion<readonly [z.ZodLiteral<"PENDING">, z.ZodLiteral<"LOADING">, z.ZodLiteral<"ACTIVE">, z.ZodLiteral<"FAILED">, z.ZodLiteral<"DISPOSED">, z.ZodLiteral<"UNLOADING">]>>;
|
|
57
|
+
}, z.core.$strip>>>;
|
|
58
|
+
}, z.core.$strip>>;
|
|
59
|
+
collapsed: z.ZodReadonly<z.ZodObject<{
|
|
60
|
+
kind: z.ZodReadonly<z.ZodLiteral<"collapsed">>;
|
|
61
|
+
generatedAt: z.ZodReadonly<z.ZodString>;
|
|
62
|
+
nodes: z.ZodReadonly<z.ZodArray<z.ZodObject<{
|
|
63
|
+
id: z.ZodReadonly<z.ZodString>;
|
|
64
|
+
kind: z.ZodReadonly<z.ZodLiteral<"plugin">>;
|
|
65
|
+
label: z.ZodReadonly<z.ZodString>;
|
|
66
|
+
state: z.ZodReadonly<z.ZodUnion<readonly [z.ZodLiteral<"PENDING">, z.ZodLiteral<"LOADING">, z.ZodLiteral<"ACTIVE">, z.ZodLiteral<"FAILED">, z.ZodLiteral<"DISPOSED">, z.ZodLiteral<"UNLOADING">]>>;
|
|
67
|
+
parent: z.ZodReadonly<z.ZodUnion<readonly [z.ZodLiteral<null>, z.ZodString]>>;
|
|
68
|
+
inject: z.ZodReadonly<z.ZodArray<z.ZodString>>;
|
|
69
|
+
source: z.ZodOptional<z.ZodReadonly<z.ZodString>>;
|
|
70
|
+
}, z.core.$strip>>>;
|
|
71
|
+
edges: z.ZodReadonly<z.ZodArray<z.ZodObject<{
|
|
72
|
+
source: z.ZodReadonly<z.ZodString>;
|
|
73
|
+
target: z.ZodReadonly<z.ZodString>;
|
|
74
|
+
service: z.ZodReadonly<z.ZodString>;
|
|
75
|
+
}, z.core.$strip>>>;
|
|
76
|
+
}, z.core.$strip>>;
|
|
77
|
+
nodeMetrics: z.ZodReadonly<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
78
|
+
inDegree: z.ZodReadonly<z.ZodNumber>;
|
|
79
|
+
outDegree: z.ZodReadonly<z.ZodNumber>;
|
|
80
|
+
degreeCentrality: z.ZodReadonly<z.ZodNumber>;
|
|
81
|
+
betweennessCentrality: z.ZodReadonly<z.ZodNumber>;
|
|
82
|
+
eigenvectorCentrality: z.ZodReadonly<z.ZodNumber>;
|
|
83
|
+
pagerank: z.ZodReadonly<z.ZodNumber>;
|
|
84
|
+
}, z.core.$strip>>>;
|
|
85
|
+
graphMetrics: z.ZodReadonly<z.ZodObject<{
|
|
86
|
+
nodeCount: z.ZodReadonly<z.ZodNumber>;
|
|
87
|
+
edgeCount: z.ZodReadonly<z.ZodNumber>;
|
|
88
|
+
density: z.ZodReadonly<z.ZodNumber>;
|
|
89
|
+
stronglyConnectedComponents: z.ZodReadonly<z.ZodNumber>;
|
|
90
|
+
largestSccSize: z.ZodReadonly<z.ZodNumber>;
|
|
91
|
+
isDag: z.ZodReadonly<z.ZodBoolean>;
|
|
92
|
+
weaklyConnectedComponents: z.ZodReadonly<z.ZodNumber>;
|
|
93
|
+
diameter: z.ZodReadonly<z.ZodNumber>;
|
|
94
|
+
averagePathLength: z.ZodReadonly<z.ZodNumber>;
|
|
95
|
+
sourceCount: z.ZodReadonly<z.ZodNumber>;
|
|
96
|
+
sinkCount: z.ZodReadonly<z.ZodNumber>;
|
|
97
|
+
}, z.core.$strip>>;
|
|
98
|
+
}, z.core.$strip>;
|
|
99
|
+
};
|
|
100
|
+
} | {
|
|
101
|
+
id: string;
|
|
102
|
+
service: string;
|
|
103
|
+
namespace: string;
|
|
104
|
+
method: string;
|
|
105
|
+
invocation: {
|
|
106
|
+
kind: string;
|
|
107
|
+
};
|
|
108
|
+
parameters: ({
|
|
109
|
+
name: string;
|
|
110
|
+
wire: string;
|
|
111
|
+
source: string;
|
|
112
|
+
codec: {
|
|
113
|
+
mode: string;
|
|
114
|
+
typeSymbol: string;
|
|
115
|
+
schema: z.ZodUnion<readonly [z.ZodLiteral<"json">, z.ZodLiteral<"dot">, z.ZodLiteral<"svg">]>;
|
|
116
|
+
};
|
|
117
|
+
} | {
|
|
118
|
+
name: string;
|
|
119
|
+
wire: string;
|
|
120
|
+
source: string;
|
|
121
|
+
codec: {
|
|
122
|
+
mode: string;
|
|
123
|
+
typeSymbol: string;
|
|
124
|
+
schema: z.ZodUnion<readonly [z.ZodLiteral<"TB">, z.ZodLiteral<"LR">]>;
|
|
125
|
+
};
|
|
126
|
+
})[];
|
|
127
|
+
result: {
|
|
128
|
+
mode: string;
|
|
129
|
+
typeSymbol: string;
|
|
130
|
+
schema: z.ZodString;
|
|
131
|
+
};
|
|
132
|
+
})[];
|
|
133
|
+
model: {
|
|
134
|
+
services: never[];
|
|
135
|
+
events: never[];
|
|
136
|
+
objects: never[];
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
export default TYPERT;
|