@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +1 -0
  3. package/bin/code-mapper.mjs +86 -0
  4. package/dist/graph/GraphAnalyzer.js +32 -65
  5. package/dist/graph/GraphAnalyzer.js.map +1 -1
  6. package/dist/graph/GraphBuilder.js +18 -45
  7. package/dist/graph/GraphBuilder.js.map +1 -1
  8. package/dist/index.js +100 -23
  9. package/dist/index.js.map +1 -1
  10. package/dist/mcp/cache.js +8 -17
  11. package/dist/mcp/cache.js.map +1 -1
  12. package/dist/mcp/resources.js +5 -1
  13. package/dist/mcp/resources.js.map +1 -1
  14. package/dist/mcp/tools.js +190 -35
  15. package/dist/mcp/tools.js.map +1 -1
  16. package/dist/parser/ComplexityAnalyzer.js +19 -2
  17. package/dist/parser/ComplexityAnalyzer.js.map +1 -1
  18. package/dist/parser/FileAnalyzer.js +8 -30
  19. package/dist/parser/FileAnalyzer.js.map +1 -1
  20. package/dist/parser/ProjectParser.js +8 -5
  21. package/dist/parser/ProjectParser.js.map +1 -1
  22. package/dist/parser/ProjectParser.test.js +1 -17
  23. package/dist/parser/ProjectParser.test.js.map +1 -1
  24. package/dist/tui/index.js +239 -0
  25. package/dist/tui/index.js.map +1 -0
  26. package/package.json +82 -35
  27. package/AGENTS.md +0 -174
  28. package/docs/PHASE2_PLAN.md +0 -435
  29. package/fixtures/test-project/calculator.ts +0 -28
  30. package/fixtures/test-project/index.ts +0 -2
  31. package/fixtures/test-project/math.ts +0 -11
  32. package/src/graph/Graph.test.ts +0 -222
  33. package/src/graph/GraphAnalyzer.ts +0 -502
  34. package/src/graph/GraphBuilder.ts +0 -258
  35. package/src/graph/types.ts +0 -42
  36. package/src/index.ts +0 -38
  37. package/src/mcp/cache.ts +0 -89
  38. package/src/mcp/resources.ts +0 -137
  39. package/src/mcp/tools.test.ts +0 -104
  40. package/src/mcp/tools.ts +0 -529
  41. package/src/parser/ComplexityAnalyzer.ts +0 -275
  42. package/src/parser/FileAnalyzer.ts +0 -215
  43. package/src/parser/ProjectParser.test.ts +0 -96
  44. package/src/parser/ProjectParser.ts +0 -172
  45. package/src/parser/types.ts +0 -77
  46. package/src/types/graphology-pagerank.d.ts +0 -20
  47. package/tsconfig.json +0 -17
  48. 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
- }