@sparkleideas/plugins 3.0.0-alpha.10
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.md +401 -0
- package/__tests__/collection-manager.test.ts +332 -0
- package/__tests__/dependency-graph.test.ts +434 -0
- package/__tests__/enhanced-plugin-registry.test.ts +488 -0
- package/__tests__/plugin-registry.test.ts +368 -0
- package/__tests__/ruvector-bridge.test.ts +2429 -0
- package/__tests__/ruvector-integration.test.ts +1602 -0
- package/__tests__/ruvector-migrations.test.ts +1099 -0
- package/__tests__/ruvector-quantization.test.ts +846 -0
- package/__tests__/ruvector-streaming.test.ts +1088 -0
- package/__tests__/sdk.test.ts +325 -0
- package/__tests__/security.test.ts +348 -0
- package/__tests__/utils/ruvector-test-utils.ts +860 -0
- package/examples/plugin-creator/index.ts +636 -0
- package/examples/plugin-creator/plugin-creator.test.ts +312 -0
- package/examples/ruvector/README.md +288 -0
- package/examples/ruvector/attention-patterns.ts +394 -0
- package/examples/ruvector/basic-usage.ts +288 -0
- package/examples/ruvector/docker-compose.yml +75 -0
- package/examples/ruvector/gnn-analysis.ts +501 -0
- package/examples/ruvector/hyperbolic-hierarchies.ts +557 -0
- package/examples/ruvector/init-db.sql +119 -0
- package/examples/ruvector/quantization.ts +680 -0
- package/examples/ruvector/self-learning.ts +447 -0
- package/examples/ruvector/semantic-search.ts +576 -0
- package/examples/ruvector/streaming-large-data.ts +507 -0
- package/examples/ruvector/transactions.ts +594 -0
- package/examples/ruvector-plugins/hook-pattern-library.ts +486 -0
- package/examples/ruvector-plugins/index.ts +79 -0
- package/examples/ruvector-plugins/intent-router.ts +354 -0
- package/examples/ruvector-plugins/mcp-tool-optimizer.ts +424 -0
- package/examples/ruvector-plugins/reasoning-bank.ts +657 -0
- package/examples/ruvector-plugins/ruvector-plugins.test.ts +518 -0
- package/examples/ruvector-plugins/semantic-code-search.ts +498 -0
- package/examples/ruvector-plugins/shared/index.ts +20 -0
- package/examples/ruvector-plugins/shared/vector-utils.ts +257 -0
- package/examples/ruvector-plugins/sona-learning.ts +445 -0
- package/package.json +97 -0
- package/src/collections/collection-manager.ts +661 -0
- package/src/collections/index.ts +56 -0
- package/src/collections/official/index.ts +1040 -0
- package/src/core/base-plugin.ts +416 -0
- package/src/core/plugin-interface.ts +215 -0
- package/src/hooks/index.ts +685 -0
- package/src/index.ts +378 -0
- package/src/integrations/agentic-flow.ts +743 -0
- package/src/integrations/index.ts +88 -0
- package/src/integrations/ruvector/ARCHITECTURE.md +1245 -0
- package/src/integrations/ruvector/attention-advanced.ts +1040 -0
- package/src/integrations/ruvector/attention-executor.ts +782 -0
- package/src/integrations/ruvector/attention-mechanisms.ts +757 -0
- package/src/integrations/ruvector/attention.ts +1063 -0
- package/src/integrations/ruvector/gnn.ts +3050 -0
- package/src/integrations/ruvector/hyperbolic.ts +1948 -0
- package/src/integrations/ruvector/index.ts +394 -0
- package/src/integrations/ruvector/migrations/001_create_extension.sql +135 -0
- package/src/integrations/ruvector/migrations/002_create_vector_tables.sql +259 -0
- package/src/integrations/ruvector/migrations/003_create_indices.sql +328 -0
- package/src/integrations/ruvector/migrations/004_create_functions.sql +598 -0
- package/src/integrations/ruvector/migrations/005_create_attention_functions.sql +654 -0
- package/src/integrations/ruvector/migrations/006_create_gnn_functions.sql +728 -0
- package/src/integrations/ruvector/migrations/007_create_hyperbolic_functions.sql +762 -0
- package/src/integrations/ruvector/migrations/index.ts +35 -0
- package/src/integrations/ruvector/migrations/migrations.ts +647 -0
- package/src/integrations/ruvector/quantization.ts +2036 -0
- package/src/integrations/ruvector/ruvector-bridge.ts +2000 -0
- package/src/integrations/ruvector/self-learning.ts +2376 -0
- package/src/integrations/ruvector/streaming.ts +1737 -0
- package/src/integrations/ruvector/types.ts +1945 -0
- package/src/providers/index.ts +643 -0
- package/src/registry/dependency-graph.ts +568 -0
- package/src/registry/enhanced-plugin-registry.ts +994 -0
- package/src/registry/plugin-registry.ts +604 -0
- package/src/sdk/index.ts +563 -0
- package/src/security/index.ts +594 -0
- package/src/types/index.ts +446 -0
- package/src/workers/index.ts +700 -0
- package/tmp.json +0 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +23 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency Graph
|
|
3
|
+
*
|
|
4
|
+
* Manages plugin dependencies with version constraint checking,
|
|
5
|
+
* safe unload detection, and topological sorting.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export interface PluginDependency {
|
|
13
|
+
name: string;
|
|
14
|
+
version: string; // Semver range: "^3.0.0", ">=2.1.0 <3.0.0", "*"
|
|
15
|
+
optional?: boolean; // Don't fail if missing
|
|
16
|
+
peerDependency?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DependencyNode {
|
|
20
|
+
name: string;
|
|
21
|
+
version: string;
|
|
22
|
+
dependencies: PluginDependency[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DependencyError {
|
|
26
|
+
type: 'missing' | 'version_mismatch' | 'circular';
|
|
27
|
+
plugin: string;
|
|
28
|
+
dependency?: string;
|
|
29
|
+
required?: string;
|
|
30
|
+
actual?: string;
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Semver Utilities
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse a semver version string.
|
|
40
|
+
*/
|
|
41
|
+
export function parseVersion(version: string): { major: number; minor: number; patch: number } | null {
|
|
42
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
43
|
+
if (!match) return null;
|
|
44
|
+
return {
|
|
45
|
+
major: parseInt(match[1], 10),
|
|
46
|
+
minor: parseInt(match[2], 10),
|
|
47
|
+
patch: parseInt(match[3], 10),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Compare two semver versions.
|
|
53
|
+
* Returns: -1 if a < b, 0 if a == b, 1 if a > b
|
|
54
|
+
*/
|
|
55
|
+
export function compareVersions(a: string, b: string): number {
|
|
56
|
+
const va = parseVersion(a);
|
|
57
|
+
const vb = parseVersion(b);
|
|
58
|
+
|
|
59
|
+
if (!va || !vb) return 0;
|
|
60
|
+
|
|
61
|
+
if (va.major !== vb.major) return va.major < vb.major ? -1 : 1;
|
|
62
|
+
if (va.minor !== vb.minor) return va.minor < vb.minor ? -1 : 1;
|
|
63
|
+
if (va.patch !== vb.patch) return va.patch < vb.patch ? -1 : 1;
|
|
64
|
+
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if a version satisfies a semver range.
|
|
70
|
+
*
|
|
71
|
+
* Supported formats:
|
|
72
|
+
* - "*" - any version
|
|
73
|
+
* - "3.0.0" - exact version
|
|
74
|
+
* - "^3.0.0" - compatible with 3.x.x (major must match)
|
|
75
|
+
* - "~3.1.0" - approximately 3.1.x (major.minor must match)
|
|
76
|
+
* - ">=3.0.0" - greater than or equal
|
|
77
|
+
* - "<=3.0.0" - less than or equal
|
|
78
|
+
* - ">3.0.0" - greater than
|
|
79
|
+
* - "<3.0.0" - less than
|
|
80
|
+
* - ">=2.0.0 <3.0.0" - range (space-separated)
|
|
81
|
+
*/
|
|
82
|
+
export function satisfiesVersion(range: string, version: string): boolean {
|
|
83
|
+
const trimmed = range.trim();
|
|
84
|
+
|
|
85
|
+
// Wildcard
|
|
86
|
+
if (trimmed === '*' || trimmed === '') return true;
|
|
87
|
+
|
|
88
|
+
// Parse actual version
|
|
89
|
+
const actual = parseVersion(version);
|
|
90
|
+
if (!actual) return false;
|
|
91
|
+
|
|
92
|
+
// Handle space-separated ranges (AND logic)
|
|
93
|
+
if (trimmed.includes(' ')) {
|
|
94
|
+
const parts = trimmed.split(/\s+/);
|
|
95
|
+
return parts.every(part => satisfiesVersion(part, version));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Caret range: ^3.0.0 means >=3.0.0 <4.0.0
|
|
99
|
+
if (trimmed.startsWith('^')) {
|
|
100
|
+
const required = parseVersion(trimmed.slice(1));
|
|
101
|
+
if (!required) return false;
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
actual.major === required.major &&
|
|
105
|
+
(actual.minor > required.minor ||
|
|
106
|
+
(actual.minor === required.minor && actual.patch >= required.patch))
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Tilde range: ~3.1.0 means >=3.1.0 <3.2.0
|
|
111
|
+
if (trimmed.startsWith('~')) {
|
|
112
|
+
const required = parseVersion(trimmed.slice(1));
|
|
113
|
+
if (!required) return false;
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
actual.major === required.major &&
|
|
117
|
+
actual.minor === required.minor &&
|
|
118
|
+
actual.patch >= required.patch
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Greater than or equal: >=3.0.0
|
|
123
|
+
if (trimmed.startsWith('>=')) {
|
|
124
|
+
const required = parseVersion(trimmed.slice(2));
|
|
125
|
+
if (!required) return false;
|
|
126
|
+
return compareVersions(version, trimmed.slice(2)) >= 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Less than or equal: <=3.0.0
|
|
130
|
+
if (trimmed.startsWith('<=')) {
|
|
131
|
+
const required = parseVersion(trimmed.slice(2));
|
|
132
|
+
if (!required) return false;
|
|
133
|
+
return compareVersions(version, trimmed.slice(2)) <= 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Greater than: >3.0.0
|
|
137
|
+
if (trimmed.startsWith('>')) {
|
|
138
|
+
const required = parseVersion(trimmed.slice(1));
|
|
139
|
+
if (!required) return false;
|
|
140
|
+
return compareVersions(version, trimmed.slice(1)) > 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Less than: <3.0.0
|
|
144
|
+
if (trimmed.startsWith('<')) {
|
|
145
|
+
const required = parseVersion(trimmed.slice(1));
|
|
146
|
+
if (!required) return false;
|
|
147
|
+
return compareVersions(version, trimmed.slice(1)) < 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Exact match
|
|
151
|
+
const required = parseVersion(trimmed);
|
|
152
|
+
if (!required) return false;
|
|
153
|
+
return compareVersions(version, trimmed) === 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// Dependency Graph
|
|
158
|
+
// ============================================================================
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Dependency graph for plugin management.
|
|
162
|
+
*
|
|
163
|
+
* Features:
|
|
164
|
+
* - Version constraint validation
|
|
165
|
+
* - Circular dependency detection
|
|
166
|
+
* - Safe unload checking (no dependents)
|
|
167
|
+
* - Topological sort for load order
|
|
168
|
+
* - Depth-level grouping for parallel initialization
|
|
169
|
+
*/
|
|
170
|
+
export class DependencyGraph {
|
|
171
|
+
// Plugin name -> node data
|
|
172
|
+
private nodes = new Map<string, DependencyNode>();
|
|
173
|
+
|
|
174
|
+
// Forward edges: plugin -> plugins it depends on
|
|
175
|
+
private dependencies = new Map<string, Set<string>>();
|
|
176
|
+
|
|
177
|
+
// Reverse edges: plugin -> plugins that depend on it
|
|
178
|
+
private dependents = new Map<string, Set<string>>();
|
|
179
|
+
|
|
180
|
+
// =========================================================================
|
|
181
|
+
// Node Management
|
|
182
|
+
// =========================================================================
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Add a plugin to the graph.
|
|
186
|
+
*/
|
|
187
|
+
addPlugin(name: string, version: string, dependencies: PluginDependency[] = []): void {
|
|
188
|
+
// Store node data
|
|
189
|
+
this.nodes.set(name, { name, version, dependencies });
|
|
190
|
+
|
|
191
|
+
// Initialize edge sets
|
|
192
|
+
if (!this.dependencies.has(name)) {
|
|
193
|
+
this.dependencies.set(name, new Set());
|
|
194
|
+
}
|
|
195
|
+
if (!this.dependents.has(name)) {
|
|
196
|
+
this.dependents.set(name, new Set());
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Add edges
|
|
200
|
+
for (const dep of dependencies) {
|
|
201
|
+
this.dependencies.get(name)!.add(dep.name);
|
|
202
|
+
|
|
203
|
+
// Ensure dependent set exists
|
|
204
|
+
if (!this.dependents.has(dep.name)) {
|
|
205
|
+
this.dependents.set(dep.name, new Set());
|
|
206
|
+
}
|
|
207
|
+
this.dependents.get(dep.name)!.add(name);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Remove a plugin from the graph.
|
|
213
|
+
*/
|
|
214
|
+
removePlugin(name: string): void {
|
|
215
|
+
const node = this.nodes.get(name);
|
|
216
|
+
if (!node) return;
|
|
217
|
+
|
|
218
|
+
// Remove forward edges
|
|
219
|
+
const deps = this.dependencies.get(name) ?? new Set();
|
|
220
|
+
for (const dep of deps) {
|
|
221
|
+
this.dependents.get(dep)?.delete(name);
|
|
222
|
+
}
|
|
223
|
+
this.dependencies.delete(name);
|
|
224
|
+
|
|
225
|
+
// Remove reverse edges
|
|
226
|
+
const dependers = this.dependents.get(name) ?? new Set();
|
|
227
|
+
for (const depender of dependers) {
|
|
228
|
+
this.dependencies.get(depender)?.delete(name);
|
|
229
|
+
}
|
|
230
|
+
this.dependents.delete(name);
|
|
231
|
+
|
|
232
|
+
// Remove node
|
|
233
|
+
this.nodes.delete(name);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Check if a plugin exists in the graph.
|
|
238
|
+
*/
|
|
239
|
+
hasPlugin(name: string): boolean {
|
|
240
|
+
return this.nodes.has(name);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get plugin version.
|
|
245
|
+
*/
|
|
246
|
+
getVersion(name: string): string | undefined {
|
|
247
|
+
return this.nodes.get(name)?.version;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// =========================================================================
|
|
251
|
+
// Dependency Queries
|
|
252
|
+
// =========================================================================
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get direct dependencies of a plugin.
|
|
256
|
+
*/
|
|
257
|
+
getDependencies(name: string): string[] {
|
|
258
|
+
return Array.from(this.dependencies.get(name) ?? []);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get direct dependents of a plugin (plugins that depend on it).
|
|
263
|
+
*/
|
|
264
|
+
getDependents(name: string): string[] {
|
|
265
|
+
return Array.from(this.dependents.get(name) ?? []);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get all transitive dependencies of a plugin.
|
|
270
|
+
*/
|
|
271
|
+
getAllDependencies(name: string): string[] {
|
|
272
|
+
const visited = new Set<string>();
|
|
273
|
+
const result: string[] = [];
|
|
274
|
+
|
|
275
|
+
const visit = (current: string): void => {
|
|
276
|
+
if (visited.has(current)) return;
|
|
277
|
+
visited.add(current);
|
|
278
|
+
|
|
279
|
+
const deps = this.dependencies.get(current) ?? new Set();
|
|
280
|
+
for (const dep of deps) {
|
|
281
|
+
visit(dep);
|
|
282
|
+
if (!result.includes(dep)) {
|
|
283
|
+
result.push(dep);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
visit(name);
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Get all transitive dependents of a plugin.
|
|
294
|
+
*/
|
|
295
|
+
getAllDependents(name: string): string[] {
|
|
296
|
+
const visited = new Set<string>();
|
|
297
|
+
const result: string[] = [];
|
|
298
|
+
|
|
299
|
+
const visit = (current: string): void => {
|
|
300
|
+
if (visited.has(current)) return;
|
|
301
|
+
visited.add(current);
|
|
302
|
+
|
|
303
|
+
const deps = this.dependents.get(current) ?? new Set();
|
|
304
|
+
for (const dep of deps) {
|
|
305
|
+
visit(dep);
|
|
306
|
+
if (!result.includes(dep)) {
|
|
307
|
+
result.push(dep);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
visit(name);
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// =========================================================================
|
|
317
|
+
// Safe Operations
|
|
318
|
+
// =========================================================================
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Check if a plugin can be safely removed (no dependents).
|
|
322
|
+
*/
|
|
323
|
+
canSafelyRemove(name: string): boolean {
|
|
324
|
+
const dependents = this.getDependents(name);
|
|
325
|
+
return dependents.length === 0;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get the order to remove plugins for cascade unload.
|
|
330
|
+
* Returns plugins in reverse dependency order (dependents first).
|
|
331
|
+
*/
|
|
332
|
+
getRemovalOrder(name: string): string[] {
|
|
333
|
+
const allDependents = this.getAllDependents(name);
|
|
334
|
+
|
|
335
|
+
// Sort by reverse topological order
|
|
336
|
+
// Plugins that depend on others should be removed first
|
|
337
|
+
const sorted: string[] = [];
|
|
338
|
+
const visited = new Set<string>();
|
|
339
|
+
|
|
340
|
+
const visit = (current: string): void => {
|
|
341
|
+
if (visited.has(current)) return;
|
|
342
|
+
visited.add(current);
|
|
343
|
+
|
|
344
|
+
// Visit dependents first (reverse order)
|
|
345
|
+
const deps = this.dependents.get(current) ?? new Set();
|
|
346
|
+
for (const dep of deps) {
|
|
347
|
+
if (allDependents.includes(dep)) {
|
|
348
|
+
visit(dep);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
sorted.push(current);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
visit(name);
|
|
356
|
+
return sorted;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// =========================================================================
|
|
360
|
+
// Validation
|
|
361
|
+
// =========================================================================
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Validate all dependencies and return errors.
|
|
365
|
+
*/
|
|
366
|
+
validate(): DependencyError[] {
|
|
367
|
+
const errors: DependencyError[] = [];
|
|
368
|
+
|
|
369
|
+
for (const [name, node] of this.nodes) {
|
|
370
|
+
for (const dep of node.dependencies) {
|
|
371
|
+
// Check if dependency exists
|
|
372
|
+
const depNode = this.nodes.get(dep.name);
|
|
373
|
+
|
|
374
|
+
if (!depNode) {
|
|
375
|
+
if (!dep.optional) {
|
|
376
|
+
errors.push({
|
|
377
|
+
type: 'missing',
|
|
378
|
+
plugin: name,
|
|
379
|
+
dependency: dep.name,
|
|
380
|
+
required: dep.version,
|
|
381
|
+
message: `Plugin ${name} requires ${dep.name}@${dep.version} but it is not installed`,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Check version constraint
|
|
388
|
+
if (!satisfiesVersion(dep.version, depNode.version)) {
|
|
389
|
+
errors.push({
|
|
390
|
+
type: 'version_mismatch',
|
|
391
|
+
plugin: name,
|
|
392
|
+
dependency: dep.name,
|
|
393
|
+
required: dep.version,
|
|
394
|
+
actual: depNode.version,
|
|
395
|
+
message: `Plugin ${name} requires ${dep.name}@${dep.version} but found ${depNode.version}`,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Check for circular dependencies
|
|
402
|
+
const circular = this.detectCircular();
|
|
403
|
+
for (const cycle of circular) {
|
|
404
|
+
errors.push({
|
|
405
|
+
type: 'circular',
|
|
406
|
+
plugin: cycle[0],
|
|
407
|
+
message: `Circular dependency detected: ${cycle.join(' -> ')}`,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return errors;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Detect circular dependencies.
|
|
416
|
+
* Returns array of cycles (each cycle is an array of plugin names).
|
|
417
|
+
*/
|
|
418
|
+
detectCircular(): string[][] {
|
|
419
|
+
const cycles: string[][] = [];
|
|
420
|
+
const visited = new Set<string>();
|
|
421
|
+
const recursionStack = new Set<string>();
|
|
422
|
+
const path: string[] = [];
|
|
423
|
+
|
|
424
|
+
const dfs = (name: string): void => {
|
|
425
|
+
visited.add(name);
|
|
426
|
+
recursionStack.add(name);
|
|
427
|
+
path.push(name);
|
|
428
|
+
|
|
429
|
+
const deps = this.dependencies.get(name) ?? new Set();
|
|
430
|
+
for (const dep of deps) {
|
|
431
|
+
if (!visited.has(dep)) {
|
|
432
|
+
dfs(dep);
|
|
433
|
+
} else if (recursionStack.has(dep)) {
|
|
434
|
+
// Found a cycle
|
|
435
|
+
const cycleStart = path.indexOf(dep);
|
|
436
|
+
const cycle = [...path.slice(cycleStart), dep];
|
|
437
|
+
cycles.push(cycle);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
path.pop();
|
|
442
|
+
recursionStack.delete(name);
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
for (const name of this.nodes.keys()) {
|
|
446
|
+
if (!visited.has(name)) {
|
|
447
|
+
dfs(name);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return cycles;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// =========================================================================
|
|
455
|
+
// Load Order
|
|
456
|
+
// =========================================================================
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Get topological sort order for loading plugins.
|
|
460
|
+
* Dependencies come before dependents.
|
|
461
|
+
*/
|
|
462
|
+
getLoadOrder(): string[] {
|
|
463
|
+
const visited = new Set<string>();
|
|
464
|
+
const order: string[] = [];
|
|
465
|
+
|
|
466
|
+
const visit = (name: string): void => {
|
|
467
|
+
if (visited.has(name)) return;
|
|
468
|
+
visited.add(name);
|
|
469
|
+
|
|
470
|
+
// Visit dependencies first
|
|
471
|
+
const deps = this.dependencies.get(name) ?? new Set();
|
|
472
|
+
for (const dep of deps) {
|
|
473
|
+
if (this.nodes.has(dep)) {
|
|
474
|
+
visit(dep);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
order.push(name);
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
for (const name of this.nodes.keys()) {
|
|
482
|
+
visit(name);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return order;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Get plugins grouped by dependency depth.
|
|
490
|
+
* Level 0 = no dependencies, Level 1 = depends only on level 0, etc.
|
|
491
|
+
*
|
|
492
|
+
* Useful for parallel initialization: init all level N before starting level N+1.
|
|
493
|
+
*/
|
|
494
|
+
getDepthLevels(): string[][] {
|
|
495
|
+
const depths = new Map<string, number>();
|
|
496
|
+
const order = this.getLoadOrder();
|
|
497
|
+
|
|
498
|
+
// Calculate depth for each plugin
|
|
499
|
+
for (const name of order) {
|
|
500
|
+
const deps = this.dependencies.get(name) ?? new Set();
|
|
501
|
+
let maxDepth = -1;
|
|
502
|
+
|
|
503
|
+
for (const dep of deps) {
|
|
504
|
+
const depDepth = depths.get(dep);
|
|
505
|
+
if (depDepth !== undefined && depDepth > maxDepth) {
|
|
506
|
+
maxDepth = depDepth;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
depths.set(name, maxDepth + 1);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Group by depth
|
|
514
|
+
const levels: string[][] = [];
|
|
515
|
+
for (const [name, depth] of depths) {
|
|
516
|
+
while (levels.length <= depth) {
|
|
517
|
+
levels.push([]);
|
|
518
|
+
}
|
|
519
|
+
levels[depth].push(name);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return levels;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// =========================================================================
|
|
526
|
+
// Utilities
|
|
527
|
+
// =========================================================================
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Get all plugin names.
|
|
531
|
+
*/
|
|
532
|
+
getPluginNames(): string[] {
|
|
533
|
+
return Array.from(this.nodes.keys());
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Get plugin count.
|
|
538
|
+
*/
|
|
539
|
+
size(): number {
|
|
540
|
+
return this.nodes.size;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Clear the graph.
|
|
545
|
+
*/
|
|
546
|
+
clear(): void {
|
|
547
|
+
this.nodes.clear();
|
|
548
|
+
this.dependencies.clear();
|
|
549
|
+
this.dependents.clear();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Export graph state for debugging.
|
|
554
|
+
*/
|
|
555
|
+
toJSON(): object {
|
|
556
|
+
return {
|
|
557
|
+
nodes: Array.from(this.nodes.entries()).map(([name, node]) => ({
|
|
558
|
+
name,
|
|
559
|
+
version: node.version,
|
|
560
|
+
dependencies: node.dependencies,
|
|
561
|
+
})),
|
|
562
|
+
edges: Array.from(this.dependencies.entries()).map(([from, tos]) => ({
|
|
563
|
+
from,
|
|
564
|
+
to: Array.from(tos),
|
|
565
|
+
})),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
}
|