@kridaydave/code-mapper 1.0.0 → 1.0.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/CHANGELOG.md +31 -0
- package/README.md +1 -0
- package/bin/code-mapper.mjs +86 -0
- package/dist/graph/GraphAnalyzer.js +32 -65
- package/dist/graph/GraphAnalyzer.js.map +1 -1
- package/dist/graph/GraphBuilder.js +18 -45
- package/dist/graph/GraphBuilder.js.map +1 -1
- package/dist/index.js +100 -23
- package/dist/index.js.map +1 -1
- package/dist/mcp/cache.js +8 -17
- package/dist/mcp/cache.js.map +1 -1
- package/dist/mcp/resources.js +5 -1
- package/dist/mcp/resources.js.map +1 -1
- package/dist/mcp/tools.js +190 -35
- package/dist/mcp/tools.js.map +1 -1
- package/dist/parser/ComplexityAnalyzer.js +19 -2
- package/dist/parser/ComplexityAnalyzer.js.map +1 -1
- package/dist/parser/FileAnalyzer.js +8 -30
- package/dist/parser/FileAnalyzer.js.map +1 -1
- package/dist/parser/ProjectParser.js +8 -5
- package/dist/parser/ProjectParser.js.map +1 -1
- package/dist/parser/ProjectParser.test.js +1 -17
- package/dist/parser/ProjectParser.test.js.map +1 -1
- package/dist/tui/index.js +239 -0
- package/dist/tui/index.js.map +1 -0
- package/package.json +82 -35
- package/AGENTS.md +0 -174
- package/docs/PHASE2_PLAN.md +0 -435
- package/fixtures/test-project/calculator.ts +0 -28
- package/fixtures/test-project/index.ts +0 -2
- package/fixtures/test-project/math.ts +0 -11
- package/src/graph/Graph.test.ts +0 -222
- package/src/graph/GraphAnalyzer.ts +0 -502
- package/src/graph/GraphBuilder.ts +0 -258
- package/src/graph/types.ts +0 -42
- package/src/index.ts +0 -38
- package/src/mcp/cache.ts +0 -89
- package/src/mcp/resources.ts +0 -137
- package/src/mcp/tools.test.ts +0 -104
- package/src/mcp/tools.ts +0 -529
- package/src/parser/ComplexityAnalyzer.ts +0 -275
- package/src/parser/FileAnalyzer.ts +0 -215
- package/src/parser/ProjectParser.test.ts +0 -96
- package/src/parser/ProjectParser.ts +0 -172
- package/src/parser/types.ts +0 -77
- package/src/types/graphology-pagerank.d.ts +0 -20
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -15
|
@@ -1,502 +0,0 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
const require = createRequire(import.meta.url);
|
|
3
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
4
|
-
const Graph: typeof import("graphology").default = require("graphology");
|
|
5
|
-
import type { AbstractGraph, Attributes } from "graphology-types";
|
|
6
|
-
import * as shortestPath from "graphology-shortest-path";
|
|
7
|
-
import { centrality } from "graphology-metrics";
|
|
8
|
-
import pagerank from "graphology-metrics/centrality/pagerank";
|
|
9
|
-
import { ParseResult } from "../parser/types.js";
|
|
10
|
-
import { GraphNode, GraphEdge, RankedFile, FunctionMatch, CallChainResult } from "./types.js";
|
|
11
|
-
|
|
12
|
-
type CodeGraph = AbstractGraph<Attributes, Attributes, Attributes>;
|
|
13
|
-
|
|
14
|
-
export type ExportFormat = "json" | "mermaid" | "dot" | "plantuml";
|
|
15
|
-
|
|
16
|
-
export interface ComplexityResult {
|
|
17
|
-
filePath: string;
|
|
18
|
-
relativePath: string;
|
|
19
|
-
cyclomaticComplexity: number;
|
|
20
|
-
linesOfCode: number;
|
|
21
|
-
functionCount: number;
|
|
22
|
-
classCount: number;
|
|
23
|
-
nestingDepth: number;
|
|
24
|
-
cognitiveComplexity: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class GraphAnalyzer {
|
|
28
|
-
private graph: CodeGraph;
|
|
29
|
-
private parseResult: ParseResult;
|
|
30
|
-
private nodes: GraphNode[];
|
|
31
|
-
private edges: GraphEdge[];
|
|
32
|
-
|
|
33
|
-
constructor(graph: CodeGraph, parseResult: ParseResult, nodes: GraphNode[], edges: GraphEdge[]) {
|
|
34
|
-
this.graph = graph;
|
|
35
|
-
this.parseResult = parseResult;
|
|
36
|
-
this.nodes = nodes;
|
|
37
|
-
this.edges = edges;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
getGraph(): CodeGraph {
|
|
41
|
-
return this.graph;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
getNodes(): GraphNode[] {
|
|
45
|
-
return this.nodes;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
getEdges(): GraphEdge[] {
|
|
49
|
-
return this.edges;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
getParseResult(): ParseResult {
|
|
53
|
-
return this.parseResult;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Rank files by centrality metric
|
|
58
|
-
*/
|
|
59
|
-
rankImpact(metric: "inDegree" | "outDegree" | "betweenness" | "pagerank" = "inDegree"): RankedFile[] {
|
|
60
|
-
const fileNodes = this.nodes.filter((n: GraphNode) => n.kind === "file");
|
|
61
|
-
const scores = new Map<string, number>();
|
|
62
|
-
|
|
63
|
-
if (metric === "betweenness") {
|
|
64
|
-
const nodeCount = this.graph.order;
|
|
65
|
-
if (nodeCount > 200) {
|
|
66
|
-
metric = "inDegree";
|
|
67
|
-
} else {
|
|
68
|
-
const betweenness = centrality.betweenness(this.graph);
|
|
69
|
-
for (const [node, score] of Object.entries(betweenness)) {
|
|
70
|
-
scores.set(node, score as number);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (metric === "pagerank") {
|
|
76
|
-
const ranks = pagerank(this.graph);
|
|
77
|
-
for (const [node, score] of Object.entries(ranks)) {
|
|
78
|
-
scores.set(node, score as number);
|
|
79
|
-
}
|
|
80
|
-
} else if (metric === "inDegree") {
|
|
81
|
-
for (const node of fileNodes) {
|
|
82
|
-
scores.set(node.id, this.graph.inDegree(node.id));
|
|
83
|
-
}
|
|
84
|
-
} else {
|
|
85
|
-
for (const node of fileNodes) {
|
|
86
|
-
scores.set(node.id, this.graph.outDegree(node.id));
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const ranked: RankedFile[] = fileNodes
|
|
91
|
-
.map((node: GraphNode): RankedFile | null => {
|
|
92
|
-
const fileInfo = this.parseResult.files.find((f) => f.filePath === node.filePath);
|
|
93
|
-
if (!fileInfo) return null;
|
|
94
|
-
return {
|
|
95
|
-
filePath: node.filePath,
|
|
96
|
-
relativePath: fileInfo.relativePath,
|
|
97
|
-
score: scores.get(node.id) ?? 0,
|
|
98
|
-
metric,
|
|
99
|
-
functionCount: fileInfo.functions.length,
|
|
100
|
-
classCount: fileInfo.classes.length,
|
|
101
|
-
importCount: fileInfo.imports.length,
|
|
102
|
-
exportCount: fileInfo.exports.length,
|
|
103
|
-
};
|
|
104
|
-
})
|
|
105
|
-
.filter((r): r is RankedFile => r !== null)
|
|
106
|
-
.sort((a, b) => b.score - a.score);
|
|
107
|
-
|
|
108
|
-
return ranked;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Find functions or classes by name
|
|
113
|
-
*/
|
|
114
|
-
findFunction(name: string, type: "function" | "class" | "any" = "any"): FunctionMatch[] {
|
|
115
|
-
const matches: FunctionMatch[] = [];
|
|
116
|
-
const lowerName = name.toLowerCase();
|
|
117
|
-
|
|
118
|
-
for (const node of this.nodes) {
|
|
119
|
-
if (type !== "any" && node.kind !== type) continue;
|
|
120
|
-
if (node.kind === "file") continue;
|
|
121
|
-
if (!node.label.toLowerCase().includes(lowerName)) continue;
|
|
122
|
-
|
|
123
|
-
const fileInfo = this.parseResult.files.find((f) => f.filePath === node.filePath);
|
|
124
|
-
if (!fileInfo) continue;
|
|
125
|
-
|
|
126
|
-
if (node.kind === "function") {
|
|
127
|
-
const fn = fileInfo.functions.find((f) => f.name.toLowerCase() === node.label.toLowerCase());
|
|
128
|
-
if (fn) {
|
|
129
|
-
matches.push({
|
|
130
|
-
name: fn.name,
|
|
131
|
-
filePath: fn.filePath,
|
|
132
|
-
relativePath: fileInfo.relativePath,
|
|
133
|
-
lineNumber: fn.lineNumber,
|
|
134
|
-
kind: "function",
|
|
135
|
-
parameters: fn.parameters,
|
|
136
|
-
returnType: fn.returnType,
|
|
137
|
-
isExported: fn.isExported,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
} else if (node.kind === "class") {
|
|
141
|
-
const cls = fileInfo.classes.find((c) => c.name.toLowerCase() === node.label.toLowerCase());
|
|
142
|
-
if (cls) {
|
|
143
|
-
matches.push({
|
|
144
|
-
name: cls.name,
|
|
145
|
-
filePath: cls.filePath,
|
|
146
|
-
relativePath: fileInfo.relativePath,
|
|
147
|
-
lineNumber: cls.lineNumber,
|
|
148
|
-
kind: "class",
|
|
149
|
-
parameters: [],
|
|
150
|
-
returnType: cls.name,
|
|
151
|
-
isExported: cls.isExported,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return matches;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Get callers (files that import this file)
|
|
162
|
-
*/
|
|
163
|
-
getCallers(nodeId: string): string[] {
|
|
164
|
-
const callers: string[] = [];
|
|
165
|
-
for (const neighbor of this.graph.inNeighbors(nodeId)) {
|
|
166
|
-
const node = this.nodes.find((n: GraphNode) => n.id === neighbor);
|
|
167
|
-
if (node) {
|
|
168
|
-
callers.push(node.label);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return callers;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Get callees (files this file imports)
|
|
176
|
-
*/
|
|
177
|
-
getCallees(nodeId: string): string[] {
|
|
178
|
-
const callees: string[] = [];
|
|
179
|
-
for (const neighbor of this.graph.outNeighbors(nodeId)) {
|
|
180
|
-
const node = this.nodes.find((n: GraphNode) => n.id === neighbor);
|
|
181
|
-
if (node) {
|
|
182
|
-
callees.push(node.label);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return callees;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Find shortest paths between two nodes
|
|
190
|
-
*/
|
|
191
|
-
traceCallChain(from: string, to: string): CallChainResult {
|
|
192
|
-
const fromNodes = this.nodes.filter((n: GraphNode) => n.label.toLowerCase().includes(from.toLowerCase()));
|
|
193
|
-
const toNodes = this.nodes.filter((n: GraphNode) => n.label.toLowerCase().includes(to.toLowerCase()));
|
|
194
|
-
|
|
195
|
-
if (fromNodes.length === 0 || toNodes.length === 0) {
|
|
196
|
-
return { found: false, paths: [] };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const nodeMap = new Map<string, GraphNode>(this.nodes.map(n => [n.id, n]));
|
|
200
|
-
const paths: string[][] = [];
|
|
201
|
-
|
|
202
|
-
for (const fromNode of fromNodes) {
|
|
203
|
-
for (const toNode of toNodes) {
|
|
204
|
-
if (fromNode.id === toNode.id) {
|
|
205
|
-
const node = fromNode;
|
|
206
|
-
paths.push([`${node.kind}:${node.label}`]);
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const pathResult = shortestPath.bidirectional(this.graph, fromNode.id, toNode.id);
|
|
211
|
-
if (pathResult && pathResult.length > 0) {
|
|
212
|
-
const pathLabels = pathResult.map((nodeId: string) => {
|
|
213
|
-
const node = nodeMap.get(nodeId);
|
|
214
|
-
return node ? `${node.kind}:${node.label}` : nodeId;
|
|
215
|
-
});
|
|
216
|
-
paths.push(pathLabels);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
found: paths.length > 0,
|
|
223
|
-
paths,
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Generate a Mermaid graph diagram string
|
|
229
|
-
*/
|
|
230
|
-
toMermaid(targetFile?: string): string {
|
|
231
|
-
const lines: string[] = ["graph TD"];
|
|
232
|
-
|
|
233
|
-
const nodesToInclude = targetFile
|
|
234
|
-
? this.nodes.filter((n: GraphNode) => {
|
|
235
|
-
const lowerTarget = targetFile.toLowerCase().replace(/\\/g, "/");
|
|
236
|
-
const lowerPath = n.filePath.toLowerCase().replace(/\\/g, "/");
|
|
237
|
-
const basename = lowerPath.split("/").pop() ?? "";
|
|
238
|
-
return basename.includes(lowerTarget) ||
|
|
239
|
-
lowerPath.split("/").some(seg => seg.includes(lowerTarget));
|
|
240
|
-
})
|
|
241
|
-
: this.nodes;
|
|
242
|
-
|
|
243
|
-
const nodeIds = new Set(nodesToInclude.map((n: GraphNode) => n.id));
|
|
244
|
-
|
|
245
|
-
// Add nodes
|
|
246
|
-
for (const node of nodesToInclude) {
|
|
247
|
-
const safeId = this.sanitizeId(node.id);
|
|
248
|
-
const safeLabel = this.sanitizeMermaidText(`${node.kind}: ${node.label}`);
|
|
249
|
-
lines.push(` ${safeId}["${safeLabel}"]`);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Add edges
|
|
253
|
-
for (const edge of this.edges) {
|
|
254
|
-
if (nodeIds.has(edge.source) && nodeIds.has(edge.target)) {
|
|
255
|
-
const sourceId = this.sanitizeId(edge.source);
|
|
256
|
-
const targetId = this.sanitizeId(edge.target);
|
|
257
|
-
const safeKind = this.sanitizeMermaidText(edge.kind);
|
|
258
|
-
lines.push(` ${sourceId} -->|${safeKind}| ${targetId}`);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return lines.join("\n");
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Generate a DOT (Graphviz) graph diagram string
|
|
267
|
-
*/
|
|
268
|
-
toDot(targetFile?: string): string {
|
|
269
|
-
const lines: string[] = [
|
|
270
|
-
"digraph codegraph {",
|
|
271
|
-
" rankdir=LR;",
|
|
272
|
-
" node [fontname=\"Helvetica\"];",
|
|
273
|
-
" edge [fontname=\"Helvetica\"];",
|
|
274
|
-
];
|
|
275
|
-
|
|
276
|
-
const nodesToInclude = targetFile
|
|
277
|
-
? this.nodes.filter((n: GraphNode) => {
|
|
278
|
-
const lowerTarget = targetFile.toLowerCase().replace(/\\/g, "/");
|
|
279
|
-
const lowerPath = n.filePath.toLowerCase().replace(/\\/g, "/");
|
|
280
|
-
const basename = lowerPath.split("/").pop() ?? "";
|
|
281
|
-
return basename.includes(lowerTarget) ||
|
|
282
|
-
lowerPath.split("/").some(seg => seg.includes(lowerTarget));
|
|
283
|
-
})
|
|
284
|
-
: this.nodes;
|
|
285
|
-
|
|
286
|
-
const nodeIds = new Set(nodesToInclude.map((n: GraphNode) => n.id));
|
|
287
|
-
|
|
288
|
-
for (const node of nodesToInclude) {
|
|
289
|
-
const safeId = this.sanitizeId(node.id);
|
|
290
|
-
const safeLabel = this.sanitizeDotText(node.label);
|
|
291
|
-
let shape: string;
|
|
292
|
-
let fillcolor: string;
|
|
293
|
-
|
|
294
|
-
switch (node.kind) {
|
|
295
|
-
case "file":
|
|
296
|
-
shape = "box";
|
|
297
|
-
fillcolor = "lightblue";
|
|
298
|
-
break;
|
|
299
|
-
case "function":
|
|
300
|
-
shape = "ellipse";
|
|
301
|
-
fillcolor = "lightgreen";
|
|
302
|
-
break;
|
|
303
|
-
case "class":
|
|
304
|
-
shape = "box3d";
|
|
305
|
-
fillcolor = "lightyellow";
|
|
306
|
-
break;
|
|
307
|
-
default:
|
|
308
|
-
shape = "ellipse";
|
|
309
|
-
fillcolor = "lightgray";
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
lines.push(` "${safeId}" [label="${safeLabel}" shape=${shape} style=filled fillcolor="${fillcolor}"];`);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
for (const edge of this.edges) {
|
|
316
|
-
if (nodeIds.has(edge.source) && nodeIds.has(edge.target)) {
|
|
317
|
-
const sourceId = this.sanitizeId(edge.source);
|
|
318
|
-
const targetId = this.sanitizeId(edge.target);
|
|
319
|
-
let style: string;
|
|
320
|
-
let color: string;
|
|
321
|
-
|
|
322
|
-
switch (edge.kind) {
|
|
323
|
-
case "imports":
|
|
324
|
-
style = "solid";
|
|
325
|
-
color = "black";
|
|
326
|
-
break;
|
|
327
|
-
case "contains":
|
|
328
|
-
style = "dotted";
|
|
329
|
-
color = "gray";
|
|
330
|
-
break;
|
|
331
|
-
case "extends":
|
|
332
|
-
style = "dashed";
|
|
333
|
-
color = "blue";
|
|
334
|
-
break;
|
|
335
|
-
case "implements":
|
|
336
|
-
style = "dashed";
|
|
337
|
-
color = "green";
|
|
338
|
-
break;
|
|
339
|
-
default:
|
|
340
|
-
style = "solid";
|
|
341
|
-
color = "black";
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
lines.push(` "${sourceId}" -> "${targetId}" [style=${style} color="${color}"];`);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
lines.push("}");
|
|
349
|
-
return lines.join("\n");
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Generate a PlantUML graph diagram string
|
|
354
|
-
*/
|
|
355
|
-
toPlantUML(targetFile?: string): string {
|
|
356
|
-
const lines: string[] = [
|
|
357
|
-
"@startuml",
|
|
358
|
-
"skinparam linetype ortho",
|
|
359
|
-
];
|
|
360
|
-
|
|
361
|
-
const nodesToInclude = targetFile
|
|
362
|
-
? this.nodes.filter((n: GraphNode) => {
|
|
363
|
-
const lowerTarget = targetFile.toLowerCase().replace(/\\/g, "/");
|
|
364
|
-
const lowerPath = n.filePath.toLowerCase().replace(/\\/g, "/");
|
|
365
|
-
const basename = lowerPath.split("/").pop() ?? "";
|
|
366
|
-
return basename.includes(lowerTarget) ||
|
|
367
|
-
lowerPath.split("/").some(seg => seg.includes(lowerTarget));
|
|
368
|
-
})
|
|
369
|
-
: this.nodes;
|
|
370
|
-
|
|
371
|
-
const nodeIds = new Set(nodesToInclude.map((n: GraphNode) => n.id));
|
|
372
|
-
|
|
373
|
-
for (const node of nodesToInclude) {
|
|
374
|
-
const safeId = this.sanitizeId(node.id);
|
|
375
|
-
const safeLabel = this.sanitizePlantUMLText(`${node.kind}: ${node.label}`);
|
|
376
|
-
|
|
377
|
-
switch (node.kind) {
|
|
378
|
-
case "file":
|
|
379
|
-
lines.push(`[${safeLabel}] as ${safeId}`);
|
|
380
|
-
break;
|
|
381
|
-
case "function":
|
|
382
|
-
lines.push(`() "${safeLabel}" as ${safeId}`);
|
|
383
|
-
break;
|
|
384
|
-
case "class":
|
|
385
|
-
lines.push(`interface "${safeLabel}" as ${safeId}`);
|
|
386
|
-
break;
|
|
387
|
-
default:
|
|
388
|
-
lines.push(`[${safeLabel}] as ${safeId}`);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
for (const edge of this.edges) {
|
|
393
|
-
if (nodeIds.has(edge.source) && nodeIds.has(edge.target)) {
|
|
394
|
-
const sourceId = this.sanitizeId(edge.source);
|
|
395
|
-
const targetId = this.sanitizeId(edge.target);
|
|
396
|
-
let arrow: string;
|
|
397
|
-
|
|
398
|
-
switch (edge.kind) {
|
|
399
|
-
case "imports":
|
|
400
|
-
arrow = "-->";
|
|
401
|
-
break;
|
|
402
|
-
case "contains":
|
|
403
|
-
arrow = "*--";
|
|
404
|
-
break;
|
|
405
|
-
case "extends":
|
|
406
|
-
arrow = "--|>";
|
|
407
|
-
break;
|
|
408
|
-
case "implements":
|
|
409
|
-
arrow = "..|>";
|
|
410
|
-
break;
|
|
411
|
-
default:
|
|
412
|
-
arrow = "-->";
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
lines.push(`${sourceId} ${arrow} ${targetId}`);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
lines.push("@enduml");
|
|
420
|
-
return lines.join("\n");
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
private sanitizeDotText(text: string): string {
|
|
424
|
-
return text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
private sanitizePlantUMLText(text: string): string {
|
|
428
|
-
return text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Detect cycles in the graph
|
|
433
|
-
*/
|
|
434
|
-
detectCycles(maxDepth = 10000): string[][] {
|
|
435
|
-
const cycles: string[][] = [];
|
|
436
|
-
const visited = new Set<string>();
|
|
437
|
-
const stackSet = new Set<string>();
|
|
438
|
-
|
|
439
|
-
for (const startNode of this.nodes) {
|
|
440
|
-
if (visited.has(startNode.id)) continue;
|
|
441
|
-
|
|
442
|
-
const workStack: Array<{ node: string; neighbors: string[]; pathIndex: number }> = [];
|
|
443
|
-
const path: string[] = [];
|
|
444
|
-
|
|
445
|
-
workStack.push({
|
|
446
|
-
node: startNode.id,
|
|
447
|
-
neighbors: this.graph.outNeighbors(startNode.id),
|
|
448
|
-
pathIndex: 0,
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
while (workStack.length > 0) {
|
|
452
|
-
if (workStack.length > maxDepth) break;
|
|
453
|
-
|
|
454
|
-
const frame = workStack[workStack.length - 1];
|
|
455
|
-
|
|
456
|
-
if (frame.pathIndex === 0) {
|
|
457
|
-
if (stackSet.has(frame.node)) {
|
|
458
|
-
const cycleStart = path.indexOf(frame.node);
|
|
459
|
-
if (cycleStart !== -1) {
|
|
460
|
-
cycles.push([...path.slice(cycleStart), frame.node]);
|
|
461
|
-
}
|
|
462
|
-
stackSet.delete(frame.node);
|
|
463
|
-
path.pop();
|
|
464
|
-
workStack.pop();
|
|
465
|
-
continue;
|
|
466
|
-
}
|
|
467
|
-
if (visited.has(frame.node)) {
|
|
468
|
-
workStack.pop();
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
stackSet.add(frame.node);
|
|
472
|
-
path.push(frame.node);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (frame.pathIndex < frame.neighbors.length) {
|
|
476
|
-
visited.add(frame.node);
|
|
477
|
-
const neighbor = frame.neighbors[frame.pathIndex];
|
|
478
|
-
frame.pathIndex++;
|
|
479
|
-
workStack.push({
|
|
480
|
-
node: neighbor,
|
|
481
|
-
neighbors: this.graph.outNeighbors(neighbor),
|
|
482
|
-
pathIndex: 0,
|
|
483
|
-
});
|
|
484
|
-
} else {
|
|
485
|
-
stackSet.delete(frame.node);
|
|
486
|
-
path.pop();
|
|
487
|
-
workStack.pop();
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
return cycles;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
private sanitizeId(id: string): string {
|
|
496
|
-
return id.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
private sanitizeMermaidText(text: string): string {
|
|
500
|
-
return text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%%/g, "\\%%");
|
|
501
|
-
}
|
|
502
|
-
}
|