@kodus/kodus-graph 0.1.0

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 (50) hide show
  1. package/package.json +62 -0
  2. package/src/analysis/blast-radius.ts +54 -0
  3. package/src/analysis/communities.ts +135 -0
  4. package/src/analysis/diff.ts +120 -0
  5. package/src/analysis/flows.ts +112 -0
  6. package/src/analysis/review-context.ts +141 -0
  7. package/src/analysis/risk-score.ts +62 -0
  8. package/src/analysis/search.ts +76 -0
  9. package/src/analysis/test-gaps.ts +21 -0
  10. package/src/cli.ts +192 -0
  11. package/src/commands/analyze.ts +66 -0
  12. package/src/commands/communities.ts +19 -0
  13. package/src/commands/context.ts +69 -0
  14. package/src/commands/diff.ts +96 -0
  15. package/src/commands/flows.ts +19 -0
  16. package/src/commands/parse.ts +100 -0
  17. package/src/commands/search.ts +41 -0
  18. package/src/commands/update.ts +166 -0
  19. package/src/graph/builder.ts +170 -0
  20. package/src/graph/edges.ts +101 -0
  21. package/src/graph/loader.ts +100 -0
  22. package/src/graph/merger.ts +25 -0
  23. package/src/graph/types.ts +218 -0
  24. package/src/parser/batch.ts +74 -0
  25. package/src/parser/discovery.ts +42 -0
  26. package/src/parser/extractor.ts +37 -0
  27. package/src/parser/extractors/generic.ts +87 -0
  28. package/src/parser/extractors/python.ts +127 -0
  29. package/src/parser/extractors/ruby.ts +142 -0
  30. package/src/parser/extractors/typescript.ts +329 -0
  31. package/src/parser/languages.ts +122 -0
  32. package/src/resolver/call-resolver.ts +179 -0
  33. package/src/resolver/import-map.ts +27 -0
  34. package/src/resolver/import-resolver.ts +72 -0
  35. package/src/resolver/languages/csharp.ts +7 -0
  36. package/src/resolver/languages/go.ts +7 -0
  37. package/src/resolver/languages/java.ts +7 -0
  38. package/src/resolver/languages/php.ts +7 -0
  39. package/src/resolver/languages/python.ts +35 -0
  40. package/src/resolver/languages/ruby.ts +21 -0
  41. package/src/resolver/languages/rust.ts +7 -0
  42. package/src/resolver/languages/typescript.ts +168 -0
  43. package/src/resolver/symbol-table.ts +53 -0
  44. package/src/shared/file-hash.ts +7 -0
  45. package/src/shared/filters.ts +243 -0
  46. package/src/shared/logger.ts +14 -0
  47. package/src/shared/qualified-name.ts +5 -0
  48. package/src/shared/safe-path.ts +31 -0
  49. package/src/shared/schemas.ts +31 -0
  50. package/src/shared/temp.ts +17 -0
