@kodus/kodus-graph 0.2.7 → 0.2.9

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 (167) 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 +57 -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 +83 -0
  9. package/dist/analysis/diff.d.ts +35 -0
  10. package/dist/analysis/diff.js +140 -0
  11. package/dist/analysis/enrich.d.ts +5 -0
  12. package/dist/analysis/enrich.js +98 -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 +166 -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 +208 -0
  27. package/dist/commands/analyze.d.ts +9 -0
  28. package/dist/commands/analyze.js +114 -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 +10 -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 +2 -0
  44. package/dist/graph/builder.js +216 -0
  45. package/dist/graph/edges.d.ts +19 -0
  46. package/dist/graph/edges.js +105 -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 +249 -0
  54. package/dist/graph/types.js +1 -0
  55. package/dist/parser/batch.d.ts +4 -0
  56. package/dist/parser/batch.js +78 -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 +303 -0
  71. package/dist/resolver/call-resolver.d.ts +36 -0
  72. package/dist/resolver/call-resolver.js +178 -0
  73. package/dist/resolver/import-map.d.ts +12 -0
  74. package/dist/resolver/import-map.js +21 -0
  75. package/dist/resolver/import-resolver.d.ts +19 -0
  76. package/dist/resolver/import-resolver.js +212 -0
  77. package/dist/resolver/languages/csharp.d.ts +1 -0
  78. package/dist/resolver/languages/csharp.js +31 -0
  79. package/dist/resolver/languages/go.d.ts +3 -0
  80. package/dist/resolver/languages/go.js +196 -0
  81. package/dist/resolver/languages/java.d.ts +1 -0
  82. package/dist/resolver/languages/java.js +108 -0
  83. package/dist/resolver/languages/php.d.ts +3 -0
  84. package/dist/resolver/languages/php.js +54 -0
  85. package/dist/resolver/languages/python.d.ts +11 -0
  86. package/dist/resolver/languages/python.js +51 -0
  87. package/dist/resolver/languages/ruby.d.ts +9 -0
  88. package/dist/resolver/languages/ruby.js +59 -0
  89. package/dist/resolver/languages/rust.d.ts +1 -0
  90. package/dist/resolver/languages/rust.js +196 -0
  91. package/dist/resolver/languages/typescript.d.ts +27 -0
  92. package/dist/resolver/languages/typescript.js +240 -0
  93. package/dist/resolver/re-export-resolver.d.ts +24 -0
  94. package/dist/resolver/re-export-resolver.js +57 -0
  95. package/dist/resolver/symbol-table.d.ts +17 -0
  96. package/dist/resolver/symbol-table.js +60 -0
  97. package/dist/shared/extract-calls.d.ts +26 -0
  98. package/dist/shared/extract-calls.js +57 -0
  99. package/dist/shared/file-hash.d.ts +3 -0
  100. package/dist/shared/file-hash.js +10 -0
  101. package/dist/shared/filters.d.ts +3 -0
  102. package/dist/shared/filters.js +240 -0
  103. package/dist/shared/logger.d.ts +6 -0
  104. package/dist/shared/logger.js +17 -0
  105. package/dist/shared/qualified-name.d.ts +1 -0
  106. package/dist/shared/qualified-name.js +9 -0
  107. package/dist/shared/safe-path.d.ts +6 -0
  108. package/dist/shared/safe-path.js +29 -0
  109. package/dist/shared/schemas.d.ts +43 -0
  110. package/dist/shared/schemas.js +30 -0
  111. package/dist/shared/temp.d.ts +11 -0
  112. package/{src/shared/temp.ts → dist/shared/temp.js} +4 -5
  113. package/package.json +20 -6
  114. package/src/analysis/blast-radius.ts +0 -54
  115. package/src/analysis/communities.ts +0 -135
  116. package/src/analysis/context-builder.ts +0 -130
  117. package/src/analysis/diff.ts +0 -131
  118. package/src/analysis/enrich.ts +0 -110
  119. package/src/analysis/flows.ts +0 -112
  120. package/src/analysis/inheritance.ts +0 -34
  121. package/src/analysis/prompt-formatter.ts +0 -175
  122. package/src/analysis/risk-score.ts +0 -62
  123. package/src/analysis/search.ts +0 -76
  124. package/src/analysis/test-gaps.ts +0 -21
  125. package/src/cli.ts +0 -207
  126. package/src/commands/analyze.ts +0 -128
  127. package/src/commands/communities.ts +0 -19
  128. package/src/commands/context.ts +0 -139
  129. package/src/commands/diff.ts +0 -96
  130. package/src/commands/flows.ts +0 -19
  131. package/src/commands/parse.ts +0 -124
  132. package/src/commands/search.ts +0 -41
  133. package/src/commands/update.ts +0 -166
  134. package/src/graph/builder.ts +0 -209
  135. package/src/graph/edges.ts +0 -101
  136. package/src/graph/json-writer.ts +0 -43
  137. package/src/graph/loader.ts +0 -113
  138. package/src/graph/merger.ts +0 -25
  139. package/src/graph/types.ts +0 -283
  140. package/src/parser/batch.ts +0 -82
  141. package/src/parser/discovery.ts +0 -75
  142. package/src/parser/extractor.ts +0 -37
  143. package/src/parser/extractors/generic.ts +0 -132
  144. package/src/parser/extractors/python.ts +0 -133
  145. package/src/parser/extractors/ruby.ts +0 -147
  146. package/src/parser/extractors/typescript.ts +0 -350
  147. package/src/parser/languages.ts +0 -122
  148. package/src/resolver/call-resolver.ts +0 -244
  149. package/src/resolver/import-map.ts +0 -27
  150. package/src/resolver/import-resolver.ts +0 -72
  151. package/src/resolver/languages/csharp.ts +0 -7
  152. package/src/resolver/languages/go.ts +0 -7
  153. package/src/resolver/languages/java.ts +0 -7
  154. package/src/resolver/languages/php.ts +0 -7
  155. package/src/resolver/languages/python.ts +0 -35
  156. package/src/resolver/languages/ruby.ts +0 -21
  157. package/src/resolver/languages/rust.ts +0 -7
  158. package/src/resolver/languages/typescript.ts +0 -168
  159. package/src/resolver/re-export-resolver.ts +0 -66
  160. package/src/resolver/symbol-table.ts +0 -67
  161. package/src/shared/extract-calls.ts +0 -75
  162. package/src/shared/file-hash.ts +0 -12
  163. package/src/shared/filters.ts +0 -243
  164. package/src/shared/logger.ts +0 -14
  165. package/src/shared/qualified-name.ts +0 -5
  166. package/src/shared/safe-path.ts +0 -31
  167. package/src/shared/schemas.ts +0 -32
