@kodus/kodus-graph 0.2.8 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +252 -0
- package/dist/analysis/blast-radius.d.ts +2 -0
- package/dist/analysis/blast-radius.js +55 -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 +92 -0
- package/dist/analysis/diff.d.ts +41 -0
- package/dist/analysis/diff.js +155 -0
- package/dist/analysis/enrich.d.ts +5 -0
- package/dist/analysis/enrich.js +126 -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 +173 -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 +210 -0
- package/dist/commands/analyze.d.ts +9 -0
- package/dist/commands/analyze.js +116 -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 +11 -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 +6 -0
- package/dist/graph/builder.js +248 -0
- package/dist/graph/edges.d.ts +23 -0
- package/dist/graph/edges.js +159 -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 +252 -0
- package/dist/graph/types.js +1 -0
- package/dist/parser/batch.d.ts +5 -0
- package/dist/parser/batch.js +93 -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 +304 -0
- package/dist/resolver/call-resolver.d.ts +36 -0
- package/dist/resolver/call-resolver.js +178 -0
- package/dist/resolver/external-detector.d.ts +11 -0
- package/dist/resolver/external-detector.js +820 -0
- package/dist/resolver/fs-cache.d.ts +8 -0
- package/dist/resolver/fs-cache.js +36 -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 +310 -0
- package/dist/resolver/languages/csharp.d.ts +3 -0
- package/dist/resolver/languages/csharp.js +94 -0
- package/dist/resolver/languages/go.d.ts +3 -0
- package/dist/resolver/languages/go.js +197 -0
- package/dist/resolver/languages/java.d.ts +1 -0
- package/dist/resolver/languages/java.js +193 -0
- package/dist/resolver/languages/php.d.ts +3 -0
- package/dist/resolver/languages/php.js +75 -0
- package/dist/resolver/languages/python.d.ts +11 -0
- package/dist/resolver/languages/python.js +127 -0
- package/dist/resolver/languages/ruby.d.ts +24 -0
- package/dist/resolver/languages/ruby.js +110 -0
- package/dist/resolver/languages/rust.d.ts +1 -0
- package/dist/resolver/languages/rust.js +197 -0
- package/dist/resolver/languages/typescript.d.ts +35 -0
- package/dist/resolver/languages/typescript.js +416 -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
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
export const SKIP_DIRS = new Set([
|
|
2
|
+
'node_modules',
|
|
3
|
+
'.git',
|
|
4
|
+
'dist',
|
|
5
|
+
'build',
|
|
6
|
+
'.next',
|
|
7
|
+
'coverage',
|
|
8
|
+
'vendor',
|
|
9
|
+
'__pycache__',
|
|
10
|
+
'.venv',
|
|
11
|
+
'venv',
|
|
12
|
+
'target',
|
|
13
|
+
'.turbo',
|
|
14
|
+
'.cache',
|
|
15
|
+
'.output',
|
|
16
|
+
'out',
|
|
17
|
+
'.nuxt',
|
|
18
|
+
'.svelte-kit',
|
|
19
|
+
'.idea',
|
|
20
|
+
'.mypy_cache',
|
|
21
|
+
'.tox',
|
|
22
|
+
'.pytest_cache',
|
|
23
|
+
'.eggs',
|
|
24
|
+
'bower_components',
|
|
25
|
+
]);
|
|
26
|
+
/** File name patterns to skip during discovery (minified, bundled, vendored) */
|
|
27
|
+
const SKIP_FILE_PATTERNS = [
|
|
28
|
+
/\.min\.\w+$/, // *.min.js, *.min.css
|
|
29
|
+
/[.-]bundle\.\w+$/, // *.bundle.js, *-bundle.js
|
|
30
|
+
/\.chunk\.\w+$/, // *.chunk.js (webpack)
|
|
31
|
+
/\.packed\.\w+$/, // *.packed.js
|
|
32
|
+
];
|
|
33
|
+
export function isSkippableFile(fileName) {
|
|
34
|
+
return SKIP_FILE_PATTERNS.some((p) => p.test(fileName));
|
|
35
|
+
}
|
|
36
|
+
export const NOISE = new Set([
|
|
37
|
+
// JS/TS builtins
|
|
38
|
+
'log',
|
|
39
|
+
'error',
|
|
40
|
+
'warn',
|
|
41
|
+
'info',
|
|
42
|
+
'debug',
|
|
43
|
+
'trace',
|
|
44
|
+
'push',
|
|
45
|
+
'pop',
|
|
46
|
+
'shift',
|
|
47
|
+
'unshift',
|
|
48
|
+
'splice',
|
|
49
|
+
'slice',
|
|
50
|
+
'map',
|
|
51
|
+
'filter',
|
|
52
|
+
'reduce',
|
|
53
|
+
'forEach',
|
|
54
|
+
'find',
|
|
55
|
+
'findIndex',
|
|
56
|
+
'some',
|
|
57
|
+
'every',
|
|
58
|
+
'flat',
|
|
59
|
+
'flatMap',
|
|
60
|
+
'sort',
|
|
61
|
+
'reverse',
|
|
62
|
+
'join',
|
|
63
|
+
'split',
|
|
64
|
+
'trim',
|
|
65
|
+
'replace',
|
|
66
|
+
'match',
|
|
67
|
+
'test',
|
|
68
|
+
'includes',
|
|
69
|
+
'indexOf',
|
|
70
|
+
'lastIndexOf',
|
|
71
|
+
'startsWith',
|
|
72
|
+
'endsWith',
|
|
73
|
+
'keys',
|
|
74
|
+
'values',
|
|
75
|
+
'entries',
|
|
76
|
+
'assign',
|
|
77
|
+
'freeze',
|
|
78
|
+
'create',
|
|
79
|
+
'stringify',
|
|
80
|
+
'parse',
|
|
81
|
+
'toString',
|
|
82
|
+
'toLowerCase',
|
|
83
|
+
'toUpperCase',
|
|
84
|
+
'concat',
|
|
85
|
+
'charAt',
|
|
86
|
+
'substring',
|
|
87
|
+
'parseInt',
|
|
88
|
+
'parseFloat',
|
|
89
|
+
'isNaN',
|
|
90
|
+
'isFinite',
|
|
91
|
+
'isArray',
|
|
92
|
+
'resolve',
|
|
93
|
+
'reject',
|
|
94
|
+
'all',
|
|
95
|
+
'allSettled',
|
|
96
|
+
'race',
|
|
97
|
+
'any',
|
|
98
|
+
'then',
|
|
99
|
+
'catch',
|
|
100
|
+
'finally',
|
|
101
|
+
'get',
|
|
102
|
+
'set',
|
|
103
|
+
'has',
|
|
104
|
+
'delete',
|
|
105
|
+
'clear',
|
|
106
|
+
'add',
|
|
107
|
+
'next',
|
|
108
|
+
'return',
|
|
109
|
+
'throw',
|
|
110
|
+
'setTimeout',
|
|
111
|
+
'clearTimeout',
|
|
112
|
+
'setInterval',
|
|
113
|
+
'clearInterval',
|
|
114
|
+
'require',
|
|
115
|
+
'length',
|
|
116
|
+
'call',
|
|
117
|
+
'apply',
|
|
118
|
+
'bind',
|
|
119
|
+
'createElement',
|
|
120
|
+
'useState',
|
|
121
|
+
'useEffect',
|
|
122
|
+
'useRef',
|
|
123
|
+
'useCallback',
|
|
124
|
+
'useMemo',
|
|
125
|
+
'useContext',
|
|
126
|
+
'useReducer',
|
|
127
|
+
'render',
|
|
128
|
+
// Test helpers
|
|
129
|
+
'expect',
|
|
130
|
+
'toBe',
|
|
131
|
+
'toEqual',
|
|
132
|
+
'toBeDefined',
|
|
133
|
+
'toBeNull',
|
|
134
|
+
'toBeUndefined',
|
|
135
|
+
'toBeTruthy',
|
|
136
|
+
'toBeFalsy',
|
|
137
|
+
'toContain',
|
|
138
|
+
'toHaveLength',
|
|
139
|
+
'toThrow',
|
|
140
|
+
'toHaveBeenCalled',
|
|
141
|
+
'toHaveBeenCalledWith',
|
|
142
|
+
'toMatchObject',
|
|
143
|
+
'toHaveBeenCalledTimes',
|
|
144
|
+
'toHaveProperty',
|
|
145
|
+
'describe',
|
|
146
|
+
'it',
|
|
147
|
+
'test',
|
|
148
|
+
'beforeEach',
|
|
149
|
+
'afterEach',
|
|
150
|
+
'beforeAll',
|
|
151
|
+
'afterAll',
|
|
152
|
+
'fn',
|
|
153
|
+
'spyOn',
|
|
154
|
+
'mock',
|
|
155
|
+
'mockResolvedValue',
|
|
156
|
+
'mockReturnValue',
|
|
157
|
+
'mockImplementation',
|
|
158
|
+
'mockReturnThis',
|
|
159
|
+
'now',
|
|
160
|
+
'toISOString',
|
|
161
|
+
'getTime',
|
|
162
|
+
// Globals
|
|
163
|
+
'console',
|
|
164
|
+
'Math',
|
|
165
|
+
'Date',
|
|
166
|
+
'JSON',
|
|
167
|
+
'Object',
|
|
168
|
+
'Array',
|
|
169
|
+
'String',
|
|
170
|
+
'Number',
|
|
171
|
+
'Boolean',
|
|
172
|
+
'Promise',
|
|
173
|
+
'Error',
|
|
174
|
+
'Map',
|
|
175
|
+
'Set',
|
|
176
|
+
'RegExp',
|
|
177
|
+
'Buffer',
|
|
178
|
+
'process',
|
|
179
|
+
// Python builtins
|
|
180
|
+
'print',
|
|
181
|
+
'len',
|
|
182
|
+
'range',
|
|
183
|
+
'enumerate',
|
|
184
|
+
'zip',
|
|
185
|
+
'isinstance',
|
|
186
|
+
'type',
|
|
187
|
+
'super',
|
|
188
|
+
'self',
|
|
189
|
+
'cls',
|
|
190
|
+
'None',
|
|
191
|
+
'True',
|
|
192
|
+
'False',
|
|
193
|
+
'append',
|
|
194
|
+
'extend',
|
|
195
|
+
'insert',
|
|
196
|
+
'remove',
|
|
197
|
+
'update',
|
|
198
|
+
'items',
|
|
199
|
+
'format',
|
|
200
|
+
'strip',
|
|
201
|
+
'upper',
|
|
202
|
+
'lower',
|
|
203
|
+
// Ruby builtins
|
|
204
|
+
'puts',
|
|
205
|
+
'raise',
|
|
206
|
+
'yield',
|
|
207
|
+
'each',
|
|
208
|
+
'do',
|
|
209
|
+
'end',
|
|
210
|
+
'attr_accessor',
|
|
211
|
+
'attr_reader',
|
|
212
|
+
'attr_writer',
|
|
213
|
+
'respond_to',
|
|
214
|
+
'render',
|
|
215
|
+
'redirect_to',
|
|
216
|
+
'before_action',
|
|
217
|
+
'after_action',
|
|
218
|
+
'validates',
|
|
219
|
+
'has_many',
|
|
220
|
+
'belongs_to',
|
|
221
|
+
'has_one',
|
|
222
|
+
'new',
|
|
223
|
+
'initialize',
|
|
224
|
+
// Go builtins
|
|
225
|
+
'fmt',
|
|
226
|
+
'Println',
|
|
227
|
+
'Printf',
|
|
228
|
+
'Sprintf',
|
|
229
|
+
'Errorf',
|
|
230
|
+
'make',
|
|
231
|
+
'panic',
|
|
232
|
+
'recover',
|
|
233
|
+
'defer',
|
|
234
|
+
// Java builtins
|
|
235
|
+
'System',
|
|
236
|
+
'println',
|
|
237
|
+
'equals',
|
|
238
|
+
'hashCode',
|
|
239
|
+
'getClass',
|
|
240
|
+
]);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/shared/logger.ts
|
|
2
|
+
export const log = {
|
|
3
|
+
info(msg, ctx) {
|
|
4
|
+
process.stderr.write(`[INFO] ${msg}${ctx ? ` ${JSON.stringify(ctx)}` : ''}\n`);
|
|
5
|
+
},
|
|
6
|
+
debug(msg, ctx) {
|
|
7
|
+
if (process.env.KODUS_GRAPH_DEBUG) {
|
|
8
|
+
process.stderr.write(`[DEBUG] ${msg}${ctx ? ` ${JSON.stringify(ctx)}` : ''}\n`);
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
warn(msg, ctx) {
|
|
12
|
+
process.stderr.write(`[WARN] ${msg}${ctx ? ` ${JSON.stringify(ctx)}` : ''}\n`);
|
|
13
|
+
},
|
|
14
|
+
error(msg, ctx) {
|
|
15
|
+
process.stderr.write(`[ERROR] ${msg}${ctx ? ` ${JSON.stringify(ctx)}` : ''}\n`);
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function qualifiedName(filePath: string, name: string, className?: string, isTest?: boolean): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { realpathSync } from 'fs';
|
|
2
|
+
import { relative, resolve } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Validate that a resolved path is within the repository root.
|
|
5
|
+
* Returns the validated absolute path.
|
|
6
|
+
* Throws if the path escapes the root.
|
|
7
|
+
*/
|
|
8
|
+
export function ensureWithinRoot(filePath, repoRoot) {
|
|
9
|
+
let absRoot;
|
|
10
|
+
try {
|
|
11
|
+
absRoot = realpathSync(resolve(repoRoot));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
absRoot = resolve(repoRoot);
|
|
15
|
+
}
|
|
16
|
+
let absPath;
|
|
17
|
+
try {
|
|
18
|
+
absPath = realpathSync(resolve(absRoot, filePath));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// File doesn't exist yet or is unreadable — use resolve without symlink follow
|
|
22
|
+
absPath = resolve(absRoot, filePath);
|
|
23
|
+
}
|
|
24
|
+
const rel = relative(absRoot, absPath);
|
|
25
|
+
if (rel.startsWith('..') || resolve(absRoot, rel) !== absPath) {
|
|
26
|
+
throw new Error(`Path escapes repository root: ${filePath}`);
|
|
27
|
+
}
|
|
28
|
+
return absPath;
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const GraphInputSchema: z.ZodObject<{
|
|
3
|
+
sha: z.ZodOptional<z.ZodString>;
|
|
4
|
+
nodes: z.ZodArray<z.ZodObject<{
|
|
5
|
+
kind: z.ZodEnum<{
|
|
6
|
+
Function: "Function";
|
|
7
|
+
Method: "Method";
|
|
8
|
+
Constructor: "Constructor";
|
|
9
|
+
Class: "Class";
|
|
10
|
+
Interface: "Interface";
|
|
11
|
+
Enum: "Enum";
|
|
12
|
+
Test: "Test";
|
|
13
|
+
}>;
|
|
14
|
+
name: z.ZodString;
|
|
15
|
+
qualified_name: z.ZodString;
|
|
16
|
+
file_path: z.ZodString;
|
|
17
|
+
line_start: z.ZodNumber;
|
|
18
|
+
line_end: z.ZodNumber;
|
|
19
|
+
language: z.ZodString;
|
|
20
|
+
is_test: z.ZodBoolean;
|
|
21
|
+
file_hash: z.ZodOptional<z.ZodString>;
|
|
22
|
+
content_hash: z.ZodOptional<z.ZodString>;
|
|
23
|
+
parent_name: z.ZodOptional<z.ZodString>;
|
|
24
|
+
params: z.ZodOptional<z.ZodString>;
|
|
25
|
+
return_type: z.ZodOptional<z.ZodString>;
|
|
26
|
+
modifiers: z.ZodOptional<z.ZodString>;
|
|
27
|
+
}, z.core.$strip>>;
|
|
28
|
+
edges: z.ZodArray<z.ZodObject<{
|
|
29
|
+
kind: z.ZodEnum<{
|
|
30
|
+
CALLS: "CALLS";
|
|
31
|
+
IMPORTS: "IMPORTS";
|
|
32
|
+
INHERITS: "INHERITS";
|
|
33
|
+
IMPLEMENTS: "IMPLEMENTS";
|
|
34
|
+
TESTED_BY: "TESTED_BY";
|
|
35
|
+
CONTAINS: "CONTAINS";
|
|
36
|
+
}>;
|
|
37
|
+
source_qualified: z.ZodString;
|
|
38
|
+
target_qualified: z.ZodString;
|
|
39
|
+
file_path: z.ZodString;
|
|
40
|
+
line: z.ZodNumber;
|
|
41
|
+
confidence: z.ZodOptional<z.ZodNumber>;
|
|
42
|
+
}, z.core.$strip>>;
|
|
43
|
+
}, z.core.$strip>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const GraphNodeSchema = z.object({
|
|
3
|
+
kind: z.enum(['Function', 'Method', 'Constructor', 'Class', 'Interface', 'Enum', 'Test']),
|
|
4
|
+
name: z.string(),
|
|
5
|
+
qualified_name: z.string(),
|
|
6
|
+
file_path: z.string(),
|
|
7
|
+
line_start: z.number(),
|
|
8
|
+
line_end: z.number(),
|
|
9
|
+
language: z.string(),
|
|
10
|
+
is_test: z.boolean(),
|
|
11
|
+
file_hash: z.string().optional(),
|
|
12
|
+
content_hash: z.string().optional(),
|
|
13
|
+
parent_name: z.string().optional(),
|
|
14
|
+
params: z.string().optional(),
|
|
15
|
+
return_type: z.string().optional(),
|
|
16
|
+
modifiers: z.string().optional(),
|
|
17
|
+
});
|
|
18
|
+
const GraphEdgeSchema = z.object({
|
|
19
|
+
kind: z.enum(['CALLS', 'IMPORTS', 'INHERITS', 'IMPLEMENTS', 'TESTED_BY', 'CONTAINS']),
|
|
20
|
+
source_qualified: z.string(),
|
|
21
|
+
target_qualified: z.string(),
|
|
22
|
+
file_path: z.string(),
|
|
23
|
+
line: z.number(),
|
|
24
|
+
confidence: z.number().optional(),
|
|
25
|
+
});
|
|
26
|
+
export const GraphInputSchema = z.object({
|
|
27
|
+
sha: z.string().optional(),
|
|
28
|
+
nodes: z.array(GraphNodeSchema),
|
|
29
|
+
edges: z.array(GraphEdgeSchema),
|
|
30
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a secure temp directory + file path.
|
|
3
|
+
* Directory is created with 0700 permissions via mkdtempSync.
|
|
4
|
+
* File name uses crypto.randomBytes for unpredictability.
|
|
5
|
+
*
|
|
6
|
+
* Caller is responsible for cleanup (rmSync(dir, { recursive: true, force: true })).
|
|
7
|
+
*/
|
|
8
|
+
export declare function createSecureTempFile(prefix: string): {
|
|
9
|
+
dir: string;
|
|
10
|
+
filePath: string;
|
|
11
|
+
};
|
|
@@ -2,7 +2,6 @@ import { randomBytes } from 'crypto';
|
|
|
2
2
|
import { mkdtempSync } from 'fs';
|
|
3
3
|
import { tmpdir } from 'os';
|
|
4
4
|
import { join } from 'path';
|
|
5
|
-
|
|
6
5
|
/**
|
|
7
6
|
* Create a secure temp directory + file path.
|
|
8
7
|
* Directory is created with 0700 permissions via mkdtempSync.
|
|
@@ -10,8 +9,8 @@ import { join } from 'path';
|
|
|
10
9
|
*
|
|
11
10
|
* Caller is responsible for cleanup (rmSync(dir, { recursive: true, force: true })).
|
|
12
11
|
*/
|
|
13
|
-
export function createSecureTempFile(prefix
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
export function createSecureTempFile(prefix) {
|
|
13
|
+
const dir = mkdtempSync(join(tmpdir(), `kodus-graph-${prefix}-`));
|
|
14
|
+
const filePath = join(dir, `${randomBytes(8).toString('hex')}.json`);
|
|
15
|
+
return { dir, filePath };
|
|
17
16
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kodus/kodus-graph",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "Code graph builder for Kodus code review — parses source code into structural graphs with nodes, edges, and analysis",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"main": "./dist/cli.js",
|
|
7
|
+
"types": "./dist/cli.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/cli.d.ts",
|
|
11
|
+
"import": "./dist/cli.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
6
14
|
"bin": {
|
|
7
|
-
"kodus-graph": "./
|
|
15
|
+
"kodus-graph": "./dist/cli.js"
|
|
8
16
|
},
|
|
9
17
|
"files": [
|
|
10
|
-
"
|
|
18
|
+
"dist/**/*.js",
|
|
19
|
+
"dist/**/*.d.ts",
|
|
11
20
|
"README.md",
|
|
12
21
|
"LICENSE"
|
|
13
22
|
],
|
|
@@ -19,7 +28,9 @@
|
|
|
19
28
|
"format": "biome format --write src/ tests/",
|
|
20
29
|
"typecheck": "tsc --noEmit",
|
|
21
30
|
"check": "bun run typecheck && bun run lint && bun test",
|
|
22
|
-
"build": "
|
|
31
|
+
"build:dist": "tsc -p tsconfig.build.json",
|
|
32
|
+
"prepublishOnly": "bun run build:dist",
|
|
33
|
+
"build": "bun build src/cli.ts --compile --outfile dist/kodus-graph",
|
|
23
34
|
"build:all": "bun run build:darwin-arm64 && bun run build:darwin-x64 && bun run build:linux-x64 && bun run build:linux-arm64",
|
|
24
35
|
"build:darwin-arm64": "bun build src/cli.ts --compile --target=bun-darwin-arm64 --outfile dist/kodus-graph-darwin-arm64",
|
|
25
36
|
"build:darwin-x64": "bun build src/cli.ts --compile --target=bun-darwin-x64 --outfile dist/kodus-graph-darwin-x64",
|
|
@@ -39,8 +50,11 @@
|
|
|
39
50
|
"type": "git",
|
|
40
51
|
"url": "https://github.com/kodustech/kodus-graph.git"
|
|
41
52
|
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
42
56
|
"engines": {
|
|
43
|
-
"bun": ">=1.
|
|
57
|
+
"bun": ">=1.3.0"
|
|
44
58
|
},
|
|
45
59
|
"dependencies": {
|
|
46
60
|
"@ast-grep/lang-csharp": "^0.0.6",
|
|
@@ -56,7 +70,7 @@
|
|
|
56
70
|
},
|
|
57
71
|
"devDependencies": {
|
|
58
72
|
"@biomejs/biome": "^2.4.10",
|
|
59
|
-
"@types/bun": "
|
|
73
|
+
"@types/bun": "^1.3.11",
|
|
60
74
|
"typescript": "^6.0.2"
|
|
61
75
|
}
|
|
62
76
|
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { BlastRadiusResult, GraphData } from '../graph/types';
|
|
2
|
-
|
|
3
|
-
export function computeBlastRadius(graph: GraphData, changedFiles: string[], maxDepth: number = 2): BlastRadiusResult {
|
|
4
|
-
// Build adjacency list from CALLS edges (callers of changed nodes)
|
|
5
|
-
const adj = new Map<string, Set<string>>();
|
|
6
|
-
for (const edge of graph.edges) {
|
|
7
|
-
if (edge.kind !== 'CALLS' && edge.kind !== 'IMPORTS') continue;
|
|
8
|
-
// Reverse direction: target -> source (who calls/imports this?)
|
|
9
|
-
if (!adj.has(edge.target_qualified)) adj.set(edge.target_qualified, new Set());
|
|
10
|
-
adj.get(edge.target_qualified)!.add(edge.source_qualified);
|
|
11
|
-
// Forward direction too for IMPORTS
|
|
12
|
-
if (edge.kind === 'IMPORTS') {
|
|
13
|
-
if (!adj.has(edge.source_qualified)) adj.set(edge.source_qualified, new Set());
|
|
14
|
-
adj.get(edge.source_qualified)!.add(edge.target_qualified);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Seed: all nodes in changed files
|
|
19
|
-
const changedSet = new Set(changedFiles);
|
|
20
|
-
const seeds = graph.nodes.filter((n) => changedSet.has(n.file_path)).map((n) => n.qualified_name);
|
|
21
|
-
|
|
22
|
-
// BFS
|
|
23
|
-
const visited = new Set<string>(seeds);
|
|
24
|
-
const byDepth: Record<string, string[]> = {};
|
|
25
|
-
let frontier = seeds;
|
|
26
|
-
|
|
27
|
-
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
28
|
-
const next: string[] = [];
|
|
29
|
-
for (const node of frontier) {
|
|
30
|
-
for (const neighbor of adj.get(node) || []) {
|
|
31
|
-
if (!visited.has(neighbor)) {
|
|
32
|
-
visited.add(neighbor);
|
|
33
|
-
next.push(neighbor);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
if (next.length > 0) byDepth[String(depth)] = next;
|
|
38
|
-
frontier = next;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Count unique files
|
|
42
|
-
const nodeIndex = new Map(graph.nodes.map((n) => [n.qualified_name, n]));
|
|
43
|
-
const impactedFiles = new Set<string>();
|
|
44
|
-
for (const q of visited) {
|
|
45
|
-
const node = nodeIndex.get(q);
|
|
46
|
-
if (node) impactedFiles.add(node.file_path);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
total_functions: visited.size,
|
|
51
|
-
total_files: impactedFiles.size,
|
|
52
|
-
by_depth: byDepth,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import type { IndexedGraph } from '../graph/loader';
|
|
2
|
-
|
|
3
|
-
export interface CommunityOptions {
|
|
4
|
-
depth: number;
|
|
5
|
-
minSize: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface Community {
|
|
9
|
-
name: string;
|
|
10
|
-
files: string[];
|
|
11
|
-
node_count: number;
|
|
12
|
-
cohesion: number;
|
|
13
|
-
language: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface CouplingPair {
|
|
17
|
-
source: string;
|
|
18
|
-
target: string;
|
|
19
|
-
edges: number;
|
|
20
|
-
strength: 'HIGH' | 'MEDIUM' | 'LOW';
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface CommunitiesResult {
|
|
24
|
-
communities: Community[];
|
|
25
|
-
coupling: CouplingPair[];
|
|
26
|
-
summary: { total_communities: number; avg_cohesion: number; high_coupling_pairs: number };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getCommunityKey(filePath: string, depth: number): string {
|
|
30
|
-
const parts = filePath.split('/');
|
|
31
|
-
return parts.slice(0, depth).join('/');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function detectCommunities(graph: IndexedGraph, opts: CommunityOptions): CommunitiesResult {
|
|
35
|
-
const { depth, minSize } = opts;
|
|
36
|
-
|
|
37
|
-
// Group nodes by directory
|
|
38
|
-
const groups = new Map<string, Set<string>>(); // community -> files
|
|
39
|
-
const nodeComm = new Map<string, string>(); // qualified_name -> community
|
|
40
|
-
|
|
41
|
-
for (const node of graph.nodes) {
|
|
42
|
-
const key = getCommunityKey(node.file_path, depth);
|
|
43
|
-
if (!groups.has(key)) groups.set(key, new Set());
|
|
44
|
-
groups.get(key)!.add(node.file_path);
|
|
45
|
-
nodeComm.set(node.qualified_name, key);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Count internal and cross edges per community pair
|
|
49
|
-
const internalEdges = new Map<string, number>();
|
|
50
|
-
const crossEdges = new Map<string, number>(); // "a|b" -> count
|
|
51
|
-
|
|
52
|
-
for (const edge of graph.edges) {
|
|
53
|
-
if (edge.kind !== 'CALLS' && edge.kind !== 'IMPORTS') continue;
|
|
54
|
-
const srcComm = nodeComm.get(edge.source_qualified);
|
|
55
|
-
const tgtComm = nodeComm.get(edge.target_qualified);
|
|
56
|
-
if (!srcComm || !tgtComm) continue;
|
|
57
|
-
|
|
58
|
-
if (srcComm === tgtComm) {
|
|
59
|
-
internalEdges.set(srcComm, (internalEdges.get(srcComm) || 0) + 1);
|
|
60
|
-
} else {
|
|
61
|
-
const pairKey = [srcComm, tgtComm].sort().join('|');
|
|
62
|
-
crossEdges.set(pairKey, (crossEdges.get(pairKey) || 0) + 1);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Build communities
|
|
67
|
-
const communities: Community[] = [];
|
|
68
|
-
for (const [name, files] of groups) {
|
|
69
|
-
const nodeCount = graph.nodes.filter((n) => getCommunityKey(n.file_path, depth) === name).length;
|
|
70
|
-
if (nodeCount < minSize) continue;
|
|
71
|
-
|
|
72
|
-
const internal = internalEdges.get(name) || 0;
|
|
73
|
-
const maxPossible = nodeCount * (nodeCount - 1);
|
|
74
|
-
const cohesion = maxPossible > 0 ? Math.round((internal / maxPossible) * 100) / 100 : 0;
|
|
75
|
-
|
|
76
|
-
const langs = new Map<string, number>();
|
|
77
|
-
for (const n of graph.nodes) {
|
|
78
|
-
if (getCommunityKey(n.file_path, depth) === name) {
|
|
79
|
-
langs.set(n.language, (langs.get(n.language) || 0) + 1);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
let dominant = 'unknown';
|
|
83
|
-
let maxCount = 0;
|
|
84
|
-
for (const [lang, count] of langs) {
|
|
85
|
-
if (count > maxCount) {
|
|
86
|
-
dominant = lang;
|
|
87
|
-
maxCount = count;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
communities.push({
|
|
92
|
-
name,
|
|
93
|
-
files: [...files].sort(),
|
|
94
|
-
node_count: nodeCount,
|
|
95
|
-
cohesion,
|
|
96
|
-
language: dominant,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
communities.sort((a, b) => b.node_count - a.node_count);
|
|
101
|
-
|
|
102
|
-
// Build coupling pairs
|
|
103
|
-
const communityNames = new Set(communities.map((c) => c.name));
|
|
104
|
-
const coupling: CouplingPair[] = [];
|
|
105
|
-
for (const [pairKey, count] of crossEdges) {
|
|
106
|
-
const [src, tgt] = pairKey.split('|');
|
|
107
|
-
if (!communityNames.has(src) || !communityNames.has(tgt)) continue;
|
|
108
|
-
|
|
109
|
-
const srcTotal = graph.edges.filter((e) => {
|
|
110
|
-
const c = nodeComm.get(e.source_qualified);
|
|
111
|
-
return c === src || c === tgt;
|
|
112
|
-
}).length;
|
|
113
|
-
const ratio = srcTotal > 0 ? count / srcTotal : 0;
|
|
114
|
-
const strength = ratio > 0.3 ? 'HIGH' : ratio > 0.1 ? 'MEDIUM' : 'LOW';
|
|
115
|
-
|
|
116
|
-
coupling.push({ source: src, target: tgt, edges: count, strength });
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
coupling.sort((a, b) => b.edges - a.edges);
|
|
120
|
-
|
|
121
|
-
const avgCohesion =
|
|
122
|
-
communities.length > 0
|
|
123
|
-
? Math.round((communities.reduce((s, c) => s + c.cohesion, 0) / communities.length) * 100) / 100
|
|
124
|
-
: 0;
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
communities,
|
|
128
|
-
coupling,
|
|
129
|
-
summary: {
|
|
130
|
-
total_communities: communities.length,
|
|
131
|
-
avg_cohesion: avgCohesion,
|
|
132
|
-
high_coupling_pairs: coupling.filter((c) => c.strength === 'HIGH').length,
|
|
133
|
-
},
|
|
134
|
-
};
|
|
135
|
-
}
|