@kodus/kodus-graph 0.2.4 → 0.2.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kodus/kodus-graph",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
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
6
  "bin": {
@@ -74,10 +74,19 @@ export function computeStructuralDiff(
74
74
  } else {
75
75
  const newN = newNodesMap.get(qn)!;
76
76
  const changes: string[] = [];
77
- // Only flag line_range when the file content actually changed (different file_hash).
78
- // When file_hash matches, line shifts are just displacement from other edits — not real modifications.
79
- const hashChanged = !n.file_hash || !newN.file_hash || n.file_hash !== newN.file_hash;
80
- if (hashChanged && (n.line_start !== newN.line_start || n.line_end !== newN.line_end)) changes.push('line_range');
77
+ // Detect real changes vs. pure displacement using content_hash.
78
+ // content_hash is per-node (SHA256 of the function/class source text).
79
+ // If both have content_hash: definitive comparison same hash = identical content.
80
+ // If either is missing (legacy data): fallback to size heuristic.
81
+ if (n.line_start !== newN.line_start || n.line_end !== newN.line_end) {
82
+ if (n.content_hash && newN.content_hash) {
83
+ if (n.content_hash !== newN.content_hash) changes.push('line_range');
84
+ } else {
85
+ const oldSize = n.line_end - n.line_start;
86
+ const newSize = newN.line_end - newN.line_start;
87
+ if (oldSize !== newSize) changes.push('line_range');
88
+ }
89
+ }
81
90
  if ((n.params || '') !== (newN.params || '')) changes.push('params');
82
91
  if ((n.return_type || '') !== (newN.return_type || '')) changes.push('return_type');
83
92
  if (changes.length > 0) modified.push({ qualified_name: qn, changes });
@@ -26,6 +26,7 @@ export function buildGraphData(
26
26
  return_type: f.returnType || undefined,
27
27
  is_test: false,
28
28
  file_hash: fileHashes.get(f.file) || '',
29
+ content_hash: f.content_hash,
29
30
  });
30
31
  }
31
32
 
@@ -41,6 +42,7 @@ export function buildGraphData(
41
42
  language: detectLang(c.file),
42
43
  is_test: false,
43
44
  file_hash: fileHashes.get(c.file) || '',
45
+ content_hash: c.content_hash,
44
46
  });
45
47
  }
46
48
 
@@ -56,6 +58,7 @@ export function buildGraphData(
56
58
  language: detectLang(i.file),
57
59
  is_test: false,
58
60
  file_hash: fileHashes.get(i.file) || '',
61
+ content_hash: i.content_hash,
59
62
  });
60
63
  }
61
64
 
@@ -71,6 +74,7 @@ export function buildGraphData(
71
74
  language: detectLang(e.file),
72
75
  is_test: false,
73
76
  file_hash: fileHashes.get(e.file) || '',
77
+ content_hash: e.content_hash,
74
78
  });
75
79
  }
76
80
 
@@ -86,6 +90,7 @@ export function buildGraphData(
86
90
  language: detectLang(t.file),
87
91
  is_test: true,
88
92
  file_hash: fileHashes.get(t.file) || '',
93
+ content_hash: t.content_hash,
89
94
  });
90
95
  }
91
96
 
@@ -19,6 +19,7 @@ export interface GraphNode {
19
19
  modifiers?: string;
20
20
  is_test: boolean;
21
21
  file_hash: string;
22
+ content_hash?: string;
22
23
  }
23
24
 
24
25
  // ── Graph edge (matches ast_edges table) ──
@@ -185,6 +186,7 @@ export interface RawFunction {
185
186
  kind: 'Function' | 'Method' | 'Constructor';
186
187
  className: string;
187
188
  qualified: string;
189
+ content_hash?: string;
188
190
  }
189
191
 
