@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.
- package/LICENSE +21 -0
- package/README.md +252 -0
- package/dist/analysis/blast-radius.d.ts +2 -0
- package/dist/analysis/blast-radius.js +57 -0
- package/dist/analysis/communities.d.ts +28 -0
- package/dist/analysis/communities.js +100 -0
- package/dist/analysis/context-builder.d.ts +34 -0
- package/dist/analysis/context-builder.js +83 -0
- package/dist/analysis/diff.d.ts +35 -0
- package/dist/analysis/diff.js +140 -0
- package/dist/analysis/enrich.d.ts +5 -0
- package/dist/analysis/enrich.js +98 -0
- package/dist/analysis/flows.d.ts +27 -0
- package/dist/analysis/flows.js +86 -0
- package/dist/analysis/inheritance.d.ts +3 -0
- package/dist/analysis/inheritance.js +31 -0
- package/dist/analysis/prompt-formatter.d.ts +2 -0
- package/dist/analysis/prompt-formatter.js +166 -0
- package/dist/analysis/risk-score.d.ts +4 -0
- package/dist/analysis/risk-score.js +51 -0
- package/dist/analysis/search.d.ts +11 -0
- package/dist/analysis/search.js +64 -0
- package/dist/analysis/test-gaps.d.ts +2 -0
- package/dist/analysis/test-gaps.js +14 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +208 -0
- package/dist/commands/analyze.d.ts +9 -0
- package/dist/commands/analyze.js +114 -0
- package/dist/commands/communities.d.ts +8 -0
- package/dist/commands/communities.js +9 -0
- package/dist/commands/context.d.ts +12 -0
- package/dist/commands/context.js +130 -0
- package/dist/commands/diff.d.ts +9 -0
- package/dist/commands/diff.js +89 -0
- package/dist/commands/flows.d.ts +8 -0
- package/dist/commands/flows.js +9 -0
- package/dist/commands/parse.d.ts +10 -0
- package/dist/commands/parse.js +101 -0
- package/dist/commands/search.d.ts +12 -0
- package/dist/commands/search.js +27 -0
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.js +154 -0
- package/dist/graph/builder.d.ts +2 -0
- package/dist/graph/builder.js +216 -0
- package/dist/graph/edges.d.ts +19 -0
- package/dist/graph/edges.js +105 -0
- package/dist/graph/json-writer.d.ts +9 -0
- package/dist/graph/json-writer.js +38 -0
- package/dist/graph/loader.d.ts +13 -0
- package/dist/graph/loader.js +101 -0
- package/dist/graph/merger.d.ts +7 -0
- package/dist/graph/merger.js +18 -0
- package/dist/graph/types.d.ts +249 -0
- package/dist/graph/types.js +1 -0
- package/dist/parser/batch.d.ts +4 -0
- package/dist/parser/batch.js +78 -0
- package/dist/parser/discovery.d.ts +7 -0
- package/dist/parser/discovery.js +61 -0
- package/dist/parser/extractor.d.ts +4 -0
- package/dist/parser/extractor.js +33 -0
- package/dist/parser/extractors/generic.d.ts +8 -0
- package/dist/parser/extractors/generic.js +471 -0
- package/dist/parser/extractors/python.d.ts +8 -0
- package/dist/parser/extractors/python.js +133 -0
- package/dist/parser/extractors/ruby.d.ts +8 -0
- package/dist/parser/extractors/ruby.js +153 -0
- package/dist/parser/extractors/typescript.d.ts +10 -0
- package/dist/parser/extractors/typescript.js +365 -0
- package/dist/parser/languages.d.ts +32 -0
- package/dist/parser/languages.js +303 -0
- package/dist/resolver/call-resolver.d.ts +36 -0
- package/dist/resolver/call-resolver.js +178 -0
- package/dist/resolver/import-map.d.ts +12 -0
- package/dist/resolver/import-map.js +21 -0
- package/dist/resolver/import-resolver.d.ts +19 -0
- package/dist/resolver/import-resolver.js +212 -0
- package/dist/resolver/languages/csharp.d.ts +1 -0
- package/dist/resolver/languages/csharp.js +31 -0
- package/dist/resolver/languages/go.d.ts +3 -0
- package/dist/resolver/languages/go.js +196 -0
- package/dist/resolver/languages/java.d.ts +1 -0
- package/dist/resolver/languages/java.js +108 -0
- package/dist/resolver/languages/php.d.ts +3 -0
- package/dist/resolver/languages/php.js +54 -0
- package/dist/resolver/languages/python.d.ts +11 -0
- package/dist/resolver/languages/python.js +51 -0
- package/dist/resolver/languages/ruby.d.ts +9 -0
- package/dist/resolver/languages/ruby.js +59 -0
- package/dist/resolver/languages/rust.d.ts +1 -0
- package/dist/resolver/languages/rust.js +196 -0
- package/dist/resolver/languages/typescript.d.ts +27 -0
- package/dist/resolver/languages/typescript.js +240 -0
- package/dist/resolver/re-export-resolver.d.ts +24 -0
- package/dist/resolver/re-export-resolver.js +57 -0
- package/dist/resolver/symbol-table.d.ts +17 -0
- package/dist/resolver/symbol-table.js +60 -0
- package/dist/shared/extract-calls.d.ts +26 -0
- package/dist/shared/extract-calls.js +57 -0
- package/dist/shared/file-hash.d.ts +3 -0
- package/dist/shared/file-hash.js +10 -0
- package/dist/shared/filters.d.ts +3 -0
- package/dist/shared/filters.js +240 -0
- package/dist/shared/logger.d.ts +6 -0
- package/dist/shared/logger.js +17 -0
- package/dist/shared/qualified-name.d.ts +1 -0
- package/dist/shared/qualified-name.js +9 -0
- package/dist/shared/safe-path.d.ts +6 -0
- package/dist/shared/safe-path.js +29 -0
- package/dist/shared/schemas.d.ts +43 -0
- package/dist/shared/schemas.js +30 -0
- package/dist/shared/temp.d.ts +11 -0
- package/{src/shared/temp.ts → dist/shared/temp.js} +4 -5
- package/package.json +20 -6
- package/src/analysis/blast-radius.ts +0 -54
- package/src/analysis/communities.ts +0 -135
- package/src/analysis/context-builder.ts +0 -130
- package/src/analysis/diff.ts +0 -169
- package/src/analysis/enrich.ts +0 -110
- package/src/analysis/flows.ts +0 -112
- package/src/analysis/inheritance.ts +0 -34
- package/src/analysis/prompt-formatter.ts +0 -175
- package/src/analysis/risk-score.ts +0 -62
- package/src/analysis/search.ts +0 -76
- package/src/analysis/test-gaps.ts +0 -21
- package/src/cli.ts +0 -210
- package/src/commands/analyze.ts +0 -128
- package/src/commands/communities.ts +0 -19
- package/src/commands/context.ts +0 -182
- package/src/commands/diff.ts +0 -96
- package/src/commands/flows.ts +0 -19
- package/src/commands/parse.ts +0 -124
- package/src/commands/search.ts +0 -41
- package/src/commands/update.ts +0 -166
- package/src/graph/builder.ts +0 -209
- package/src/graph/edges.ts +0 -101
- package/src/graph/json-writer.ts +0 -43
- package/src/graph/loader.ts +0 -113
- package/src/graph/merger.ts +0 -25
- package/src/graph/types.ts +0 -283
- package/src/parser/batch.ts +0 -82
- package/src/parser/discovery.ts +0 -75
- package/src/parser/extractor.ts +0 -37
- package/src/parser/extractors/generic.ts +0 -132
- package/src/parser/extractors/python.ts +0 -133
- package/src/parser/extractors/ruby.ts +0 -147
- package/src/parser/extractors/typescript.ts +0 -350
- package/src/parser/languages.ts +0 -122
- package/src/resolver/call-resolver.ts +0 -244
- package/src/resolver/import-map.ts +0 -27
- package/src/resolver/import-resolver.ts +0 -72
- package/src/resolver/languages/csharp.ts +0 -7
- package/src/resolver/languages/go.ts +0 -7
- package/src/resolver/languages/java.ts +0 -7
- package/src/resolver/languages/php.ts +0 -7
- package/src/resolver/languages/python.ts +0 -35
- package/src/resolver/languages/ruby.ts +0 -21
- package/src/resolver/languages/rust.ts +0 -7
- package/src/resolver/languages/typescript.ts +0 -168
- package/src/resolver/re-export-resolver.ts +0 -66
- package/src/resolver/symbol-table.ts +0 -67
- package/src/shared/extract-calls.ts +0 -75
- package/src/shared/file-hash.ts +0 -12
- package/src/shared/filters.ts +0 -243
- package/src/shared/logger.ts +0 -17
- package/src/shared/qualified-name.ts +0 -5
- package/src/shared/safe-path.ts +0 -31
- 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
|
+
[](https://www.npmjs.com/package/@kodus/kodus-graph)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](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,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;
|