@kodus/kodus-graph 0.2.8 → 0.2.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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +252 -0
  3. package/dist/analysis/blast-radius.d.ts +2 -0
  4. package/dist/analysis/blast-radius.js +55 -0
  5. package/dist/analysis/communities.d.ts +28 -0
  6. package/dist/analysis/communities.js +100 -0
  7. package/dist/analysis/context-builder.d.ts +34 -0
  8. package/dist/analysis/context-builder.js +92 -0
  9. package/dist/analysis/diff.d.ts +41 -0
  10. package/dist/analysis/diff.js +155 -0
  11. package/dist/analysis/enrich.d.ts +5 -0
  12. package/dist/analysis/enrich.js +126 -0
  13. package/dist/analysis/flows.d.ts +27 -0
  14. package/dist/analysis/flows.js +86 -0
  15. package/dist/analysis/inheritance.d.ts +3 -0
  16. package/dist/analysis/inheritance.js +31 -0
  17. package/dist/analysis/prompt-formatter.d.ts +2 -0
  18. package/dist/analysis/prompt-formatter.js +173 -0
  19. package/dist/analysis/risk-score.d.ts +4 -0
  20. package/dist/analysis/risk-score.js +51 -0
  21. package/dist/analysis/search.d.ts +11 -0
  22. package/dist/analysis/search.js +64 -0
  23. package/dist/analysis/test-gaps.d.ts +2 -0
  24. package/dist/analysis/test-gaps.js +14 -0
  25. package/dist/cli.d.ts +2 -0
  26. package/dist/cli.js +210 -0
  27. package/dist/commands/analyze.d.ts +9 -0
  28. package/dist/commands/analyze.js +116 -0
  29. package/dist/commands/communities.d.ts +8 -0
  30. package/dist/commands/communities.js +9 -0
  31. package/dist/commands/context.d.ts +12 -0
  32. package/dist/commands/context.js +130 -0
  33. package/dist/commands/diff.d.ts +9 -0
  34. package/dist/commands/diff.js +89 -0
  35. package/dist/commands/flows.d.ts +8 -0
  36. package/dist/commands/flows.js +9 -0
  37. package/dist/commands/parse.d.ts +11 -0
  38. package/dist/commands/parse.js +101 -0
  39. package/dist/commands/search.d.ts +12 -0
  40. package/dist/commands/search.js +27 -0
  41. package/dist/commands/update.d.ts +7 -0
  42. package/dist/commands/update.js +154 -0
  43. package/dist/graph/builder.d.ts +6 -0
  44. package/dist/graph/builder.js +248 -0
  45. package/dist/graph/edges.d.ts +23 -0
  46. package/dist/graph/edges.js +159 -0
  47. package/dist/graph/json-writer.d.ts +9 -0
  48. package/dist/graph/json-writer.js +38 -0
  49. package/dist/graph/loader.d.ts +13 -0
  50. package/dist/graph/loader.js +101 -0
  51. package/dist/graph/merger.d.ts +7 -0
  52. package/dist/graph/merger.js +18 -0
  53. package/dist/graph/types.d.ts +252 -0
  54. package/dist/graph/types.js +1 -0
  55. package/dist/parser/batch.d.ts +5 -0
  56. package/dist/parser/batch.js +93 -0
  57. package/dist/parser/discovery.d.ts +7 -0
  58. package/dist/parser/discovery.js +61 -0
  59. package/dist/parser/extractor.d.ts +4 -0
  60. package/dist/parser/extractor.js +33 -0
  61. package/dist/parser/extractors/generic.d.ts +8 -0
  62. package/dist/parser/extractors/generic.js +471 -0
  63. package/dist/parser/extractors/python.d.ts +8 -0
  64. package/dist/parser/extractors/python.js +133 -0
  65. package/dist/parser/extractors/ruby.d.ts +8 -0
  66. package/dist/parser/extractors/ruby.js +153 -0
  67. package/dist/parser/extractors/typescript.d.ts +10 -0
  68. package/dist/parser/extractors/typescript.js +365 -0
  69. package/dist/parser/languages.d.ts +32 -0
  70. package/dist/parser/languages.js +304 -0
  71. package/dist/resolver/call-resolver.d.ts +36 -0
  72. package/dist/resolver/call-resolver.js +178 -0
  73. package/dist/resolver/external-detector.d.ts +11 -0
  74. package/dist/resolver/external-detector.js +820 -0
  75. package/dist/resolver/fs-cache.d.ts +8 -0
  76. package/dist/resolver/fs-cache.js +36 -0
  77. package/dist/resolver/import-map.d.ts +12 -0
  78. package/dist/resolver/import-map.js +21 -0
  79. package/dist/resolver/import-resolver.d.ts +19 -0
  80. package/dist/resolver/import-resolver.js +310 -0
  81. package/dist/resolver/languages/csharp.d.ts +3 -0
  82. package/dist/resolver/languages/csharp.js +94 -0
  83. package/dist/resolver/languages/go.d.ts +3 -0
  84. package/dist/resolver/languages/go.js +197 -0
  85. package/dist/resolver/languages/java.d.ts +1 -0
  86. package/dist/resolver/languages/java.js +193 -0
  87. package/dist/resolver/languages/php.d.ts +3 -0
  88. package/dist/resolver/languages/php.js +75 -0
  89. package/dist/resolver/languages/python.d.ts +11 -0
  90. package/dist/resolver/languages/python.js +127 -0
  91. package/dist/resolver/languages/ruby.d.ts +24 -0
  92. package/dist/resolver/languages/ruby.js +110 -0
  93. package/dist/resolver/languages/rust.d.ts +1 -0
  94. package/dist/resolver/languages/rust.js +197 -0
  95. package/dist/resolver/languages/typescript.d.ts +35 -0
  96. package/dist/resolver/languages/typescript.js +416 -0
  97. package/dist/resolver/re-export-resolver.d.ts +24 -0
  98. package/dist/resolver/re-export-resolver.js +57 -0
  99. package/dist/resolver/symbol-table.d.ts +17 -0
  100. package/dist/resolver/symbol-table.js +60 -0
  101. package/dist/shared/extract-calls.d.ts +26 -0
  102. package/dist/shared/extract-calls.js +57 -0
  103. package/dist/shared/file-hash.d.ts +3 -0
  104. package/dist/shared/file-hash.js +10 -0
  105. package/dist/shared/filters.d.ts +3 -0
  106. package/dist/shared/filters.js +240 -0
  107. package/dist/shared/logger.d.ts +6 -0
  108. package/dist/shared/logger.js +17 -0
  109. package/dist/shared/qualified-name.d.ts +1 -0
  110. package/dist/shared/qualified-name.js +9 -0
  111. package/dist/shared/safe-path.d.ts +6 -0
  112. package/dist/shared/safe-path.js +29 -0
  113. package/dist/shared/schemas.d.ts +43 -0
  114. package/dist/shared/schemas.js +30 -0
  115. package/dist/shared/temp.d.ts +11 -0
  116. package/{src/shared/temp.ts → dist/shared/temp.js} +4 -5
  117. package/package.json +20 -6
  118. package/src/analysis/blast-radius.ts +0 -54
  119. package/src/analysis/communities.ts +0 -135
  120. package/src/analysis/context-builder.ts +0 -130
  121. package/src/analysis/diff.ts +0 -169
  122. package/src/analysis/enrich.ts +0 -110
  123. package/src/analysis/flows.ts +0 -112
  124. package/src/analysis/inheritance.ts +0 -34
  125. package/src/analysis/prompt-formatter.ts +0 -175
  126. package/src/analysis/risk-score.ts +0 -62
  127. package/src/analysis/search.ts +0 -76
  128. package/src/analysis/test-gaps.ts +0 -21
  129. package/src/cli.ts +0 -210
  130. package/src/commands/analyze.ts +0 -128
  131. package/src/commands/communities.ts +0 -19
  132. package/src/commands/context.ts +0 -182
  133. package/src/commands/diff.ts +0 -96
  134. package/src/commands/flows.ts +0 -19
  135. package/src/commands/parse.ts +0 -124
  136. package/src/commands/search.ts +0 -41
  137. package/src/commands/update.ts +0 -166
  138. package/src/graph/builder.ts +0 -209
  139. package/src/graph/edges.ts +0 -101
  140. package/src/graph/json-writer.ts +0 -43
  141. package/src/graph/loader.ts +0 -113
  142. package/src/graph/merger.ts +0 -25
  143. package/src/graph/types.ts +0 -283
  144. package/src/parser/batch.ts +0 -82
  145. package/src/parser/discovery.ts +0 -75
  146. package/src/parser/extractor.ts +0 -37
  147. package/src/parser/extractors/generic.ts +0 -132
  148. package/src/parser/extractors/python.ts +0 -133
  149. package/src/parser/extractors/ruby.ts +0 -147
  150. package/src/parser/extractors/typescript.ts +0 -350
  151. package/src/parser/languages.ts +0 -122
  152. package/src/resolver/call-resolver.ts +0 -244
  153. package/src/resolver/import-map.ts +0 -27
  154. package/src/resolver/import-resolver.ts +0 -72
  155. package/src/resolver/languages/csharp.ts +0 -7
  156. package/src/resolver/languages/go.ts +0 -7
  157. package/src/resolver/languages/java.ts +0 -7
  158. package/src/resolver/languages/php.ts +0 -7
  159. package/src/resolver/languages/python.ts +0 -35
  160. package/src/resolver/languages/ruby.ts +0 -21
  161. package/src/resolver/languages/rust.ts +0 -7
  162. package/src/resolver/languages/typescript.ts +0 -168
  163. package/src/resolver/re-export-resolver.ts +0 -66
  164. package/src/resolver/symbol-table.ts +0 -67
  165. package/src/shared/extract-calls.ts +0 -75
  166. package/src/shared/file-hash.ts +0 -12
  167. package/src/shared/filters.ts +0 -243
  168. package/src/shared/logger.ts +0 -17
  169. package/src/shared/qualified-name.ts +0 -5
  170. package/src/shared/safe-path.ts +0 -31
  171. package/src/shared/schemas.ts +0 -32