190
192
  export interface RawClass {
@@ -195,6 +197,7 @@ export interface RawClass {
195
197
  extends: string;
196
198
  implements: string;
197
199
  qualified: string;
200
+ content_hash?: string;
198
201
  }
199
202
 
200
203
  export interface RawInterface {
@@ -204,6 +207,7 @@ export interface RawInterface {
204
207
  line_end: number;
205
208
  methods: string[];
206
209
  qualified: string;
210
+ content_hash?: string;
207
211
  }
208
212
 
209
213
  export interface RawEnum {
@@ -212,6 +216,7 @@ export interface RawEnum {
212
216
  line_start: number;
213
217
  line_end: number;
214
218
  qualified: string;
219
+ content_hash?: string;
215
220
  }
216
221
 
217
222
  export interface RawTest {
@@ -220,6 +225,7 @@ export interface RawTest {
220
225
  line_start: number;
221
226
  line_end: number;
222
227
  qualified: string;
228
+ content_hash?: string;
223
229
  }
224
230
 
225
231
  export interface RawImport {
@@ -1,5 +1,6 @@
1
1
  import type { SgNode, SgRoot } from '@ast-grep/napi';
2
2
  import type { RawCallSite, RawGraph } from '../../graph/types';
3
+ import { computeContentHash } from '../../shared/file-hash';
3
4
  import { NOISE } from '../../shared/filters';
4
5
  import { log } from '../../shared/logger';
5
6
  import { LANG_KINDS } from '../languages';
@@ -24,6 +25,7 @@ export function extractGeneric(root: SgRoot, fp: string, lang: string, seen: Set
24
25
  extends: '',
25
26
  implements: '',
26
27
  qualified: `${fp}::${name}`,
28
+ content_hash: computeContentHash(node.text()),
27
29
  });
28
30
  }
29
31
  } catch (err) {
@@ -58,6 +60,7 @@ export function extractGeneric(root: SgRoot, fp: string, lang: string, seen: Set
58
60
  kind: className ? 'Method' : 'Function',
59
61
  className,
60
62
  qualified: className ? `${fp}::${className}.${name}` : `${fp}::${name}`,
63
+ content_hash: computeContentHash(node.text()),
61
64
  });
62
65
  }
63
66
  } catch (err) {
@@ -1,5 +1,6 @@
1
1
  import type { SgNode, SgRoot } from '@ast-grep/napi';
2
2
  import type { RawCallSite, RawGraph } from '../../graph/types';
3
+ import { computeContentHash } from '../../shared/file-hash';
3
4
  import { NOISE } from '../../shared/filters';
4
5
  import { LANG_KINDS } from '../languages';
5
6
 
@@ -28,6 +29,7 @@ export function extractPython(root: SgRoot, fp: string, seen: Set<string>, graph
28
29
  extends: extendsName,
29
30
  implements: '',
30
31
  qualified: `${fp}::${name}`,
32
+ content_hash: computeContentHash(node.text()),
31
33
  });
32
34
  }
33
35
 
@@ -55,6 +57,7 @@ export function extractPython(root: SgRoot, fp: string, seen: Set<string>, graph
55
57
  line_start: line,
56
58
  line_end: node.range().end.line,
57
59
  qualified: `${fp}::test:${name}`,
60
+ content_hash: computeContentHash(node.text()),
58
61
  });
59
62
  }
60
63
 
@@ -68,6 +71,7 @@ export function extractPython(root: SgRoot, fp: string, seen: Set<string>, graph
68
71
  kind: name === '__init__' ? 'Constructor' : className ? 'Method' : 'Function',
69
72
  className,
70
73
  qualified: className ? `${fp}::${className}.${name}` : `${fp}::${name}`,
74
+ content_hash: computeContentHash(node.text()),
71
75
  });
72
76
  }
73
77
 
@@ -1,5 +1,6 @@
1
1
  import type { SgNode, SgRoot } from '@ast-grep/napi';
2
2
  import type { RawCallSite, RawGraph } from '../../graph/types';
3
+ import { computeContentHash } from '../../shared/file-hash';
3
4
  import { NOISE } from '../../shared/filters';
4
5
  import { log } from '../../shared/logger';
5
6
  import { LANG_KINDS } from '../languages';
@@ -23,6 +24,7 @@ export function extractRuby(root: SgRoot, fp: string, seen: Set<string>, graph:
23
24
  extends: superclass,
24
25
  implements: '',
25
26
  qualified: `${fp}::${name}`,
27
+ content_hash: computeContentHash(node.text()),
26
28
  });
27
29
  }
28
30
 
@@ -39,6 +41,7 @@ export function extractRuby(root: SgRoot, fp: string, seen: Set<string>, graph:
39
41
  extends: '',
40
42
  implements: '',
41
43
  qualified: `${fp}::${name}`,
44
+ content_hash: computeContentHash(node.text()),
42
45
  });
43
46
  }
44
47
 