@@ -0,0 +1,243 @@
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
+
27
+ /** File name patterns to skip during discovery (minified, bundled, vendored) */
28
+ const SKIP_FILE_PATTERNS: RegExp[] = [
29
+ /\.min\.\w+$/, // *.min.js, *.min.css
30
+ /[.-]bundle\.\w+$/, // *.bundle.js, *-bundle.js
31
+ /\.chunk\.\w+$/, // *.chunk.js (webpack)
32
+ /\.packed\.\w+$/, // *.packed.js
33
+ ];
34
+
35
+ export function isSkippableFile(fileName: string): boolean {
36
+ return SKIP_FILE_PATTERNS.some((p) => p.test(fileName));
37
+ }
38
+
39
+ export const NOISE = new Set([
40
+ // JS/TS builtins
41
+ 'log',
42
+ 'error',
43
+ 'warn',
44
+ 'info',
45
+ 'debug',
46
+ 'trace',
47
+ 'push',
48
+ 'pop',
49
+ 'shift',
50
+ 'unshift',
51
+ 'splice',
52
+ 'slice',
53
+ 'map',
54
+ 'filter',
55
+ 'reduce',
56
+ 'forEach',
57
+ 'find',
58
+ 'findIndex',
59
+ 'some',
60
+ 'every',
61
+ 'flat',
62
+ 'flatMap',
63
+ 'sort',
64
+ 'reverse',
65
+ 'join',
66
+ 'split',
67
+ 'trim',
68
+ 'replace',
69
+ 'match',
70
+ 'test',
71
+ 'includes',
72
+ 'indexOf',
73
+ 'lastIndexOf',
74
+ 'startsWith',
75
+ 'endsWith',
76
+ 'keys',
77
+ 'values',
78
+ 'entries',
79
+ 'assign',
80
+ 'freeze',
81
+ 'create',
82
+ 'stringify',
83
+ 'parse',
84
+ 'toString',
85
+ 'toLowerCase',
86
+ 'toUpperCase',
87
+ 'concat',
88
+ 'charAt',
89
+ 'substring',
90
+ 'parseInt',
91
+ 'parseFloat',
92
+ 'isNaN',
93
+ 'isFinite',
94
+ 'isArray',
95
+ 'resolve',
96
+ 'reject',
97
+ 'all',
98
+ 'allSettled',
99
+ 'race',
100
+ 'any',
101
+ 'then',
102
+ 'catch',
103
+ 'finally',
104
+ 'get',
105
+ 'set',
106
+ 'has',
107
+ 'delete',
108
+ 'clear',
109
+ 'add',
110
+ 'next',
111
+ 'return',
112
+ 'throw',
113
+ 'setTimeout',
114
+ 'clearTimeout',
115
+ 'setInterval',
116
+ 'clearInterval',
117
+ 'require',
118
+ 'length',
119
+ 'call',
120
+ 'apply',
121
+ 'bind',
122
+ 'createElement',
123
+ 'useState',
124
+ 'useEffect',
125
+ 'useRef',
126
+ 'useCallback',
127
+ 'useMemo',
128
+ 'useContext',
129
+ 'useReducer',
130
+ 'render',
131
+ // Test helpers
132
+ 'expect',
133
+ 'toBe',
134
+ 'toEqual',
135
+ 'toBeDefined',
136
+ 'toBeNull',
137
+ 'toBeUndefined',
138
+ 'toBeTruthy',
139
+ 'toBeFalsy',
140
+ 'toContain',
141
+ 'toHaveLength',
142
+ 'toThrow',
143
+ 'toHaveBeenCalled',
144
+ 'toHaveBeenCalledWith',
145
+ 'toMatchObject',
146
+ 'toHaveBeenCalledTimes',
147
+ 'toHaveProperty',
148
+ 'describe',
149
+ 'it',
150
+ 'test',
151
+ 'beforeEach',
152
+ 'afterEach',
153
+ 'beforeAll',
154
+ 'afterAll',
155
+ 'fn',
156
+ 'spyOn',
157
+ 'mock',
158
+ 'mockResolvedValue',
159
+ 'mockReturnValue',
160
+ 'mockImplementation',
161
+ 'mockReturnThis',
162
+ 'now',
163
+ 'toISOString',
164
+ 'getTime',
165
+ // Globals
166
+ 'console',
167
+ 'Math',
168
+ 'Date',
169
+ 'JSON',
170
+ 'Object',
171
+ 'Array',
172
+ 'String',
173
+ 'Number',
174
+ 'Boolean',
175
+ 'Promise',
176
+ 'Error',
177
+ 'Map',
178
+ 'Set',
179
+ 'RegExp',
180
+ 'Buffer',
181
+ 'process',
182
+ // Python builtins
183
+ 'print',
184
+ 'len',
185
+ 'range',
186
+ 'enumerate',
187
+ 'zip',
188
+ 'isinstance',
189
+ 'type',
190
+ 'super',
191
+ 'self',
192
+ 'cls',
193
+ 'None',
194
+ 'True',
195
+ 'False',
196
+ 'append',
197
+ 'extend',
198
+ 'insert',
199
+ 'remove',
200
+ 'update',
201
+ 'items',
202
+ 'format',
203
+ 'strip',
204
+ 'upper',
205
+ 'lower',
206
+ // Ruby builtins
207
+ 'puts',
208
+ 'raise',
209
+ 'yield',
210
+ 'each',
211
+ 'do',
212
+ 'end',
213
+ 'attr_accessor',
214
+ 'attr_reader',
215
+ 'attr_writer',
216
+ 'respond_to',
217
+ 'render',
218
+ 'redirect_to',
219
+ 'before_action',
220
+ 'after_action',
221
+ 'validates',
222
+ 'has_many',
223
+ 'belongs_to',
224
+ 'has_one',
225
+ 'new',
226
+ 'initialize',
227
+ // Go builtins
228
+ 'fmt',
229
+ 'Println',
230
+ 'Printf',
231
+ 'Sprintf',
232
+ 'Errorf',
233
+ 'make',
234
+ 'panic',
235
+ 'recover',
236
+ 'defer',
237
+ // Java builtins
238
+ 'System',
239
+ 'println',
240
+ 'equals',
241
+ 'hashCode',
242
+ 'getClass',
243
+ ]);
@@ -0,0 +1,14 @@
1
+ // src/shared/logger.ts
2
+ export const log = {
3
+ debug(msg: string, ctx?: Record<string, unknown>): void {
4
+ if (process.env.KODUS_GRAPH_DEBUG) {
5
+ process.stderr.write(`[DEBUG] ${msg}${ctx ? ` ${JSON.stringify(ctx)}` : ''}\n`);
6
+ }
7
+ },
8
+ warn(msg: string, ctx?: Record<string, unknown>): void {
9
+ process.stderr.write(`[WARN] ${msg}${ctx ? ` ${JSON.stringify(ctx)}` : ''}\n`);
10
+ },
11
+ error(msg: string, ctx?: Record<string, unknown>): void {
12
+ process.stderr.write(`[ERROR] ${msg}${ctx ? ` ${JSON.stringify(ctx)}` : ''}\n`);
13
+ },
14
+ };
@@ -0,0 +1,5 @@
1
+ export function qualifiedName(filePath: string, name: string, className?: string, isTest?: boolean): string {
2
+ if (isTest) return `${filePath}::test:${name}`;
3
+ if (className) return `${filePath}::${className}.${name}`;
4
+ return `${filePath}::${name}`;
5
+ }
@@ -0,0 +1,31 @@
1
+ import { realpathSync } from 'fs';
2
+ import { relative, resolve } from 'path';
3
+
4
+ /**
5
+ * Validate that a resolved path is within the repository root.
6
+ * Returns the validated absolute path.
7
+ * Throws if the path escapes the root.
8
+ */
9
+ export function ensureWithinRoot(filePath: string, repoRoot: string): string {
10
+ let absRoot: string;
11
+ try {
12
+ absRoot = realpathSync(resolve(repoRoot));
13
+ } catch {
14
+ absRoot = resolve(repoRoot);
15
+ }
16
+
17
+ let absPath: string;
18
+ try {
19
+ absPath = realpathSync(resolve(absRoot, filePath));
20
+ } catch {
21
+ // File doesn't exist yet or is unreadable — use resolve without symlink follow
22
+ absPath = resolve(absRoot, filePath);
23
+ }
24
+
25
+ const rel = relative(absRoot, absPath);
26
+ if (rel.startsWith('..') || resolve(absRoot, rel) !== absPath) {
27
+ throw new Error(`Path escapes repository root: ${filePath}`);
28
+ }
29
+
30
+ return absPath;
31
+ }
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+
3
+ const GraphNodeSchema = z.object({
4
+ kind: z.enum(['Function', 'Method', 'Constructor', 'Class', 'Interface', 'Enum', 'Test']),
5
+ name: z.string(),
6
+ qualified_name: z.string(),
7
+ file_path: z.string(),
8
+ line_start: z.number(),
9
+ line_end: z.number(),
10
+ language: z.string(),
11
+ is_test: z.boolean(),
12
+ file_hash: z.string(),
13
+ parent_name: z.string().optional(),
14
+ params: z.string().optional(),
15
+ return_type: z.string().optional(),
16
+ modifiers: z.string().optional(),
17
+ });
18
+
19
+ const GraphEdgeSchema = z.object({
20
+ kind: z.enum(['CALLS', 'IMPORTS', 'INHERITS', 'IMPLEMENTS', 'TESTED_BY', 'CONTAINS']),
21
+ source_qualified: z.string(),
22
+ target_qualified: z.string(),
23
+ file_path: z.string(),
24
+ line: z.number(),
25
+ confidence: z.number().optional(),
26
+ });
27
+
28
+ export const GraphInputSchema = z.object({
29
+ nodes: z.array(GraphNodeSchema),
30
+ edges: z.array(GraphEdgeSchema),
31
+ });
@@ -0,0 +1,17 @@
1
+ import { randomBytes } from 'crypto';
2
+ import { mkdtempSync } from 'fs';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+
6
+ /**
7
+ * Create a secure temp directory + file path.
8
+ * Directory is created with 0700 permissions via mkdtempSync.
9
+ * File name uses crypto.randomBytes for unpredictability.
10
+ *
11
+ * Caller is responsible for cleanup (rmSync(dir, { recursive: true, force: true })).
12
+ */
13
+ export function createSecureTempFile(prefix: string): { dir: string; filePath: string } {
14
+ const dir = mkdtempSync(join(tmpdir(), `kodus-graph-${prefix}-`));
15
+ const filePath = join(dir, `${randomBytes(8).toString('hex')}.json`);
16
+ return { dir, filePath };
17
+ }