@@ -1,130 +0,0 @@
1
- import { performance } from 'perf_hooks';
2
- import { type IndexedGraph, indexGraph } from '../graph/loader';
3
- import type {
4
- AffectedFlow,
5
- ContextAnalysisMetadata,
6
- GraphData,
7
- GraphEdge,
8
- GraphNode,
9
- ParseMetadata,
10
- } from '../graph/types';
11
- import { computeBlastRadius } from './blast-radius';
12
- import { computeStructuralDiff, type DiffResult } from './diff';
13
- import { enrichChangedFunctions } from './enrich';
14
- import { detectFlows } from './flows';
15
- import { extractInheritance } from './inheritance';
16
- import { computeRiskScore } from './risk-score';
17
- import { findTestGaps } from './test-gaps';
18
-
19
- export interface ContextV2Output {
20
- graph: {
21
- nodes: GraphNode[];
22
- edges: GraphEdge[];
23
- metadata: ParseMetadata;
24
- };
25
- analysis: {
26
- changed_functions: ReturnType<typeof enrichChangedFunctions>;
27
- structural_diff: DiffResult;
28
- blast_radius: ReturnType<typeof computeBlastRadius>;
29
- affected_flows: AffectedFlow[];
30
- inheritance: ReturnType<typeof extractInheritance>;
31
- test_gaps: ReturnType<typeof findTestGaps>;
32
- risk: ReturnType<typeof computeRiskScore>;
33
- metadata: ContextAnalysisMetadata;
34
- };
35
- }
36
-
37
- interface BuildContextV2Options {
38
- mergedGraph: GraphData;
39
- oldGraph: GraphData | null;
40
- changedFiles: string[];
41
- minConfidence: number;
42
- maxDepth: number;
43
- }
44
-
45
- export function buildContextV2(opts: BuildContextV2Options): ContextV2Output {
46
- const t0 = performance.now();
47
- const { mergedGraph, oldGraph, changedFiles, minConfidence, maxDepth } = opts;
48
-
49
- // Phase 1: Index
50
- const indexed = indexGraph(mergedGraph);
51
- const oldIndexed: IndexedGraph = oldGraph ? indexGraph(oldGraph) : indexGraph({ nodes: [], edges: [] });
52
-
53
- // Phase 2: Independent analyses
54
- const changedSet = new Set(changedFiles);
55
- const newNodesInChanged = mergedGraph.nodes.filter((n) => changedSet.has(n.file_path));
56
- const newEdgesInChanged = mergedGraph.edges.filter((e) => changedSet.has(e.file_path));
57
-
58
- const structuralDiff = computeStructuralDiff(oldIndexed, newNodesInChanged, newEdgesInChanged, changedFiles);
59
- const blastRadius = computeBlastRadius(mergedGraph, changedFiles, maxDepth);
60
- const allFlows = detectFlows(indexed, { maxDepth: 10, type: 'all' });
61
- const testGaps = findTestGaps(mergedGraph, changedFiles);
62
- const risk = computeRiskScore(mergedGraph, changedFiles, blastRadius);
63
- const inheritance = extractInheritance(indexed, changedFiles);
64
-
65
- // Phase 3: Filter affected flows
66
- const changedFuncSet = new Set(
67
- mergedGraph.nodes.filter((n) => changedSet.has(n.file_path) && !n.is_test).map((n) => n.qualified_name),
68
- );
69
-
70
- const affectedFlows: AffectedFlow[] = [];
71
- for (const flow of allFlows.flows) {
72
- const touches = flow.path.filter((qn) => changedFuncSet.has(qn));
73
- if (touches.length > 0) {
74
- affectedFlows.push({
75
- entry_point: flow.entry_point,
76
- type: flow.type,
77
- touches_changed: touches,
78
- depth: flow.depth,
79
- path: flow.path,
80
- });
81
- }
82
- }
83
-
84
- // Phase 3: Enrichment
85
- const enriched = enrichChangedFunctions(indexed, changedFiles, structuralDiff, allFlows.flows, minConfidence);
86
-
87
- // Phase 4: Assembly
88
- const totalCallers = enriched.reduce((s, f) => s + f.callers.length, 0);
89
- const totalCallees = enriched.reduce((s, f) => s + f.callees.length, 0);
90
-
91
- const metadata: ContextAnalysisMetadata = {
92
- changed_functions_count: enriched.length,
93
- total_callers: totalCallers,
94
- total_callees: totalCallees,
95
- untested_count: testGaps.length,
96
- affected_flows_count: affectedFlows.length,
97
- duration_ms: Math.round(performance.now() - t0),
98
- min_confidence: minConfidence,
99
- };
100
-
101
- const graphMetadata: ParseMetadata = indexed.metadata.repo_dir
102
- ? indexed.metadata
103
- : {
104
- repo_dir: '',
105
- files_parsed: changedFiles.length,
106
- total_nodes: mergedGraph.nodes.length,
107
- total_edges: mergedGraph.edges.length,
108
- duration_ms: 0,
109
- parse_errors: 0,
110
- extract_errors: 0,
111
- };
112
-
113
- return {
114
- graph: {
115
- nodes: mergedGraph.nodes,
116
- edges: mergedGraph.edges,
117
- metadata: graphMetadata,
118
- },
119
- analysis: {
120
- changed_functions: enriched,
121
- structural_diff: structuralDiff,
122
- blast_radius: blastRadius,
123
- affected_flows: affectedFlows,
124
- inheritance,
125
- test_gaps: testGaps,
126
- risk,
127
- metadata,
128
- },
129
- };
130
- }
@@ -1,169 +0,0 @@
1
- import type { IndexedGraph } from '../graph/loader';
2
- import type { GraphEdge, GraphNode } from '../graph/types';
3
- import { log } from '../shared/logger';
4
-
5
- export interface NodeChange {
6
- qualified_name: string;
7
- kind: string;
8
- file_path: string;
9
- line_start: number;
10
- line_end: number;
11
- }
12
-
13
- export interface ModifiedNode {
14
- qualified_name: string;
15
- changes: string[];
16
- }
17
-
18
- export interface DiffResult {
19
- changed_files: string[];
20
- summary: { added: number; removed: number; modified: number };
21
- nodes: { added: NodeChange[]; removed: NodeChange[]; modified: ModifiedNode[] };
22
- edges: {
23
- added: Pick<GraphEdge, 'kind' | 'source_qualified' | 'target_qualified'>[];
24
- removed: Pick<GraphEdge, 'kind' | 'source_qualified' | 'target_qualified'>[];
25
- };
26
- risk_by_file: Record<string, { dependents: number; risk: 'HIGH' | 'MEDIUM' | 'LOW' }>;
27
- }
28
-
29
- export function computeStructuralDiff(
30
- oldGraph: IndexedGraph,
31
- newNodes: GraphNode[],
32
- newEdges: GraphEdge[],
33
- changedFiles: string[],
34
- ): DiffResult {
35
- const changedSet = new Set(changedFiles);
36
-
37
- // Old nodes in changed files
38
- const oldNodesInChanged = new Map<string, GraphNode>();
39
- for (const n of oldGraph.nodes) {
40
- if (changedSet.has(n.file_path)) oldNodesInChanged.set(n.qualified_name, n);
41
- }
42
-
43
- // New nodes in changed files
44
- const newNodesMap = new Map<string, GraphNode>();
45
- for (const n of newNodes) {
46
- if (changedSet.has(n.file_path)) newNodesMap.set(n.qualified_name, n);
47
- }
48
-
49
- log.debug('diff: input', {
50
- oldNodesInChanged: oldNodesInChanged.size,
51
- newNodesInChanged: newNodesMap.size,
52
- changedFiles,
53
- });
54
-
55
- // Classify nodes
56
- const added: NodeChange[] = [];
57
- const removed: NodeChange[] = [];
58
- const modified: ModifiedNode[] = [];
59
-
60
- for (const [qn, n] of newNodesMap) {
61
- if (!oldNodesInChanged.has(qn)) {
62
- added.push({
63
- qualified_name: qn,
64
- kind: n.kind,
65
- file_path: n.file_path,
66
- line_start: n.line_start,
67
- line_end: n.line_end,
68
- });
69
- }
70
- }
71
-
72
- for (const [qn, n] of oldNodesInChanged) {
73
- if (!newNodesMap.has(qn)) {
74
- removed.push({
75
- qualified_name: qn,
76
- kind: n.kind,
77
- file_path: n.file_path,
78
- line_start: n.line_start,
79
- line_end: n.line_end,
80
- });
81
- } else {
82
- const newN = newNodesMap.get(qn)!;
83
- const changes: string[] = [];
84
- // Detect real content changes vs. pure displacement.
85
- // content_hash = SHA256 of the node's source text (position-independent).
86
- if (n.content_hash && newN.content_hash) {
87
- // Definitive: hash comparison catches ALL content changes,
88
- // even same-line-count edits (e.g. `return 1` → `return 2`).
89
- if (n.content_hash !== newN.content_hash) {
90
- changes.push('body');
91
- log.debug('diff: body change detected', {
92
- node: qn,
93
- oldHash: n.content_hash.substring(0, 8),
94
- newHash: newN.content_hash.substring(0, 8),
95
- });
96
- } else {
97
- log.debug('diff: hash match (displacement only)', {
98
- node: qn,
99
- oldLines: `${n.line_start}-${n.line_end}`,
100
- newLines: `${newN.line_start}-${newN.line_end}`,
101
- });
102
- }
103
- } else if (n.line_start !== newN.line_start || n.line_end !== newN.line_end) {
104
- // Fallback (legacy data without content_hash): size heuristic.
105
- const oldSize = n.line_end - n.line_start;
106
- const newSize = newN.line_end - newN.line_start;
107
- if (oldSize !== newSize) {
108
- changes.push('line_range');
109
- log.debug('diff: line_range fallback (no content_hash)', {
110
- node: qn,
111
- hasOldHash: !!n.content_hash,
112
- hasNewHash: !!newN.content_hash,
113
- oldSize,
114
- newSize,
115
- });
116
- }
117
- }
118
- if ((n.params || '') !== (newN.params || '')) changes.push('params');
119
- if ((n.return_type || '') !== (newN.return_type || '')) changes.push('return_type');
120
- if (changes.length > 0) modified.push({ qualified_name: qn, changes });
121
- }
122
- }
123
-
124
- // Classify edges
125
- const oldEdgesInChanged = oldGraph.edges.filter((e) => changedSet.has(e.file_path));
126
- const oldEdgeKeys = new Set(oldEdgesInChanged.map((e) => `${e.kind}|${e.source_qualified}|${e.target_qualified}`));
127
- const newEdgesInChanged = newEdges.filter((e) => changedSet.has(e.file_path));
128
- const newEdgeKeys = new Set(newEdgesInChanged.map((e) => `${e.kind}|${e.source_qualified}|${e.target_qualified}`));
129
-
130
- const addedEdges = newEdgesInChanged
131
- .filter((e) => !oldEdgeKeys.has(`${e.kind}|${e.source_qualified}|${e.target_qualified}`))
132
- .map((e) => ({ kind: e.kind, source_qualified: e.source_qualified, target_qualified: e.target_qualified }));
133
-
134
- const removedEdges = oldEdgesInChanged
135
- .filter((e) => !newEdgeKeys.has(`${e.kind}|${e.source_qualified}|${e.target_qualified}`))
136
- .map((e) => ({ kind: e.kind, source_qualified: e.source_qualified, target_qualified: e.target_qualified }));
137
-
138
- // Risk by file: count unique dependents via reverse adjacency
139
- const riskByFile: Record<string, { dependents: number; risk: 'HIGH' | 'MEDIUM' | 'LOW' }> = {};
140
- for (const file of changedFiles) {
141
- const nodesInFile = oldGraph.byFile.get(file) || [];
142
- const dependents = new Set<string>();
143
- for (const n of nodesInFile) {
144
- for (const edge of oldGraph.reverseAdjacency.get(n.qualified_name) || []) {
145
- if (!changedSet.has(edge.file_path)) dependents.add(edge.source_qualified);
146
- }
147
- }
148
- const count = dependents.size;
149
- const risk = count >= 10 ? 'HIGH' : count >= 3 ? 'MEDIUM' : 'LOW';
150
- riskByFile[file] = { dependents: count, risk };
151
- }
152
-
153
- log.info('diff: result', {
154
- added: added.length,
155
- removed: removed.length,
156
- modified: modified.length,
157
- edgesAdded: addedEdges.length,
158
- edgesRemoved: removedEdges.length,
159
- modifiedDetails: modified.map((m) => `${m.qualified_name} [${m.changes.join(',')}]`),
160
- });
161
-
162
- return {
163
- changed_files: changedFiles,
164
- summary: { added: added.length, removed: removed.length, modified: modified.length },
165
- nodes: { added, removed, modified },
166
- edges: { added: addedEdges, removed: removedEdges },
167
- risk_by_file: riskByFile,
168
- };
169
- }
@@ -1,110 +0,0 @@
1
- import type { IndexedGraph } from '../graph/loader';
2
- import type { CalleeRef, CallerRef, EnrichedFunction } from '../graph/types';
3
- import type { DiffResult } from './diff';
4
- import type { Flow } from './flows';
5
-
6
- export function enrichChangedFunctions(
7
- graph: IndexedGraph,
8
- changedFiles: string[],
9
- diff: DiffResult,
10
- allFlows: Flow[],
11
- minConfidence: number,
12
- ): EnrichedFunction[] {
13
- const changedSet = new Set(changedFiles);
14
-
15
- // Pre-index diff results
16
- const addedSet = new Set(diff.nodes.added.map((n) => n.qualified_name));
17
- const modifiedMap = new Map(diff.nodes.modified.map((m) => [m.qualified_name, m.changes]));
18
-
19
- // Pre-index TESTED_BY
20
- const testedFiles = new Set(graph.edges.filter((e) => e.kind === 'TESTED_BY').map((e) => e.source_qualified));
21
-
22
- // Pre-index flows by function
23
- const flowsByFunction = new Map<string, string[]>();
24
- for (const flow of allFlows) {
25
- for (const qn of flow.path) {
26
- const list = flowsByFunction.get(qn);
27
- if (list) {
28
- if (!list.includes(flow.entry_point)) list.push(flow.entry_point);
29
- } else {
30
- flowsByFunction.set(qn, [flow.entry_point]);
31
- }
32
- }
33
- }
34
-
35
- // Filter functions in changed files
36
- const changedFunctions = graph.nodes.filter(
37
- (n) =>
38
- changedSet.has(n.file_path) &&
39
- !n.is_test &&
40
- n.kind !== 'Constructor' &&
41
- n.kind !== 'Class' &&
42
- n.kind !== 'Interface' &&
43
- n.kind !== 'Enum',
44
- );
45
-
46
- return changedFunctions
47
- .sort((a, b) => a.file_path.localeCompare(b.file_path) || a.line_start - b.line_start)
48
- .map((node) => {
49
- // Callers
50
- const callers: CallerRef[] = [];
51
- for (const edge of graph.reverseAdjacency.get(node.qualified_name) || []) {
52
- if (edge.kind !== 'CALLS') continue;
53
- // null/undefined confidence = high confidence (edge came from DB or parser without scoring)
54
- if ((edge.confidence ?? 1.0) < minConfidence) continue;
55
- const sourceNode = graph.byQualified.get(edge.source_qualified);
56
- callers.push({
57
- qualified_name: edge.source_qualified,
58
- name: sourceNode?.name || edge.source_qualified.split('::').pop() || 'unknown',
59
- file_path: sourceNode?.file_path || edge.file_path,
60
- line: edge.line,
61
- confidence: edge.confidence ?? 1.0,
62
- });
63
- }
64
-
65
- // Callees
66
- const callees: CalleeRef[] = [];
67
- const seenCallees = new Set<string>();
68
- for (const edge of graph.adjacency.get(node.qualified_name) || []) {
69
- if (edge.kind !== 'CALLS') continue;
70
- if (seenCallees.has(edge.target_qualified)) continue;
71
- seenCallees.add(edge.target_qualified);
72
- const targetNode = graph.byQualified.get(edge.target_qualified);
73
- const name = targetNode?.name || edge.target_qualified.split('::').pop() || 'unknown';
74
- const params = targetNode?.params && targetNode.params !== '()' ? targetNode.params : '';
75
- const ret = targetNode?.return_type ? ` -> ${targetNode.return_type}` : '';
76
- callees.push({
77
- qualified_name: edge.target_qualified,
78
- name,
79
- file_path: targetNode?.file_path || '',
80
- signature: `${name}${params}${ret}`,
81
- });
82
- }
83
-
84
- // Signature
85
- const shortName = node.name.includes('.') ? node.name.split('.').pop()! : node.name;
86
- const params = node.params && node.params !== '()' ? node.params : '';
87
- const ret = node.return_type ? ` -> ${node.return_type}` : '';
88
- const signature = `${shortName}${params}${ret}`;
89
-
90
- // Diff
91
- const isNew = addedSet.has(node.qualified_name);
92
- const diffChanges = isNew ? [] : modifiedMap.get(node.qualified_name) || [];
93
-
94
- return {
95
- qualified_name: node.qualified_name,
96
- name: node.name,
97
- kind: node.kind,
98
- signature,
99
- file_path: node.file_path,
100
- line_start: node.line_start,
101
- line_end: node.line_end,
102
- callers,
103
- callees,
104
- has_test_coverage: testedFiles.has(node.file_path),
105
- diff_changes: diffChanges,
106
- is_new: isNew,
107
- in_flows: flowsByFunction.get(node.qualified_name) || [],
108
- };
109
- });
110
- }
@@ -1,112 +0,0 @@
1
- import type { IndexedGraph } from '../graph/loader';
2
-
3
- export interface FlowOptions {
4
- maxDepth: number;
5
- type: 'test' | 'http' | 'all';
6
- }
7
-
8
- export interface Flow {
9
- entry_point: string;
10
- type: 'test' | 'http';
11
- depth: number;
12
- node_count: number;
13
- file_count: number;
14
- criticality: number;
15
- path: string[];
16
- }
17
-
18
- export interface FlowsResult {
19
- flows: Flow[];
20
- summary: {
21
- total_flows: number;
22
- by_type: { test: number; http: number };
23
- avg_depth: number;
24
- max_criticality: number;
25
- };
26
- }
27
-
28
- const HTTP_METHOD_NAMES = new Set(['get', 'post', 'put', 'delete', 'patch', 'handle', 'handler']);
29
-
30
- function isHttpHandler(_qualifiedName: string, name: string, parentName?: string): boolean {
31
- if (HTTP_METHOD_NAMES.has(name.toLowerCase())) return true;
32
- if (parentName?.toLowerCase().endsWith('controller')) return true;
33
- return false;
34
- }
35
-
36
- export function detectFlows(graph: IndexedGraph, opts: FlowOptions): FlowsResult {
37
- const { maxDepth, type } = opts;
38
-
39
- // Find entry points
40
- const entryPoints: { qualified: string; type: 'test' | 'http' }[] = [];
41
-
42
- for (const node of graph.nodes) {
43
- if (type !== 'http' && node.kind === 'Test') {
44
- entryPoints.push({ qualified: node.qualified_name, type: 'test' });
45
- }
46
- if (type !== 'test' && (node.kind === 'Method' || node.kind === 'Function')) {
47
- if (isHttpHandler(node.qualified_name, node.name, node.parent_name)) {
48
- entryPoints.push({ qualified: node.qualified_name, type: 'http' });
49
- }
50
- }
51
- }
52
-
53
- // BFS for each entry point
54
- const flows: Flow[] = [];
55
-
56
- for (const ep of entryPoints) {
57
- const path: string[] = [ep.qualified];
58
- const visited = new Set<string>([ep.qualified]);
59
- const files = new Set<string>();
60
-
61
- const startNode = graph.byQualified.get(ep.qualified);
62
- if (startNode) files.add(startNode.file_path);
63
-
64
- let frontier = [ep.qualified];
65
- let depth = 0;
66
-
67
- while (frontier.length > 0 && depth < maxDepth) {
68
- const next: string[] = [];
69
- for (const q of frontier) {
70
- for (const edge of graph.adjacency.get(q) || []) {
71
- if (edge.kind !== 'CALLS') continue;
72
- if (visited.has(edge.target_qualified)) continue;
73
- visited.add(edge.target_qualified);
74
- next.push(edge.target_qualified);
75
- path.push(edge.target_qualified);
76
- const targetNode = graph.byQualified.get(edge.target_qualified);
77
- if (targetNode) files.add(targetNode.file_path);
78
- }
79
- }
80
- if (next.length === 0) break;
81
- frontier = next;
82
- depth++;
83
- }
84
-
85
- flows.push({
86
- entry_point: ep.qualified,
87
- type: ep.type,
88
- depth,
89
- node_count: visited.size,
90
- file_count: files.size,
91
- criticality: visited.size * files.size,
92
- path,
93
- });
94
- }
95
-
96
- flows.sort((a, b) => b.criticality - a.criticality);
97
-
98
- const testFlows = flows.filter((f) => f.type === 'test').length;
99
- const httpFlows = flows.filter((f) => f.type === 'http').length;
100
- const avgDepth = flows.length > 0 ? Math.round((flows.reduce((s, f) => s + f.depth, 0) / flows.length) * 10) / 10 : 0;
101
- const maxCriticality = flows.length > 0 ? flows[0].criticality : 0;
102
-
103
- return {
104
- flows,
105
- summary: {
106
- total_flows: flows.length,
107
- by_type: { test: testFlows, http: httpFlows },
108
- avg_depth: avgDepth,
109
- max_criticality: maxCriticality,
110
- },
111
- };
112
- }
@@ -1,34 +0,0 @@
1
- import type { IndexedGraph } from '../graph/loader';
2
- import type { InheritanceEntry } from '../graph/types';
3
-
4
- export function extractInheritance(graph: IndexedGraph, changedFiles: string[]): InheritanceEntry[] {
5
- const changedSet = new Set(changedFiles);
6
- const entries: InheritanceEntry[] = [];
7
-
8
- const changedClasses = graph.nodes.filter((n) => changedSet.has(n.file_path) && n.kind === 'Class');
9
-
10
- for (const cls of changedClasses) {
11
- let extendsClass: string | undefined;
12
- const implementsList: string[] = [];
13
- const children: string[] = [];
14
-
15
- for (const edge of graph.adjacency.get(cls.qualified_name) || []) {
16
- if (edge.kind === 'INHERITS') extendsClass = edge.target_qualified;
17
- if (edge.kind === 'IMPLEMENTS') implementsList.push(edge.target_qualified);
18
- }
19
-
20
- for (const edge of graph.reverseAdjacency.get(cls.qualified_name) || []) {
21
- if (edge.kind === 'INHERITS') children.push(edge.source_qualified);
22
- }
23
-
24
- entries.push({
25
- qualified_name: cls.qualified_name,
26
- file_path: cls.file_path,
27
- extends: extendsClass,
28
- implements: implementsList,
29
- children,
30
- });
31
- }
32
-
33
- return entries;
34
- }