@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Kodus AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,252 @@
1
+ # @kodus/kodus-graph
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@kodus/kodus-graph)](https://www.npmjs.com/package/@kodus/kodus-graph)
4
+ [![license](https://img.shields.io/npm/l/@kodus/kodus-graph)](LICENSE)
5
+ [![Bun](https://img.shields.io/badge/runtime-Bun-%23f9f1e1)](https://bun.sh)
6
+
7
+ Code graph builder for Kodus code review. Parses source code into structural graphs with nodes, edges, and analysis — enabling blast radius detection, risk scoring, test gap analysis, and enriched review context for AI agents.
8
+
9
+ ## Features
10
+
11
+ - **Multi-language** — TypeScript, Python, Go, Java, Ruby, Rust, C#, PHP
12
+ - **Structural graph** — Functions, classes, interfaces, enums as nodes; CALLS, IMPORTS, INHERITS, IMPLEMENTS, TESTED_BY, CONTAINS as edges
13
+ - **Call resolution** — 5-tier confidence cascade with DI pattern detection
14
+ - **Incremental parsing** — Content hashing skips unchanged files
15
+ - **Streaming JSON** — Memory-efficient output for large codebases
16
+
17
+ ## Requirements
18
+
19
+ - [Bun](https://bun.sh) >= 1.3.0
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ # Global (recommended for CLI usage)
25
+ bun install -g @kodus/kodus-graph
26
+
27
+ # Or via npm/yarn (requires Bun as runtime)
28
+ npm install -g @kodus/kodus-graph
29
+ yarn global add @kodus/kodus-graph
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ```bash
35
+ # 1. Parse a repository
36
+ kodus-graph parse --all --repo-dir ./my-project --out graph.json
37
+
38
+ # 2. Analyze changed files
39
+ kodus-graph analyze --files src/auth.ts src/db.ts --graph graph.json --out analysis.json
40
+
41
+ # 3. Generate review context for AI agents
42
+ kodus-graph context --files src/auth.ts --graph graph.json --out context.json --format json
43
+ ```
44
+
45
+ ## Commands
46
+
47
+ ### `parse`
48
+
49
+ Builds the structural graph of your codebase — extracts every function, class, interface, enum, and their relationships (calls, imports, inheritance).
50
+
51
+ **When to use:** First step in any workflow. Run once on the full repo to create the baseline graph, then use `update` for incremental changes.
52
+
53
+ ```bash
54
+ # Parse all files
55
+ kodus-graph parse --all --repo-dir . --out graph.json
56
+
57
+ # Parse specific files
58
+ kodus-graph parse --files src/auth.ts src/db.ts --repo-dir . --out graph.json
59
+
60
+ # With glob filters
61
+ kodus-graph parse --all --repo-dir . --out graph.json \
62
+ --include "src/**/*.ts" \
63
+ --exclude "**/*.test.ts" "**/*.spec.ts"
64
+ ```
65
+
66
+ **Output:** JSON with `metadata`, `nodes`, and `edges`. See [example output](examples/parse-output.json).
67
+
68
+ ### `analyze`
69
+
70
+ Computes the impact of code changes — how far the blast radius reaches, how risky the change is (4-factor score), and which changed functions lack tests.
71
+
72
+ **When to use:** During code review or CI, to assess the risk of a PR before merging.
73
+
74
+ ```bash
75
+ kodus-graph analyze \
76
+ --files src/auth.ts src/user.service.ts \
77
+ --graph graph.json \
78
+ --out analysis.json
79
+ ```
80
+
81
+ **Output:** `blast_radius`, `risk_score` (level + factors), `test_gaps`. See [example output](examples/analyze-output.json).
82
+
83
+ ### `context`
84
+
85
+ Generates enriched review context for AI agents — caller/callee chains, affected execution flows, inheritance, risk assessment, and test coverage per changed function.
86
+
87
+ **When to use:** Feed this to an LLM-based code reviewer so it understands the full impact of a change, not just the diff.
88
+
89
+ ```bash
90
+ # JSON format (for programmatic use)
91
+ kodus-graph context \
92
+ --files src/auth.ts \
93
+ --graph graph.json \
94
+ --out context.json \
95
+ --format json
96
+
97
+ # Prompt format (for LLM agents)
98
+ kodus-graph context \
99
+ --files src/auth.ts \
100
+ --graph graph.json \
101
+ --out context.txt \
102
+ --format prompt \
103
+ --min-confidence 0.5 \
104
+ --max-depth 3
105
+ ```
106
+
107
+ **Output:** Enriched functions with callers, callees, affected flows, risk level. See [example output](examples/context-output.json).
108
+
109
+ ### `diff`
110
+
111
+ Detects structural changes between the current code and a previous graph — which nodes/edges were added, removed, or modified (signature, body, line range).
112
+
113
+ **When to use:** To understand what actually changed structurally in a PR, beyond the raw text diff.
114
+
115
+ ```bash
116
+ # Diff against a git ref
117
+ kodus-graph diff --base main --graph graph.json --out diff.json
118
+
119
+ # Diff specific files
120
+ kodus-graph diff --files src/auth.ts --graph graph.json --out diff.json
121
+ ```
122
+
123
+ **Output:** Added/removed/modified nodes and edges with detail on what changed.
124
+
125
+ ### `update`
126
+
127
+ Incrementally updates an existing graph — only re-parses files whose content hash changed. Much faster than a full parse on large repos.
128
+
129
+ **When to use:** After each commit or PR merge to keep the baseline graph up to date without re-parsing the entire codebase.
130
+
131
+ ```bash
132
+ kodus-graph update --repo-dir . --graph graph.json
133
+ ```
134
+
135
+ ### `communities`
136
+
137
+ Groups code into module clusters based on directory structure and detects coupling between them (how many cross-cluster calls exist).
138
+
139
+ **When to use:** To understand the modular architecture of a codebase and identify tightly coupled areas that may need refactoring.
140
+
141
+ ```bash
142
+ kodus-graph communities --graph graph.json --out communities.json --min-size 2 --depth 2
143
+ ```
144
+
145
+ ### `flows`
146
+
147
+ Detects entry points (HTTP handlers, test functions) and traces their execution paths through the call graph.
148
+
149
+ **When to use:** To understand which user-facing flows are affected by a code change — e.g., "this change breaks the login flow".
150
+
151
+ ```bash
152
+ kodus-graph flows --graph graph.json --out flows.json --max-depth 10 --type all
153
+ ```
154
+
155
+ ### `search`
156
+
157
+ Queries the graph by name, kind, file path, or call relationships. Supports glob patterns and regex.
158
+
159
+ **When to use:** To explore the graph interactively — find all callers of a function, list all methods in a service, etc.
160
+
161
+ ```bash
162
+ # Search by name (glob or regex)
163
+ kodus-graph search --graph graph.json --query "auth*"
164
+ kodus-graph search --graph graph.json --query "/^handle.*Request$/"
165
+
166
+ # Filter by kind
167
+ kodus-graph search --graph graph.json --query "*" --kind Method --file "src/services/*"
168
+
169
+ # Find callers/callees
170
+ kodus-graph search --graph graph.json --callers-of "src/db.ts::query"
171
+ kodus-graph search --graph graph.json --callees-of "src/auth.ts::authenticate"
172
+ ```
173
+
174
+ ## Graph Schema
175
+
176
+ ### Nodes
177
+
178
+ | Field | Type | Description |
179
+ |---|---|---|
180
+ | `kind` | `NodeKind` | Function, Method, Constructor, Class, Interface, Enum, Test |
181
+ | `name` | `string` | Symbol name |
182
+ | `qualified_name` | `string` | Unique ID: `file::Class.method` |
183
+ | `file_path` | `string` | Relative file path |
184
+ | `line_start` / `line_end` | `number` | Source location |
185
+ | `language` | `string` | Source language |
186
+ | `is_test` | `boolean` | Whether it's a test function |
187
+
188
+ ### Edges
189
+
190
+ | Field | Type | Description |
191
+ |---|---|---|
192
+ | `kind` | `EdgeKind` | CALLS, IMPORTS, INHERITS, IMPLEMENTS, TESTED_BY, CONTAINS |
193
+ | `source_qualified` | `string` | Caller/parent node |
194
+ | `target_qualified` | `string` | Callee/child node |
195
+ | `confidence` | `number` | 0.0–1.0 (for CALLS edges) |
196
+
197
+ ### Confidence Levels
198
+
199
+ | Source | Confidence | Description |
200
+ |---|---|---|
201
+ | DI injection | 0.90–0.95 | Constructor/property injection patterns |
202
+ | Same file | 0.85 | Call within the same file |
203
+ | Import resolved | 0.70–0.90 | Cross-file call via import |
204
+ | Unique match | 0.50 | Only one candidate across codebase |
205
+ | Ambiguous | 0.30 | Multiple candidates found |
206
+
207
+ ## Examples
208
+
209
+ The `examples/` directory contains real output from running kodus-graph on a sample TypeScript project:
210
+
211
+ | File | Command | Description |
212
+ |---|---|---|
213
+ | [`parse-output.json`](examples/parse-output.json) | `parse --all` | Full graph with 17 nodes, 21 edges |
214
+ | [`analyze-output.json`](examples/analyze-output.json) | `analyze --files src/auth.ts` | Blast radius, risk score, test gaps |
215
+ | [`context-output.json`](examples/context-output.json) | `context --files src/auth.ts` | Enriched review context for AI agents |
216
+
217
+ ## Architecture
218
+
219
+ ```
220
+ Source Code → Parser → Resolver → Graph → Analysis
221
+ ```
222
+
223
+ | Layer | Path | Responsibility |
224
+ |---|---|---|
225
+ | **Parser** | `src/parser/` | AST extraction via ast-grep (functions, classes, imports, tests) |
226
+ | **Resolver** | `src/resolver/` | Import resolution, call resolution, symbol table |
227
+ | **Graph** | `src/graph/` | Node/edge building, incremental merging, JSON output |
228
+ | **Analysis** | `src/analysis/` | Blast radius, risk score, test gaps, flows, context |
229
+
230
+ ## Development
231
+
232
+ ```bash
233
+ # Install dependencies
234
+ bun install
235
+
236
+ # Run in dev mode
237
+ bun run dev parse --all --repo-dir ./my-project --out graph.json
238
+
239
+ # Run tests
240
+ bun test
241
+
242
+ # Full check (typecheck + lint + tests)
243
+ bun run check
244
+
245
+ # Lint & format
246
+ bun run lint:fix
247
+ bun run format
248
+ ```
249
+
250
+ ## License
251
+
252
+ MIT
@@ -0,0 +1,2 @@
1
+ import type { BlastRadiusResult, GraphData } from '../graph/types';
2
+ export declare function computeBlastRadius(graph: GraphData, changedFiles: string[], maxDepth?: number): BlastRadiusResult;
@@ -0,0 +1,57 @@
1
+ export function computeBlastRadius(graph, changedFiles, maxDepth = 2) {
2
+ // Build adjacency list from CALLS edges (callers of changed nodes)
3
+ const adj = new Map();
4
+ for (const edge of graph.edges) {
5
+ if (edge.kind !== 'CALLS' && edge.kind !== 'IMPORTS') {
6
+ continue;
7
+ }
8
+ // Reverse direction: target -> source (who calls/imports this?)
9
+ if (!adj.has(edge.target_qualified)) {
10
+ adj.set(edge.target_qualified, new Set());
11
+ }
12
+ adj.get(edge.target_qualified).add(edge.source_qualified);
13
+ // Forward direction too for IMPORTS
14
+ if (edge.kind === 'IMPORTS') {
15
+ if (!adj.has(edge.source_qualified)) {
16
+ adj.set(edge.source_qualified, new Set());
17
+ }
18
+ adj.get(edge.source_qualified).add(edge.target_qualified);
19
+ }
20
+ }
21
+ // Seed: all nodes in changed files
22
+ const changedSet = new Set(changedFiles);
23
+ const seeds = graph.nodes.filter((n) => changedSet.has(n.file_path)).map((n) => n.qualified_name);
24
+ // BFS
25
+ const visited = new Set(seeds);
26
+ const byDepth = {};
27
+ let frontier = seeds;
28
+ for (let depth = 1; depth <= maxDepth; depth++) {
29
+ const next = [];
30
+ for (const node of frontier) {
31
+ for (const neighbor of adj.get(node) || []) {
32
+ if (!visited.has(neighbor)) {
33
+ visited.add(neighbor);
34
+ next.push(neighbor);
35
+ }
36
+ }
37
+ }
38
+ if (next.length > 0) {
39
+ byDepth[String(depth)] = next;
40
+ }
41
+ frontier = next;
42
+ }
43
+ // Count unique files
44
+ const nodeIndex = new Map(graph.nodes.map((n) => [n.qualified_name, n]));
45
+ const impactedFiles = new Set();
46
+ for (const q of visited) {
47
+ const node = nodeIndex.get(q);
48
+ if (node) {
49
+ impactedFiles.add(node.file_path);
50
+ }
51
+ }
52
+ return {
53
+ total_functions: visited.size,
54
+ total_files: impactedFiles.size,
55
+ by_depth: byDepth,
56
+ };
57
+ }
@@ -0,0 +1,28 @@
1
+ import type { IndexedGraph } from '../graph/loader';
2
+ export interface CommunityOptions {
3
+ depth: number;
4
+ minSize: number;
5
+ }
6
+ export interface Community {
7
+ name: string;
8
+ files: string[];
9
+ node_count: number;
10
+ cohesion: number;
11
+ language: string;
12
+ }
13
+ export interface CouplingPair {
14
+ source: string;
15
+ target: string;
16
+ edges: number;
17
+ strength: 'HIGH' | 'MEDIUM' | 'LOW';
18
+ }
19
+ export interface CommunitiesResult {
20
+ communities: Community[];
21
+ coupling: CouplingPair[];
22
+ summary: {
23
+ total_communities: number;
24
+ avg_cohesion: number;
25
+ high_coupling_pairs: number;
26
+ };
27
+ }
28
+ export declare function detectCommunities(graph: IndexedGraph, opts: CommunityOptions): CommunitiesResult;
@@ -0,0 +1,100 @@
1
+ function getCommunityKey(filePath, depth) {
2
+ const parts = filePath.split('/');
3
+ return parts.slice(0, depth).join('/');
4
+ }
5
+ export function detectCommunities(graph, opts) {
6
+ const { depth, minSize } = opts;
7
+ // Group nodes by directory
8
+ const groups = new Map(); // community -> files
9
+ const nodeComm = new Map(); // qualified_name -> community
10
+ for (const node of graph.nodes) {
11
+ const key = getCommunityKey(node.file_path, depth);
12
+ if (!groups.has(key)) {
13
+ groups.set(key, new Set());
14
+ }
15
+ groups.get(key).add(node.file_path);
16
+ nodeComm.set(node.qualified_name, key);
17
+ }
18
+ // Count internal and cross edges per community pair
19
+ const internalEdges = new Map();
20
+ const crossEdges = new Map(); // "a|b" -> count
21
+ for (const edge of graph.edges) {
22
+ if (edge.kind !== 'CALLS' && edge.kind !== 'IMPORTS') {
23
+ continue;
24
+ }
25
+ const srcComm = nodeComm.get(edge.source_qualified);
26
+ const tgtComm = nodeComm.get(edge.target_qualified);
27
+ if (!srcComm || !tgtComm) {
28
+ continue;
29
+ }
30
+ if (srcComm === tgtComm) {
31
+ internalEdges.set(srcComm, (internalEdges.get(srcComm) || 0) + 1);
32
+ }
33
+ else {
34
+ const pairKey = [srcComm, tgtComm].sort().join('|');
35
+ crossEdges.set(pairKey, (crossEdges.get(pairKey) || 0) + 1);
36
+ }
37
+ }
38
+ // Build communities
39
+ const communities = [];
40
+ for (const [name, files] of groups) {
41
+ const nodeCount = graph.nodes.filter((n) => getCommunityKey(n.file_path, depth) === name).length;
42
+ if (nodeCount < minSize) {
43
+ continue;
44
+ }
45
+ const internal = internalEdges.get(name) || 0;
46
+ const maxPossible = nodeCount * (nodeCount - 1);
47
+ const cohesion = maxPossible > 0 ? Math.round((internal / maxPossible) * 100) / 100 : 0;
48
+ const langs = new Map();
49
+ for (const n of graph.nodes) {
50
+ if (getCommunityKey(n.file_path, depth) === name) {
51
+ langs.set(n.language, (langs.get(n.language) || 0) + 1);
52
+ }
53
+ }
54
+ let dominant = 'unknown';
55
+ let maxCount = 0;
56
+ for (const [lang, count] of langs) {
57
+ if (count > maxCount) {
58
+ dominant = lang;
59
+ maxCount = count;
60
+ }
61
+ }
62
+ communities.push({
63
+ name,
64
+ files: [...files].sort(),
65
+ node_count: nodeCount,
66
+ cohesion,
67
+ language: dominant,
68
+ });
69
+ }
70
+ communities.sort((a, b) => b.node_count - a.node_count);
71
+ // Build coupling pairs
72
+ const communityNames = new Set(communities.map((c) => c.name));
73
+ const coupling = [];
74
+ for (const [pairKey, count] of crossEdges) {
75
+ const [src, tgt] = pairKey.split('|');
76
+ if (!communityNames.has(src) || !communityNames.has(tgt)) {
77
+ continue;
78
+ }
79
+ const srcTotal = graph.edges.filter((e) => {
80
+ const c = nodeComm.get(e.source_qualified);
81
+ return c === src || c === tgt;
82
+ }).length;
83
+ const ratio = srcTotal > 0 ? count / srcTotal : 0;
84
+ const strength = ratio > 0.3 ? 'HIGH' : ratio > 0.1 ? 'MEDIUM' : 'LOW';
85
+ coupling.push({ source: src, target: tgt, edges: count, strength });
86
+ }
87
+ coupling.sort((a, b) => b.edges - a.edges);
88
+ const avgCohesion = communities.length > 0
89
+ ? Math.round((communities.reduce((s, c) => s + c.cohesion, 0) / communities.length) * 100) / 100
90
+ : 0;
91
+ return {
92
+ communities,
93
+ coupling,
94
+ summary: {
95
+ total_communities: communities.length,
96
+ avg_cohesion: avgCohesion,
97
+ high_coupling_pairs: coupling.filter((c) => c.strength === 'HIGH').length,
98
+ },
99
+ };
100
+ }
@@ -0,0 +1,34 @@
1
+ import type { AffectedFlow, ContextAnalysisMetadata, GraphData, GraphEdge, GraphNode, ParseMetadata } from '../graph/types';
2
+ import { computeBlastRadius } from './blast-radius';
3
+ import { type DiffResult } from './diff';
4
+ import { enrichChangedFunctions } from './enrich';
5
+ import { extractInheritance } from './inheritance';
6
+ import { computeRiskScore } from './risk-score';
7
+ import { findTestGaps } from './test-gaps';
8
+ export interface ContextV2Output {
9
+ graph: {
10
+ nodes: GraphNode[];
11
+ edges: GraphEdge[];
12
+ metadata: ParseMetadata;
13
+ };
14
+ analysis: {
15
+ changed_functions: ReturnType<typeof enrichChangedFunctions>;
16
+ structural_diff: DiffResult;
17
+ blast_radius: ReturnType<typeof computeBlastRadius>;
18
+ affected_flows: AffectedFlow[];
19
+ inheritance: ReturnType<typeof extractInheritance>;
20
+ test_gaps: ReturnType<typeof findTestGaps>;
21
+ risk: ReturnType<typeof computeRiskScore>;
22
+ metadata: ContextAnalysisMetadata;
23
+ };
24
+ }
25
+ interface BuildContextV2Options {
26
+ mergedGraph: GraphData;
27
+ oldGraph: GraphData | null;
28
+ changedFiles: string[];
29
+ minConfidence: number;
30
+ maxDepth: number;
31
+ skipTests?: boolean;
32
+ }
33
+ export declare function buildContextV2(opts: BuildContextV2Options): ContextV2Output;
34
+ export {};
@@ -0,0 +1,83 @@
1
+ import { performance } from 'perf_hooks';
2
+ import { indexGraph } from '../graph/loader';
3
+ import { computeBlastRadius } from './blast-radius';
4
+ import { computeStructuralDiff } from './diff';
5
+ import { enrichChangedFunctions } from './enrich';
6
+ import { detectFlows } from './flows';
7
+ import { extractInheritance } from './inheritance';
8
+ import { computeRiskScore } from './risk-score';
9
+ import { findTestGaps } from './test-gaps';
10
+ export function buildContextV2(opts) {
11
+ const t0 = performance.now();
12
+ const { mergedGraph, oldGraph, changedFiles, minConfidence, maxDepth } = opts;
13
+ // Phase 1: Index
14
+ const indexed = indexGraph(mergedGraph);
15
+ const oldIndexed = oldGraph ? indexGraph(oldGraph) : indexGraph({ nodes: [], edges: [] });
16
+ // Phase 2: Independent analyses
17
+ const changedSet = new Set(changedFiles);
18
+ const newNodesInChanged = mergedGraph.nodes.filter((n) => changedSet.has(n.file_path));
19
+ const newEdgesInChanged = mergedGraph.edges.filter((e) => changedSet.has(e.file_path));
20
+ const structuralDiff = computeStructuralDiff(oldIndexed, newNodesInChanged, newEdgesInChanged, changedFiles);
21
+ const blastRadius = computeBlastRadius(mergedGraph, changedFiles, maxDepth);
22
+ const allFlows = detectFlows(indexed, { maxDepth: 10, type: 'all' });
23
+ const testGaps = opts.skipTests ? [] : findTestGaps(mergedGraph, changedFiles);
24
+ const risk = computeRiskScore(mergedGraph, changedFiles, blastRadius, { skipTests: opts.skipTests });
25
+ const inheritance = extractInheritance(indexed, changedFiles);
26
+ // Phase 3: Filter affected flows
27
+ const changedFuncSet = new Set(mergedGraph.nodes.filter((n) => changedSet.has(n.file_path) && !n.is_test).map((n) => n.qualified_name));
28
+ const affectedFlows = [];
29
+ for (const flow of allFlows.flows) {
30
+ const touches = flow.path.filter((qn) => changedFuncSet.has(qn));
31
+ if (touches.length > 0) {
32
+ affectedFlows.push({
33
+ entry_point: flow.entry_point,
34
+ type: flow.type,
35
+ touches_changed: touches,
36
+ depth: flow.depth,
37
+ path: flow.path,
38
+ });
39
+ }
40
+ }
41
+ // Phase 3: Enrichment
42
+ const enriched = enrichChangedFunctions(indexed, changedFiles, structuralDiff, allFlows.flows, minConfidence);
43
+ // Phase 4: Assembly
44
+ const totalCallers = enriched.reduce((s, f) => s + f.callers.length, 0);
45
+ const totalCallees = enriched.reduce((s, f) => s + f.callees.length, 0);
46
+ const metadata = {
47
+ changed_functions_count: enriched.length,
48
+ total_callers: totalCallers,
49
+ total_callees: totalCallees,
50
+ untested_count: testGaps.length,
51
+ affected_flows_count: affectedFlows.length,
52
+ duration_ms: Math.round(performance.now() - t0),
53
+ min_confidence: minConfidence,
54
+ };
55
+ const graphMetadata = indexed.metadata.repo_dir
56
+ ? indexed.metadata
57
+ : {
58
+ repo_dir: '',
59
+ files_parsed: changedFiles.length,
60
+ total_nodes: mergedGraph.nodes.length,
61
+ total_edges: mergedGraph.edges.length,
62
+ duration_ms: 0,
63
+ parse_errors: 0,
64
+ extract_errors: 0,
65
+ };
66
+ return {
67
+ graph: {
68
+ nodes: mergedGraph.nodes,
69
+ edges: mergedGraph.edges,
70
+ metadata: graphMetadata,
71
+ },
72
+ analysis: {
73
+ changed_functions: enriched,
74
+ structural_diff: structuralDiff,
75
+ blast_radius: blastRadius,
76
+ affected_flows: affectedFlows,
77
+ inheritance,
78
+ test_gaps: testGaps,
79
+ risk,
80
+ metadata,
81
+ },
82
+ };
83
+ }
@@ -0,0 +1,35 @@
1
+ import type { IndexedGraph } from '../graph/loader';
2
+ import type { GraphEdge, GraphNode } from '../graph/types';
3
+ export interface NodeChange {
4
+ qualified_name: string;
5
+ kind: string;
6
+ file_path: string;
7
+ line_start: number;
8
+ line_end: number;
9
+ }
10
+ export interface ModifiedNode {
11
+ qualified_name: string;
12
+ changes: string[];
13
+ }
14
+ export interface DiffResult {
15
+ changed_files: string[];
16
+ summary: {
17
+ added: number;
18
+ removed: number;
19
+ modified: number;
20
+ };
21
+ nodes: {
22
+ added: NodeChange[];
23
+ removed: NodeChange[];
24
+ modified: ModifiedNode[];
25
+ };
26
+ edges: {
27
+ added: Pick<GraphEdge, 'kind' | 'source_qualified' | 'target_qualified'>[];
28
+ removed: Pick<GraphEdge, 'kind' | 'source_qualified' | 'target_qualified'>[];
29
+ };
30
+ risk_by_file: Record<string, {
31
+ dependents: number;
32
+ risk: 'HIGH' | 'MEDIUM' | 'LOW';
33
+ }>;
34
+ }
35
+ export declare function computeStructuralDiff(oldGraph: IndexedGraph, newNodes: GraphNode[], newEdges: GraphEdge[], changedFiles: string[]): DiffResult;