@@ -63,6 +66,7 @@ export function extractRuby(root: SgRoot, fp: string, seen: Set<string>, graph:
63
66
  kind: className ? 'Method' : 'Function',
64
67
  className,
65
68
  qualified: className ? `${fp}::${className}.${name}` : `${fp}::${name}`,
69
+ content_hash: computeContentHash(node.text()),
66
70
  });
67
71
  }
68
72
 
@@ -88,6 +92,7 @@ export function extractRuby(root: SgRoot, fp: string, seen: Set<string>, graph:
88
92
  line_start: m.range().start.line,
89
93
  line_end: m.range().end.line,
90
94
  qualified: `${fp}::test:${name}`,
95
+ content_hash: computeContentHash(m.text()),
91
96
  });
92
97
  }
93
98
  } catch (err) {
@@ -1,6 +1,7 @@
1
1
  import type { SgNode, SgRoot } from '@ast-grep/napi';
2
2
  import { Lang } from '@ast-grep/napi';
3
3
  import type { RawCallSite, RawGraph } from '../../graph/types';
4
+ import { computeContentHash } from '../../shared/file-hash';
4
5
  import { NOISE } from '../../shared/filters';
5
6
  import { LANG_KINDS } from '../languages';
6
7
 
@@ -52,6 +53,7 @@ export function extractTypeScript(
52
53
  extends: extendsName,
53
54
  implements: implementsName,
54
55
  qualified: `${fp}::${name}`,
56
+ content_hash: computeContentHash(node.text()),
55
57
  });
56
58
  }
57
59
  }
@@ -112,6 +114,7 @@ export function extractTypeScript(
112
114
  kind: 'Constructor',
113
115
  className,
114
116
  qualified: `${fp}::${className}.constructor`,
117
+ content_hash: computeContentHash(node.text()),
115
118
  });
116
119
  } else {
117
120
  graph.functions.push({
@@ -124,6 +127,7 @@ export function extractTypeScript(
124
127
  kind: className ? 'Method' : 'Function',
125
128
  className,
126
129
  qualified: className ? `${fp}::${className}.${name}` : `${fp}::${name}`,
130
+ content_hash: computeContentHash(node.text()),
127
131
  });
128
132
  }
129
133
  }
@@ -148,6 +152,7 @@ export function extractTypeScript(
148
152
  kind: 'Function',
149
153
  className: '',
150
154
  qualified: `${fp}::${name}`,
155
+ content_hash: computeContentHash(node.text()),
151
156
  });
152
157
  }
153
158
 
@@ -172,6 +177,7 @@ export function extractTypeScript(
172
177
  kind: 'Function',
173
178
  className: '',
174
179
  qualified: `${fp}::${name}`,
180
+ content_hash: computeContentHash(node.text()),
175
181
  });
176
182
  }
177
183
 
@@ -198,6 +204,7 @@ export function extractTypeScript(
198
204
  line_end: node.range().end.line,
199
205
  methods,
200
206
  qualified: `${fp}::${name}`,
207
+ content_hash: computeContentHash(node.text()),
201
208
  });
202
209
  }
203
210
 
@@ -213,6 +220,7 @@ export function extractTypeScript(
213
220
  line_start: node.range().start.line,
214
221
  line_end: node.range().end.line,
215
222
  qualified: `${fp}::${name}`,
223
+ content_hash: computeContentHash(node.text()),
216
224
  });
217
225
  }
218
226
 
@@ -288,6 +296,7 @@ export function extractTypeScript(
288
296
  line_start: m.range().start.line,
289
297
  line_end: m.range().end.line,
290
298
  qualified: `${fp}::test:${name}`,
299
+ content_hash: computeContentHash(m.text()),
291
300
  });
292
301
  }
293
302
  }
@@ -5,3 +5,8 @@ export function computeFileHash(filePath: string): string {
5
5
  const content = readFileSync(filePath);
6
6
  return createHash('sha256').update(content).digest('hex');
7
7
  }
8
+
9
+ /** Hash a node's source text (function body, class body, etc.) */
10
+ export function computeContentHash(sourceText: string): string {
11
+ return createHash('sha256').update(sourceText).digest('hex');
12
+ }
@@ -10,6 +10,7 @@ const GraphNodeSchema = z.object({
10
10
  language: z.string(),
11
11
  is_test: z.boolean(),
12
12
  file_hash: z.string(),
13
+ content_hash: z.string().optional(),
13
14
  parent_name: z.string().optional(),
14
15
  params: z.string().optional(),
15
16
  return_type: z.string().optional(),