@kodus/kodus-graph 0.2.8 → 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 -169
  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 -210
  126. package/src/commands/analyze.ts +0 -128
  127. package/src/commands/communities.ts +0 -19
  128. package/src/commands/context.ts +0 -182
  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 -17
  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
@@ -0,0 +1,64 @@
1
+ export function searchNodes(graph, opts) {
2
+ const { query, kind, file, limit = 50 } = opts;
3
+ let results = graph.nodes;
4
+ if (query) {
5
+ const matcher = buildMatcher(query);
6
+ results = results.filter((n) => matcher(n.name) || matcher(n.qualified_name));
7
+ }
8
+ if (kind) {
9
+ results = results.filter((n) => n.kind === kind);
10
+ }
11
+ if (file) {
12
+ const fileMatcher = buildMatcher(file);
13
+ results = results.filter((n) => fileMatcher(n.file_path));
14
+ }
15
+ results.sort((a, b) => a.file_path.localeCompare(b.file_path) || a.line_start - b.line_start);
16
+ return results.slice(0, limit);
17
+ }
18
+ export function findCallers(graph, qualifiedName) {
19
+ const edges = graph.reverseAdjacency.get(qualifiedName) || [];
20
+ const callers = [];
21
+ for (const e of edges) {
22
+ if (e.kind !== 'CALLS') {
23
+ continue;
24
+ }
25
+ const node = graph.byQualified.get(e.source_qualified);
26
+ if (node) {
27
+ callers.push(node);
28
+ }
29
+ }
30
+ return callers;
31
+ }
32
+ export function findCallees(graph, qualifiedName) {
33
+ const edges = graph.adjacency.get(qualifiedName) || [];
34
+ const callees = [];
35
+ for (const e of edges) {
36
+ if (e.kind !== 'CALLS') {
37
+ continue;
38
+ }
39
+ const node = graph.byQualified.get(e.target_qualified);
40
+ if (node) {
41
+ callees.push(node);
42
+ }
43
+ }
44
+ return callees;
45
+ }
46
+ function buildMatcher(pattern) {
47
+ // Regex: /pattern/flags
48
+ if (pattern.startsWith('/')) {
49
+ const lastSlash = pattern.lastIndexOf('/');
50
+ if (lastSlash > 0) {
51
+ const regex = new RegExp(pattern.slice(1, lastSlash), pattern.slice(lastSlash + 1));
52
+ return (text) => regex.test(text);
53
+ }
54
+ }
55
+ // Glob: contains *
56
+ if (pattern.includes('*')) {
57
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
58
+ const regex = new RegExp(`^${escaped}$`, 'i');
59
+ return (text) => regex.test(text);
60
+ }
61
+ // Substring (case-insensitive)
62
+ const lower = pattern.toLowerCase();
63
+ return (text) => text.toLowerCase().includes(lower);
64
+ }
@@ -0,0 +1,2 @@
1
+ import type { GraphData, TestGap } from '../graph/types';
2
+ export declare function findTestGaps(graph: GraphData, changedFiles: string[]): TestGap[];
@@ -0,0 +1,14 @@
1
+ export function findTestGaps(graph, changedFiles) {
2
+ const changedSet = new Set(changedFiles);
3
+ const testedFiles = new Set(graph.edges.filter((e) => e.kind === 'TESTED_BY').map((e) => e.source_qualified));
4
+ return graph.nodes
5
+ .filter((n) => changedSet.has(n.file_path) &&
6
+ (n.kind === 'Function' || n.kind === 'Method') &&
7
+ !n.is_test &&
8
+ !testedFiles.has(n.file_path))
9
+ .map((n) => ({
10
+ function: n.qualified_name,
11
+ file_path: n.file_path,
12
+ line_start: n.line_start,
13
+ }));
14
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,208 @@
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
+ const program = new Command();
14
+ import pkg from '../package.json';
15
+ import { log } from './shared/logger';
16
+ log.info(`kodus-graph v${pkg.version}`, { node: process.version, platform: process.platform });
17
+ program.name('kodus-graph').description('Code graph builder for Kodus code review').version(pkg.version);
18
+ program
19
+ .command('parse')
20
+ .description('Parse source files and generate nodes + edges')
21
+ .option('--all', 'Parse all files in repo')
22
+ .option('--files <paths...>', 'Parse specific files')
23
+ .option('--repo-dir <path>', 'Repository root directory', '.')
24
+ .option('--include <glob...>', 'Include only files matching glob (repeatable)')
25
+ .option('--exclude <glob...>', 'Exclude files matching glob (repeatable)')
26
+ .option('--skip-tests', 'Skip test detection (no Test nodes, TESTED_BY edges, or test gaps)')
27
+ .requiredOption('--out <path>', 'Output JSON file path')
28
+ .action(async (opts) => {
29
+ const repoDir = resolve(opts.repoDir);
30
+ if (!existsSync(repoDir)) {
31
+ log.error('--repo-dir does not exist', { path: repoDir });
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
+ skipTests: opts.skipTests ?? false,
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
+ .option('--skip-tests', 'Skip test detection (no TESTED_BY edges or test gaps)')
51
+ .requiredOption('--out <path>', 'Output JSON file path')
52
+ .action(async (opts) => {
53
+ const repoDir = resolve(opts.repoDir);
54
+ if (!existsSync(repoDir)) {
55
+ log.error('--repo-dir does not exist', { path: repoDir });
56
+ process.exit(1);
57
+ }
58
+ await executeAnalyze({
59
+ repoDir: opts.repoDir,
60
+ files: opts.files,
61
+ graph: opts.graph,
62
+ out: opts.out,
63
+ skipTests: opts.skipTests ?? false,
64
+ });
65
+ });
66
+ program
67
+ .command('context')
68
+ .description('Generate enriched review context for agents')
69
+ .requiredOption('--files <paths...>', 'Changed files')
70
+ .option('--repo-dir <path>', 'Repository root directory', '.')
71
+ .option('--graph <path>', 'Path to main graph JSON')
72
+ .requiredOption('--out <path>', 'Output JSON file path')
73
+ .option('--min-confidence <n>', 'Minimum CALLS edge confidence', '0.5')
74
+ .option('--max-depth <n>', 'Blast radius BFS depth', '3')
75
+ .option('--format <type>', 'Output format: json or prompt', 'json')
76
+ .option('--skip-tests', 'Skip test detection (no Test nodes, TESTED_BY edges, or test gaps)')
77
+ .action(async (opts) => {
78
+ const repoDir = resolve(opts.repoDir);
79
+ if (!existsSync(repoDir)) {
80
+ log.error('--repo-dir does not exist', { path: repoDir });
81
+ process.exit(1);
82
+ }
83
+ if (opts.format !== 'json' && opts.format !== 'prompt') {
84
+ log.error('--format must be "json" or "prompt"', { got: opts.format });
85
+ process.exit(1);
86
+ }
87
+ await executeContext({
88
+ repoDir: opts.repoDir,
89
+ files: opts.files,
90
+ graph: opts.graph,
91
+ out: opts.out,
92
+ minConfidence: Number.parseFloat(opts.minConfidence),
93
+ maxDepth: Number.parseInt(opts.maxDepth, 10),
94
+ format: opts.format,
95
+ skipTests: opts.skipTests ?? false,
96
+ });
97
+ });
98
+ program
99
+ .command('diff')
100
+ .description('Compare changed files against an existing graph')
101
+ .option('--base <ref>', 'Git ref to diff against')
102
+ .option('--files <paths...>', 'Explicit list of changed files')
103
+ .option('--repo-dir <path>', 'Repository root directory', '.')
104
+ .option('--graph <path>', 'Previous graph JSON', '.kodus-graph/graph.json')
105
+ .requiredOption('--out <path>', 'Output JSON file path')
106
+ .action(async (opts) => {
107
+ if (!opts.base && !opts.files) {
108
+ log.error('one of --base or --files is required');
109
+ process.exit(1);
110
+ }
111
+ const repoDir = resolve(opts.repoDir);
112
+ if (!existsSync(repoDir)) {
113
+ log.error('--repo-dir does not exist', { path: repoDir });
114
+ process.exit(1);
115
+ }
116
+ await executeDiff({
117
+ repoDir: opts.repoDir,
118
+ base: opts.base,
119
+ files: opts.files,
120
+ graph: opts.graph,
121
+ out: opts.out,
122
+ });
123
+ });
124
+ program
125
+ .command('update')
126
+ .description('Incrementally update graph (only re-parse changed files)')
127
+ .option('--repo-dir <path>', 'Repository root directory', '.')
128
+ .option('--graph <path>', 'Previous graph JSON (default: .kodus-graph/graph.json)')
129
+ .option('--out <path>', 'Output path (default: same as --graph)')
130
+ .action(async (opts) => {
131
+ const repoDir = resolve(opts.repoDir);
132
+ if (!existsSync(repoDir)) {
133
+ log.error('--repo-dir does not exist', { path: repoDir });
134
+ process.exit(1);
135
+ }
136
+ await executeUpdate({
137
+ repoDir: opts.repoDir,
138
+ graph: opts.graph,
139
+ out: opts.out,
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
+ program
158
+ .command('flows')
159
+ .description('Detect entry points and trace execution paths')
160
+ .requiredOption('--graph <path>', 'Path to graph JSON')
161
+ .requiredOption('--out <path>', 'Output JSON file path')
162
+ .option('--max-depth <n>', 'Max BFS trace depth', '10')
163
+ .option('--type <kind>', 'Filter: test, http, all', 'all')
164
+ .action((opts) => {
165
+ executeFlows({
166
+ graph: opts.graph,
167
+ out: opts.out,
168
+ maxDepth: parseInt(opts.maxDepth, 10),
169
+ type: opts.type,
170
+ });
171
+ });
172
+ program
173
+ .command('search')
174
+ .description('Search the graph by name, kind, file, or relations')
175
+ .requiredOption('--graph <path>', 'Path to graph JSON')
176
+ .option('--query <pattern>', 'Search by name/qualified_name (glob or /regex/)')
177
+ .option('--kind <type>', 'Filter by kind: Function, Method, Class, Interface, Enum, Test')
178
+ .option('--file <pattern>', 'Filter by file path (glob)')
179
+ .option('--callers-of <qualified>', 'Find callers of this node')
180
+ .option('--callees-of <qualified>', 'Find callees of this node')
181
+ .option('--limit <n>', 'Max results', '50')
182
+ .option('--out <path>', 'Output file (default: stdout)')
183
+ .action((opts) => {
184
+ const modes = [opts.query, opts.callersOf, opts.calleesOf].filter(Boolean).length;
185
+ if (modes === 0) {
186
+ log.error('one of --query, --callers-of, or --callees-of is required');
187
+ process.exit(1);
188
+ }
189
+ if (modes > 1) {
190
+ log.error('--query, --callers-of, and --callees-of are mutually exclusive');
191
+ process.exit(1);
192
+ }
193
+ executeSearch({
194
+ graph: opts.graph,
195
+ query: opts.query,
196
+ kind: opts.kind,
197
+ file: opts.file,
198
+ callersOf: opts.callersOf,
199
+ calleesOf: opts.calleesOf,
200
+ limit: parseInt(opts.limit, 10),
201
+ out: opts.out,
202
+ });
203
+ });
204
+ program.parseAsync().catch((err) => {
205
+ const message = err instanceof Error ? err.message : String(err);
206
+ log.error(message);
207
+ process.exit(1);
208
+ });
@@ -0,0 +1,9 @@
1
+ interface AnalyzeOptions {
2
+ repoDir: string;
3
+ files: string[];
4
+ graph?: string;
5
+ out: string;
6
+ skipTests?: boolean;
7
+ }
8
+ export declare function executeAnalyze(opts: AnalyzeOptions): Promise<void>;
9
+ export {};
@@ -0,0 +1,114 @@
1
+ import { readFileSync, writeFileSync } from 'fs';
2
+ import { relative, resolve } from 'path';
3
+ import { computeBlastRadius } from '../analysis/blast-radius';
4
+ import { computeRiskScore } from '../analysis/risk-score';
5
+ import { findTestGaps } from '../analysis/test-gaps';
6
+ import { buildGraphData } from '../graph/builder';
7
+ import { mergeGraphs } from '../graph/merger';
8
+ import { parseBatch } from '../parser/batch';
9
+ import { discoverFiles } from '../parser/discovery';
10
+ import { resolveAllCalls } from '../resolver/call-resolver';
11
+ import { createImportMap } from '../resolver/import-map';
12
+ import { loadTsconfigAliases, resolveImport } from '../resolver/import-resolver';
13
+ import { buildReExportMap } from '../resolver/re-export-resolver';
14
+ import { createSymbolTable } from '../resolver/symbol-table';
15
+ import { computeFileHash } from '../shared/file-hash';
16
+ import { log } from '../shared/logger';
17
+ import { GraphInputSchema } from '../shared/schemas';
18
+ export async function executeAnalyze(opts) {
19
+ const repoDir = resolve(opts.repoDir);
20
+ // Load main graph if provided
21
+ let mainGraph = null;
22
+ if (opts.graph) {
23
+ let raw;
24
+ try {
25
+ raw = JSON.parse(readFileSync(opts.graph, 'utf-8'));
26
+ }
27
+ catch (_err) {
28
+ log.error('failed to read --graph file', { path: opts.graph });
29
+ process.exit(1);
30
+ }
31
+ const validated = GraphInputSchema.safeParse(raw);
32
+ if (!validated.success) {
33
+ log.error('invalid graph JSON', { error: validated.error.message });
34
+ process.exit(1);
35
+ }
36
+ mainGraph = {
37
+ repo_id: '',
38
+ sha: '',
39
+ nodes: validated.data.nodes,
40
+ edges: validated.data.edges,
41
+ };
42
+ }
43
+ // Parse changed files locally
44
+ const localFiles = discoverFiles(repoDir, opts.files);
45
+ const rawGraph = await parseBatch(localFiles, repoDir, { skipTests: opts.skipTests });
46
+ // Resolve imports
47
+ const tsconfigAliases = loadTsconfigAliases(repoDir);
48
+ const symbolTable = createSymbolTable();
49
+ const importMap = createImportMap();
50
+ const importEdges = [];
51
+ for (const f of rawGraph.functions) {
52
+ symbolTable.add(f.file, f.name, f.qualified);
53
+ }
54
+ for (const c of rawGraph.classes) {
55
+ symbolTable.add(c.file, c.name, c.qualified);
56
+ }
57
+ for (const i of rawGraph.interfaces) {
58
+ symbolTable.add(i.file, i.name, i.qualified);
59
+ }
60
+ // Pre-resolve re-exports so barrel imports follow through to actual definitions
61
+ const barrelMap = buildReExportMap(rawGraph.reExports, repoDir, tsconfigAliases);
62
+ for (const imp of rawGraph.imports) {
63
+ const langKey = imp.lang === 'python' ? 'python' : imp.lang === 'ruby' ? 'ruby' : 'typescript';
64
+ const resolved = resolveImport(resolve(repoDir, imp.file), imp.module, langKey, repoDir, tsconfigAliases);
65
+ const resolvedRel = resolved ? relative(repoDir, resolved) : null;
66
+ importEdges.push({
67
+ source: imp.file,
68
+ target: resolvedRel || imp.module,
69
+ resolved: !!resolvedRel,
70
+ line: imp.line,
71
+ });
72
+ const target = resolvedRel || imp.module;
73
+ for (const name of imp.names) {
74
+ let finalTarget = target;
75
+ if (resolvedRel) {
76
+ const reExportedFiles = barrelMap.get(resolvedRel);
77
+ if (reExportedFiles) {
78
+ for (const reFile of reExportedFiles) {
79
+ if (symbolTable.lookupExact(reFile, name)) {
80
+ finalTarget = reFile;
81
+ break;
82
+ }
83
+ }
84
+ }
85
+ }
86
+ importMap.add(imp.file, name, finalTarget);
87
+ }
88
+ }
89
+ // Resolve calls
90
+ const { callEdges } = resolveAllCalls(rawGraph.rawCalls, rawGraph.diMaps, symbolTable, importMap);
91
+ // Build graph with file hashes
92
+ const fileHashes = new Map();
93
+ for (const f of localFiles) {
94
+ try {
95
+ fileHashes.set(relative(repoDir, f), computeFileHash(f));
96
+ }
97
+ catch (err) {
98
+ log.warn('Failed to compute file hash', { file: f, error: String(err) });
99
+ }
100
+ }
101
+ const localGraphData = buildGraphData(rawGraph, callEdges, importEdges, repoDir, fileHashes);
102
+ // Merge with main graph (or use local only)
103
+ const mergedGraph = mainGraph ? mergeGraphs(mainGraph, localGraphData, opts.files) : localGraphData;
104
+ // Analyze
105
+ const blastRadius = computeBlastRadius(mergedGraph, opts.files);
106
+ const riskScore = computeRiskScore(mergedGraph, opts.files, blastRadius, { skipTests: opts.skipTests });
107
+ const testGaps = opts.skipTests ? [] : findTestGaps(mergedGraph, opts.files);
108
+ const output = {
109
+ blast_radius: blastRadius,
110
+ risk_score: riskScore,
111
+ test_gaps: testGaps,
112
+ };
113
+ writeFileSync(opts.out, JSON.stringify(output, null, 2));
114
+ }
@@ -0,0 +1,8 @@
1
+ interface CommunitiesCommandOptions {
2
+ graph: string;
3
+ out: string;
4
+ minSize: number;
5
+ depth: number;
6
+ }
7
+ export declare function executeCommunities(opts: CommunitiesCommandOptions): void;
8
+ export {};
@@ -0,0 +1,9 @@
1
+ import { writeFileSync } from 'fs';
2
+ import { detectCommunities } from '../analysis/communities';
3
+ import { loadGraph } from '../graph/loader';
4
+ export function executeCommunities(opts) {
5
+ const graph = loadGraph(opts.graph);
6
+ const result = detectCommunities(graph, { depth: opts.depth, minSize: opts.minSize });
7
+ writeFileSync(opts.out, JSON.stringify(result, null, 2));
8
+ process.stderr.write(`Communities: ${result.summary.total_communities} detected, avg cohesion ${result.summary.avg_cohesion}, ${result.summary.high_coupling_pairs} high-coupling pairs\n`);
9
+ }
@@ -0,0 +1,12 @@
1
+ interface ContextOptions {
2
+ repoDir: string;
3
+ files: string[];
4
+ graph?: string;
5
+ out: string;
6
+ minConfidence: number;
7
+ maxDepth: number;
8
+ format: 'json' | 'prompt';
9
+ skipTests?: boolean;
10
+ }
11
+ export declare function executeContext(opts: ContextOptions): Promise<void>;
12
+ export {};
@@ -0,0 +1,130 @@
1
+ import { execSync } from 'child_process';
2
+ import { readFileSync, rmSync, writeFileSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import { buildContextV2 } from '../analysis/context-builder';
5
+ import { formatPrompt } from '../analysis/prompt-formatter';
6
+ import { mergeGraphs } from '../graph/merger';
7
+ import { log } from '../shared/logger';
8
+ import { GraphInputSchema } from '../shared/schemas';
9
+ import { createSecureTempFile } from '../shared/temp';
10
+ import { executeParse } from './parse';
11
+ export async function executeContext(opts) {
12
+ const repoDir = resolve(opts.repoDir);
13
+ log.info('context: starting', {
14
+ files: opts.files,
15
+ repoDir,
16
+ graph: opts.graph ?? null,
17
+ format: opts.format,
18
+ minConfidence: opts.minConfidence,
19
+ maxDepth: opts.maxDepth,
20
+ });
21
+ // Parse changed files using secure temp
22
+ const tmp = createSecureTempFile('ctx');
23
+ try {
24
+ await executeParse({
25
+ repoDir,
26
+ files: opts.files,
27
+ all: false,
28
+ out: tmp.filePath,
29
+ skipTests: opts.skipTests,
30
+ });
31
+ const parseResult = JSON.parse(readFileSync(tmp.filePath, 'utf-8'));
32
+ log.info('context: parse done', {
33
+ nodes: parseResult.nodes?.length ?? 0,
34
+ edges: parseResult.edges?.length ?? 0,
35
+ });
36
+ // Load and merge with main graph if provided
37
+ let mergedGraph;
38
+ let oldGraph = null;
39
+ if (opts.graph) {
40
+ let raw;
41
+ try {
42
+ raw = JSON.parse(readFileSync(opts.graph, 'utf-8'));
43
+ }
44
+ catch (_err) {
45
+ log.error('failed to read --graph file', { path: opts.graph });
46
+ process.exit(1);
47
+ }
48
+ const validated = GraphInputSchema.safeParse(raw);
49
+ if (!validated.success) {
50
+ log.error('invalid graph JSON', { error: validated.error.message });
51
+ process.exit(1);
52
+ }
53
+ const changedSet = new Set(opts.files);
54
+ // Detect same-branch via commit sha comparison
55
+ const graphSha = raw?.sha || '';
56
+ let headSha = '';
57
+ try {
58
+ headSha = execSync('git rev-parse HEAD', { cwd: repoDir, encoding: 'utf-8' }).trim();
59
+ }
60
+ catch {
61
+ log.debug('could not resolve HEAD sha');
62
+ }
63
+ const sameBranch = graphSha !== '' && graphSha === headSha;
64
+ log.info('context: baseline graph loaded', {
65
+ graphNodes: validated.data.nodes.length,
66
+ graphEdges: validated.data.edges.length,
67
+ sameBranch,
68
+ graphSha: graphSha ? graphSha.substring(0, 8) : 'none',
69
+ headSha: headSha ? headSha.substring(0, 8) : 'none',
70
+ });
71
+ if (sameBranch) {
72
+ // --graph was built from the same commit (e.g. kodus-ai's parse --all on PR branch).
73
+ // Exclude changed files from oldGraph so diff detects their functions as "added"
74
+ // instead of falsely marking everything "unchanged".
75
+ oldGraph = {
76
+ nodes: validated.data.nodes.filter((n) => !changedSet.has(n.file_path)),
77
+ edges: validated.data.edges.filter((e) => !changedSet.has(e.file_path)),
78
+ };
79
+ log.debug('Same-branch detected: excluding changed files from baseline', {
80
+ changedFiles: opts.files.length,
81
+ });
82
+ }
83
+ else {
84
+ oldGraph = { nodes: validated.data.nodes, edges: validated.data.edges };
85
+ }
86
+ const mainGraph = {
87
+ repo_id: '',
88
+ sha: '',
89
+ nodes: validated.data.nodes,
90
+ edges: validated.data.edges,
91
+ };
92
+ mergedGraph = mergeGraphs(mainGraph, parseResult, opts.files);
93
+ }
94
+ else {
95
+ mergedGraph = { nodes: parseResult.nodes, edges: parseResult.edges };
96
+ }
97
+ // Build V2 context
98
+ const output = buildContextV2({
99
+ mergedGraph,
100
+ oldGraph,
101
+ changedFiles: opts.files,
102
+ minConfidence: opts.minConfidence,
103
+ maxDepth: opts.maxDepth,
104
+ skipTests: opts.skipTests,
105
+ });
106
+ log.info('context: analysis done', {
107
+ changedFunctions: output.analysis.changed_functions.length,
108
+ diff: output.analysis.structural_diff.summary,
109
+ blastRadius: output.analysis.blast_radius.total_functions,
110
+ risk: `${output.analysis.risk.level} (${output.analysis.risk.score})`,
111
+ testGaps: output.analysis.test_gaps.length,
112
+ affectedFlows: output.analysis.affected_flows.length,
113
+ duration_ms: output.analysis.metadata.duration_ms,
114
+ });
115
+ if (opts.format === 'prompt') {
116
+ writeFileSync(opts.out, formatPrompt(output));
117
+ }
118
+ else {
119
+ writeFileSync(opts.out, JSON.stringify(output, null, 2));
120
+ }
121
+ }
122
+ finally {
123
+ try {
124
+ rmSync(tmp.dir, { recursive: true, force: true });
125
+ }
126
+ catch (err) {
127
+ log.debug('Failed to clean up temp dir', { dir: tmp.dir, error: String(err) });
128
+ }
129
+ }
130
+ }
@@ -0,0 +1,9 @@
1
+ interface DiffCommandOptions {
2
+ repoDir: string;
3
+ base?: string;
4
+ files?: string[];
5
+ graph: string;
6
+ out: string;
7
+ }
8
+ export declare function executeDiff(opts: DiffCommandOptions): Promise<void>;
9
+ export {};