@@ -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
- }
@@ -1,175 +0,0 @@
1
- import type { ContextV2Output } from './context-builder';
2
-
3
- export function formatPrompt(output: ContextV2Output): string {
4
- const { analysis } = output;
5
- const lines: string[] = [];
6
-
7
- // Header
8
- const risk = analysis.risk;
9
- const br = analysis.blast_radius;
10
- const meta = analysis.metadata;
11
- lines.push('# Code Review Context');
12
- lines.push('');
13
- lines.push(
14
- `Risk: ${risk.level} (${risk.score}) | ${br.total_functions} functions impacted across ${br.total_files} files | ${meta.untested_count} untested`,
15
- );
16
- lines.push('');
17
-
18
- // Changed functions
19
- if (analysis.changed_functions.length > 0) {
20
- lines.push('## Changed Functions');
21
- lines.push('');
22
-
23
- for (const fn of analysis.changed_functions) {
24
- lines.push(`### ${fn.signature} [${fn.file_path}:${fn.line_start}-${fn.line_end}]`);
25
-
26
- // Status
27
- if (fn.is_new) {
28
- lines.push('Status: new');
29
- } else if (fn.diff_changes.length > 0) {
30
- lines.push(`Status: modified (${fn.diff_changes.join(', ')})`);
31
- } else {
32
- lines.push('Status: unchanged');
33
- }
34
-
35
- // Callers
36
- if (fn.callers.length > 0) {
37
- lines.push('Callers:');
38
- for (const c of fn.callers) {
39
- const conf = c.confidence < 0.85 ? ` confidence=${c.confidence.toFixed(2)}` : '';
40
- lines.push(` - ${c.name} [${c.file_path}:${c.line}]${conf}`);
41
- }
42
- } else {
43
- lines.push('Callers: none');
44
- }
45
-
46
- // Callees
47
- if (fn.callees.length > 0) {
48
- lines.push('Callees:');
49
- for (const c of fn.callees) {
50
- lines.push(` - ${c.signature} [${c.file_path}]`);
51
- }
52
- } else {
53
- lines.push('Callees: none');
54
- }
55
-
56
- // Test coverage
57
- lines.push(`Test coverage: ${fn.has_test_coverage ? 'yes' : 'no'}`);
58
-
59
- // Affected flows
60
- if (fn.in_flows.length > 0) {
61
- lines.push('Affected flows:');
62
- for (const ep of fn.in_flows) {
63
- const flow = analysis.affected_flows.find((f) => f.entry_point === ep);
64
- if (flow) {
65
- const prefix = flow.type === 'http' ? 'HTTP' : 'TEST';
66
- lines.push(` - ${prefix}: ${flow.path.map((q) => q.split('::').pop()).join(' → ')}`);
67
- } else {
68
- lines.push(` - ${ep.split('::').pop()}`);
69
- }
70
- }
71
- } else {
72
- lines.push('Affected flows: none');
73
- }
74
-
75
- lines.push('');
76
- }
77
- }
78
-
79
- // Inheritance
80
- if (analysis.inheritance.length > 0) {
81
- lines.push('## Inheritance');
82
- lines.push('');
83
- for (const entry of analysis.inheritance) {
84
- const name = entry.qualified_name.split('::').pop();
85
- const parts: string[] = [];
86
- if (entry.extends) parts.push(`extends ${entry.extends.split('::').pop()}`);
87
- if (entry.implements.length > 0)
88
- parts.push(`implements ${entry.implements.map((i) => i.split('::').pop()).join(', ')}`);
89
- lines.push(`- ${name} ${parts.join(', ')}`);
90
- if (entry.children.length > 0) {
91
- lines.push(` Children: ${entry.children.map((c) => c.split('::').pop()).join(', ')}`);
92
- }
93
- }
94
- lines.push('');
95
- }
96
-
97
- // Blast radius by depth
98
- const byDepth = analysis.blast_radius.by_depth;
99
- const depthKeys = Object.keys(byDepth).sort();
100
- if (depthKeys.length > 0) {
101
- lines.push('## Blast Radius');
102
- lines.push('');
103
- for (const depth of depthKeys) {
104
- const names = byDepth[depth].map((q) => q.split('::').pop());
105
- lines.push(`Depth ${depth}: ${names.join(', ')} (${names.length} functions)`);
106
- }
107
- lines.push('');
108
- }
109
-
110
- // Test gaps
111
- if (analysis.test_gaps.length > 0) {
112
- lines.push('## Test Gaps');
113
- lines.push('');
114
- for (const gap of analysis.test_gaps) {
115
- const name = gap.function.split('::').pop();
116
- lines.push(`- ${name} [${gap.file_path}:${gap.line_start}]`);
117
- }
118
- lines.push('');
119
- }
120
-
121
- // Structural diff
122
- const diff = analysis.structural_diff;
123
- const hasNodeChanges = diff.summary.added > 0 || diff.summary.removed > 0 || diff.summary.modified > 0;
124
- const hasEdgeChanges = diff.edges.added.length > 0 || diff.edges.removed.length > 0;
125
-
126
- if (hasNodeChanges || hasEdgeChanges) {
127
- lines.push('## Structural Changes');
128
- lines.push('');
129
-
130
- if (hasNodeChanges) {
131
- const parts: string[] = [];
132
- if (diff.summary.added > 0) parts.push(`${diff.summary.added} added`);
133
- if (diff.summary.removed > 0) parts.push(`${diff.summary.removed} removed`);
134
- if (diff.summary.modified > 0) parts.push(`${diff.summary.modified} modified`);
135
- lines.push(parts.join(', '));
136
- }
137
-
138
- if (diff.nodes.removed.length > 0) {
139
- lines.push('');
140
- lines.push('Removed:');
141
- for (const n of diff.nodes.removed) {
142
- const name = n.qualified_name.split('::').pop();
143
- lines.push(` - [${n.kind}] ${name} [${n.file_path}:${n.line_start}]`);
144
- }
145
- }
146
-
147
- if (diff.nodes.modified.length > 0) {
148
- lines.push('');
149
- lines.push('Modified:');
150
- for (const m of diff.nodes.modified) {
151
- const name = m.qualified_name.split('::').pop();
152
- lines.push(` - ${name} (${m.changes.join(', ')})`);
153
- }
154
- }
155
-
156
- if (hasEdgeChanges) {
157
- lines.push('');
158
- lines.push('Dependency changes:');
159
- for (const e of diff.edges.added) {
160
- const src = e.source_qualified.split('::').pop();
161
- const tgt = e.target_qualified.split('::').pop();
162
- lines.push(` + ${e.kind}: ${src} → ${tgt}`);
163
- }
164
- for (const e of diff.edges.removed) {
165
- const src = e.source_qualified.split('::').pop();
166
- const tgt = e.target_qualified.split('::').pop();
167
- lines.push(` - ${e.kind}: ${src} → ${tgt}`);
168
- }
169
- }
170
-
171
- lines.push('');
172
- }
173
-
174
- return lines.join('\n');
175
- }
@@ -1,62 +0,0 @@
1
- import type { BlastRadiusResult, GraphData, RiskScoreResult } from '../graph/types';
2
-
3
- export function computeRiskScore(
4
- graph: GraphData,
5
- changedFiles: string[],
6
- blastRadius: BlastRadiusResult,
7
- ): RiskScoreResult {
8
- const changedSet = new Set(changedFiles);
9
- const changedNodes = graph.nodes.filter((n) => changedSet.has(n.file_path) && !n.is_test);
10
-
11
- // Factor 1: Blast radius (0.35)
12
- const brValue = Math.min(blastRadius.total_functions / 20, 1); // cap at 20
13
-
14
- // Factor 2: Test gaps (0.30)
15
- const testedFiles = new Set(graph.edges.filter((e) => e.kind === 'TESTED_BY').map((e) => e.source_qualified));
16
- const changedFunctions = changedNodes.filter((n) => n.kind === 'Function' || n.kind === 'Method');
17
- const untestedCount = changedFunctions.filter((n) => !testedFiles.has(n.file_path)).length;
18
- const tgValue = changedFunctions.length > 0 ? untestedCount / changedFunctions.length : 0;
19
-
20
- // Factor 3: Complexity (0.20)
21
- const avgSize =
22
- changedNodes.length > 0
23
- ? changedNodes.reduce((s, n) => s + (n.line_end - n.line_start), 0) / changedNodes.length
24
- : 0;
25
- const cxValue = Math.min(avgSize / 50, 1); // cap at 50 lines
26
-
27
- // Factor 4: Inheritance (0.15)
28
- const hasInheritance = graph.edges.some(
29
- (e) => (e.kind === 'INHERITS' || e.kind === 'IMPLEMENTS') && changedSet.has(e.file_path),
30
- );
31
- const ihValue = hasInheritance ? 1 : 0;
32
-
33
- const score = brValue * 0.35 + tgValue * 0.3 + cxValue * 0.2 + ihValue * 0.15;
34
- const level = score >= 0.6 ? 'HIGH' : score >= 0.3 ? 'MEDIUM' : 'LOW';
35
-
36
- return {
37
- level,
38
- score: Math.round(score * 100) / 100,
39
- factors: {
40
- blast_radius: {
41
- weight: 0.35,
42
- value: Math.round(brValue * 100) / 100,
43
- detail: `${blastRadius.total_functions} functions, ${blastRadius.total_files} files`,
44
- },
45
- test_gaps: {
46
- weight: 0.3,
47
- value: Math.round(tgValue * 100) / 100,
48
- detail: `${untestedCount}/${changedFunctions.length} untested`,
49
- },
50
- complexity: {
51
- weight: 0.2,
52
- value: Math.round(cxValue * 100) / 100,
53
- detail: `avg ${Math.round(avgSize)} lines`,
54
- },
55
- inheritance: {
56
- weight: 0.15,
57
- value: ihValue,
58
- detail: hasInheritance ? 'has inheritance' : 'no inheritance',
59
- },
60
- },
61
- };
62
- }
@@ -1,76 +0,0 @@
1
- import type { IndexedGraph } from '../graph/loader';
2
- import type { GraphNode } from '../graph/types';
3
-
4
- export interface SearchOptions {
5
- query?: string;
6
- kind?: string;
7
- file?: string;
8
- limit?: number;
9
- }
10
-
11
- export function searchNodes(graph: IndexedGraph, opts: SearchOptions): GraphNode[] {
12
- const { query, kind, file, limit = 50 } = opts;
13
- let results = graph.nodes;
14
-
15
- if (query) {
16
- const matcher = buildMatcher(query);
17
- results = results.filter((n) => matcher(n.name) || matcher(n.qualified_name));
18
- }
19
-
20
- if (kind) {
21
- results = results.filter((n) => n.kind === kind);
22
- }
23
-
24
- if (file) {
25
- const fileMatcher = buildMatcher(file);
26
- results = results.filter((n) => fileMatcher(n.file_path));
27
- }
28
-
29
- results.sort((a, b) => a.file_path.localeCompare(b.file_path) || a.line_start - b.line_start);
30
-
31
- return results.slice(0, limit);
32
- }
33
-
34
- export function findCallers(graph: IndexedGraph, qualifiedName: string): GraphNode[] {
35
- const edges = graph.reverseAdjacency.get(qualifiedName) || [];
36
- const callers: GraphNode[] = [];
37
- for (const e of edges) {
38
- if (e.kind !== 'CALLS') continue;
39
- const node = graph.byQualified.get(e.source_qualified);
40
- if (node) callers.push(node);
41
- }
42
- return callers;
43
- }
44
-
45
- export function findCallees(graph: IndexedGraph, qualifiedName: string): GraphNode[] {
46
- const edges = graph.adjacency.get(qualifiedName) || [];
47
- const callees: GraphNode[] = [];
48
- for (const e of edges) {
49
- if (e.kind !== 'CALLS') continue;
50
- const node = graph.byQualified.get(e.target_qualified);
51
- if (node) callees.push(node);
52
- }
53
- return callees;
54
- }
55
-
56
- function buildMatcher(pattern: string): (text: string) => boolean {
57
- // Regex: /pattern/flags
58
- if (pattern.startsWith('/')) {
59
- const lastSlash = pattern.lastIndexOf('/');
60
- if (lastSlash > 0) {
61
- const regex = new RegExp(pattern.slice(1, lastSlash), pattern.slice(lastSlash + 1));
62
- return (text) => regex.test(text);
63
- }
64
- }
65
-
66
- // Glob: contains *
67
- if (pattern.includes('*')) {
68
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
69
- const regex = new RegExp(`^${escaped}$`, 'i');
70
- return (text) => regex.test(text);
71
- }
72
-
73
- // Substring (case-insensitive)
74
- const lower = pattern.toLowerCase();
75
- return (text) => text.toLowerCase().includes(lower);
76
- }
@@ -1,21 +0,0 @@
1
- import type { GraphData, TestGap } from '../graph/types';
2
-
3
- export function findTestGaps(graph: GraphData, changedFiles: string[]): TestGap[] {
4
- const changedSet = new Set(changedFiles);
5
-
6
- const testedFiles = new Set(graph.edges.filter((e) => e.kind === 'TESTED_BY').map((e) => e.source_qualified));
7
-
8
- return graph.nodes
9
- .filter(
10
- (n) =>
11
- changedSet.has(n.file_path) &&
12
- (n.kind === 'Function' || n.kind === 'Method') &&
13
- !n.is_test &&
14
- !testedFiles.has(n.file_path),
15
- )
16
- .map((n) => ({
17
- function: n.qualified_name,
18
- file_path: n.file_path,
19
- line_start: n.line_start,
20
- }));
21
- }
package/src/cli.ts DELETED
@@ -1,207 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { Command } from 'commander';
3
- import { existsSync } from 'fs';
4
- import { resolve } from 'path';
5
- import { executeAnalyze } from './commands/analyze';
6
- import { executeCommunities } from './commands/communities';
7
- import { executeContext } from './commands/context';
8
- import { executeDiff } from './commands/diff';
9
- import { executeFlows } from './commands/flows';
10
- import { executeParse } from './commands/parse';
11
- import { executeSearch } from './commands/search';
12
- import { executeUpdate } from './commands/update';
13
-
14
- const program = new Command();
15
-
16
- import pkg from '../package.json';
17
- program.name('kodus-graph').description('Code graph builder for Kodus code review').version(pkg.version);
18
-
19
- program
20
- .command('parse')
21
- .description('Parse source files and generate nodes + edges')
22
- .option('--all', 'Parse all files in repo')
23
- .option('--files <paths...>', 'Parse specific files')
24
- .option('--repo-dir <path>', 'Repository root directory', '.')
25
- .option('--include <glob...>', 'Include only files matching glob (repeatable)')
26
- .option('--exclude <glob...>', 'Exclude files matching glob (repeatable)')
27
- .requiredOption('--out <path>', 'Output JSON file path')
28
- .action(async (opts) => {
29
- const repoDir = resolve(opts.repoDir);
30
- if (!existsSync(repoDir)) {
31
- process.stderr.write(`Error: --repo-dir does not exist: ${repoDir}\n`);
32
- process.exit(1);
33
- }
34
- await executeParse({
35
- repoDir: opts.repoDir,
36
- files: opts.files,
37
- all: opts.all ?? false,
38
- out: opts.out,
39
- include: opts.include,
40
- exclude: opts.exclude,
41
- });
42
- });
43
-
44
- program
45
- .command('analyze')
46
- .description('Compute blast radius, risk score, and test gaps')
47
- .requiredOption('--files <paths...>', 'Changed files to analyze')
48
- .option('--repo-dir <path>', 'Repository root directory', '.')
49
- .option('--graph <path>', 'Path to main graph JSON')
50
- .requiredOption('--out <path>', 'Output JSON file path')
51
- .action(async (opts) => {
52
- const repoDir = resolve(opts.repoDir);
53
- if (!existsSync(repoDir)) {
54
- process.stderr.write(`Error: --repo-dir does not exist: ${repoDir}\n`);
55
- process.exit(1);
56
- }
57
- await executeAnalyze({
58
- repoDir: opts.repoDir,
59
- files: opts.files,
60
- graph: opts.graph,
61
- out: opts.out,
62
- });
63
- });
64
-
65
- program
66
- .command('context')
67
- .description('Generate enriched review context for agents')
68
- .requiredOption('--files <paths...>', 'Changed files')
69
- .option('--repo-dir <path>', 'Repository root directory', '.')
70
- .option('--graph <path>', 'Path to main graph JSON')
71
- .requiredOption('--out <path>', 'Output JSON file path')
72
- .option('--min-confidence <n>', 'Minimum CALLS edge confidence', '0.5')
73
- .option('--max-depth <n>', 'Blast radius BFS depth', '3')
74
- .option('--format <type>', 'Output format: json or prompt', 'json')
75
- .action(async (opts) => {
76
- const repoDir = resolve(opts.repoDir);
77
- if (!existsSync(repoDir)) {
78
- process.stderr.write(`Error: --repo-dir does not exist: ${repoDir}\n`);
79
- process.exit(1);
80
- }
81
- if (opts.format !== 'json' && opts.format !== 'prompt') {
82
- process.stderr.write('Error: --format must be "json" or "prompt"\n');
83
- process.exit(1);
84
- }
85
- await executeContext({
86
- repoDir: opts.repoDir,
87
- files: opts.files,
88
- graph: opts.graph,
89
- out: opts.out,
90
- minConfidence: Number.parseFloat(opts.minConfidence),
91
- maxDepth: Number.parseInt(opts.maxDepth, 10),
92
- format: opts.format,
93
- });
94
- });
95
-
96
- program
97
- .command('diff')
98
- .description('Compare changed files against an existing graph')
99
- .option('--base <ref>', 'Git ref to diff against')
100
- .option('--files <paths...>', 'Explicit list of changed files')
101
- .option('--repo-dir <path>', 'Repository root directory', '.')
102
- .option('--graph <path>', 'Previous graph JSON', '.kodus-graph/graph.json')
103
- .requiredOption('--out <path>', 'Output JSON file path')
104
- .action(async (opts) => {
105
- if (!opts.base && !opts.files) {
106
- process.stderr.write('Error: one of --base or --files is required\n');
107
- process.exit(1);
108
- }
109
- const repoDir = resolve(opts.repoDir);
110
- if (!existsSync(repoDir)) {
111
- process.stderr.write(`Error: --repo-dir does not exist: ${repoDir}\n`);
112
- process.exit(1);
113
- }
114
- await executeDiff({
115
- repoDir: opts.repoDir,
116
- base: opts.base,
117
- files: opts.files,
118
- graph: opts.graph,
119
- out: opts.out,
120
- });
121
- });
122
-
123
- program
124
- .command('update')
125
- .description('Incrementally update graph (only re-parse changed files)')
126
- .option('--repo-dir <path>', 'Repository root directory', '.')
127
- .option('--graph <path>', 'Previous graph JSON (default: .kodus-graph/graph.json)')
128
- .option('--out <path>', 'Output path (default: same as --graph)')
129
- .action(async (opts) => {
130
- const repoDir = resolve(opts.repoDir);
131
- if (!existsSync(repoDir)) {
132
- process.stderr.write(`Error: --repo-dir does not exist: ${repoDir}\n`);
133
- process.exit(1);
134
- }
135
- await executeUpdate({
136
- repoDir: opts.repoDir,
137
- graph: opts.graph,
138
- out: opts.out,
139
- });
140
- });
141
-
142
- program
143
- .command('communities')
144
- .description('Detect module clusters and coupling between them')
145
- .requiredOption('--graph <path>', 'Path to graph JSON')
146
- .requiredOption('--out <path>', 'Output JSON file path')
147
- .option('--min-size <n>', 'Minimum nodes per community', '2')
148
- .option('--depth <n>', 'Directory grouping depth', '2')
149
- .action((opts) => {
150
- executeCommunities({
151
- graph: opts.graph,
152
- out: opts.out,
153
- minSize: parseInt(opts.minSize, 10),
154
- depth: parseInt(opts.depth, 10),
155
- });
156
- });
157
-
158
- program
159
- .command('flows')
160
- .description('Detect entry points and trace execution paths')
161
- .requiredOption('--graph <path>', 'Path to graph JSON')
162
- .requiredOption('--out <path>', 'Output JSON file path')
163
- .option('--max-depth <n>', 'Max BFS trace depth', '10')
164
- .option('--type <kind>', 'Filter: test, http, all', 'all')
165
- .action((opts) => {
166
- executeFlows({
167
- graph: opts.graph,
168
- out: opts.out,
169
- maxDepth: parseInt(opts.maxDepth, 10),
170
- type: opts.type as 'test' | 'http' | 'all',
171
- });
172
- });
173
-
174
- program
175
- .command('search')
176
- .description('Search the graph by name, kind, file, or relations')
177
- .requiredOption('--graph <path>', 'Path to graph JSON')
178
- .option('--query <pattern>', 'Search by name/qualified_name (glob or /regex/)')
179
- .option('--kind <type>', 'Filter by kind: Function, Method, Class, Interface, Enum, Test')
180
- .option('--file <pattern>', 'Filter by file path (glob)')
181
- .option('--callers-of <qualified>', 'Find callers of this node')
182
- .option('--callees-of <qualified>', 'Find callees of this node')
183
- .option('--limit <n>', 'Max results', '50')
184
- .option('--out <path>', 'Output file (default: stdout)')
185
- .action((opts) => {
186
- const modes = [opts.query, opts.callersOf, opts.calleesOf].filter(Boolean).length;
187
- if (modes === 0) {
188
- process.stderr.write('Error: one of --query, --callers-of, or --callees-of is required\n');
189
- process.exit(1);
190
- }
191
- if (modes > 1) {
192
- process.stderr.write('Error: --query, --callers-of, and --callees-of are mutually exclusive\n');
193
- process.exit(1);
194
- }
195
- executeSearch({
196
- graph: opts.graph,
197
- query: opts.query,
198
- kind: opts.kind,
199
- file: opts.file,
200
- callersOf: opts.callersOf,
201
- calleesOf: opts.calleesOf,
202
- limit: parseInt(opts.limit, 10),
203
- out: opts.out,
204
- });
205
- });
206
-
207
- program.parse();