@kodus/kodus-graph 0.2.11 → 0.2.12

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.
@@ -37,27 +37,55 @@ export function enrichChangedFunctions(graph, changedFiles, diff, allFlows, minC
37
37
  }
38
38
  return true;
39
39
  });
40
+ // Pre-index INHERITS edges: child → parent qualified name
41
+ const childToParent = new Map();
42
+ for (const edge of graph.edges) {
43
+ if (edge.kind === 'INHERITS') {
44
+ childToParent.set(edge.source_qualified, edge.target_qualified);
45
+ }
46
+ }
40
47
  return changedFunctions
41
48
  .sort((a, b) => a.file_path.localeCompare(b.file_path) || a.line_start - b.line_start)
42
49
  .map((node) => {
43
- // Callers
50
+ // Callers — include both direct callers AND callers of the overridden parent method.
51
+ // When code calls `base.method()`, the edge points to the parent class method,
52
+ // so overrides show 0 callers without this inheritance resolution.
44
53
  const callers = [];
45
- for (const edge of graph.reverseAdjacency.get(node.qualified_name) || []) {
46
- if (edge.kind !== 'CALLS') {
47
- continue;
54
+ const seenCallers = new Set();
55
+ const collectCallers = (targetQN) => {
56
+ for (const edge of graph.reverseAdjacency.get(targetQN) || []) {
57
+ if (edge.kind !== 'CALLS') {
58
+ continue;
59
+ }
60
+ if (seenCallers.has(edge.source_qualified)) {
61
+ continue;
62
+ }
63
+ if ((edge.confidence ?? 1.0) < minConfidence) {
64
+ continue;
65
+ }
66
+ seenCallers.add(edge.source_qualified);
67
+ const sourceNode = graph.byQualified.get(edge.source_qualified);
68
+ callers.push({
69
+ qualified_name: edge.source_qualified,
70
+ name: sourceNode?.name || edge.source_qualified.split('::').pop() || 'unknown',
71
+ file_path: sourceNode?.file_path || edge.file_path,
72
+ line: edge.line,
73
+ confidence: edge.confidence ?? 1.0,
74
+ });
48
75
  }
49
- // null/undefined confidence = high confidence (edge came from DB or parser without scoring)
50
- if ((edge.confidence ?? 1.0) < minConfidence) {
51
- continue;
76
+ };
77
+ // Direct callers
78
+ collectCallers(node.qualified_name);
79
+ // Inherited callers: if this is a method override, also collect callers of the parent method.
80
+ // e.g. OptimizedCursorPaginator::get_result inherits callers of BasePaginator::get_result
81
+ const qnParts = node.qualified_name.split('::');
82
+ if (qnParts.length >= 3) {
83
+ const methodName = qnParts[qnParts.length - 1];
84
+ const className = qnParts.slice(0, -1).join('::');
85
+ const parentClass = childToParent.get(className);
86
+ if (parentClass) {
87
+ collectCallers(`${parentClass}::${methodName}`);
52
88
  }
53
- const sourceNode = graph.byQualified.get(edge.source_qualified);
54
- callers.push({
55
- qualified_name: edge.source_qualified,
56
- name: sourceNode?.name || edge.source_qualified.split('::').pop() || 'unknown',
57
- file_path: sourceNode?.file_path || edge.file_path,
58
- line: edge.line,
59
- confidence: edge.confidence ?? 1.0,
60
- });
61
89
  }
62
90
  // Callees
63
91
  const callees = [];
@@ -15,8 +15,9 @@ export function formatPrompt(output) {
15
15
  const risk = analysis.risk;
16
16
  const br = analysis.blast_radius;
17
17
  const meta = analysis.metadata;
18
- // ── Header: one-line stats ──
19
- lines.push(`${meta.changed_functions_count} changed | ${br.total_functions} impacted | ${br.total_files} files | risk ${risk.level} ${risk.score} | ${meta.untested_count} untested`);
18
+ // ── Header: one-line stats (untested scoped to changed functions only) ──
19
+ const changedUntested = analysis.changed_functions.filter((f) => !f.has_test_coverage).length;
20
+ lines.push(`${meta.changed_functions_count} changed (${changedUntested} untested) | ${br.total_functions} impacted | ${br.total_files} files | risk ${risk.level} ${risk.score}`);
20
21
  lines.push('');
21
22
  // ── Changed functions ──
22
23
  if (analysis.changed_functions.length > 0) {
@@ -26,8 +27,10 @@ export function formatPrompt(output) {
26
27
  for (const fn of analysis.changed_functions) {
27
28
  const status = fn.is_new ? 'new' : fn.diff_changes.length > 0 ? 'modified' : 'unchanged';
28
29
  const tested = fn.has_test_coverage ? 'tested' : 'untested';
30
+ // Class-qualified signature for methods (language-agnostic via qualified_name)
31
+ const displayName = classQualifiedSignature(fn.qualified_name, fn.signature);
29
32
  // Main line
30
- lines.push(` ${fn.signature} [${fn.file_path}:${fn.line_start}-${fn.line_end}] ${status} | ${fn.callers.length} callers | ${tested}`);
33
+ lines.push(` ${displayName} [${fn.file_path}:${fn.line_start}-${fn.line_end}] ${status} | ${fn.callers.length} callers | ${tested}`);
31
34
  // Contract changes — high value for agent to spot breaking changes
32
35
  for (const cd of fn.contract_diffs) {
33
36
  lines.push(` ⚠ ${cd.field}: ${cd.old_value} → ${cd.new_value}`);
@@ -136,6 +139,20 @@ export function formatPrompt(output) {
136
139
  function shortName(qualifiedName) {
137
140
  return qualifiedName.split('::').pop() || qualifiedName;
138
141
  }
142
+ /**
143
+ * Build class-qualified signature for methods.
144
+ * "file::Class::method" + "method(params) -> ret" → "Class.method(params) -> ret"
145
+ * For top-level functions ("file::func"), returns signature as-is.
146
+ * Language-agnostic: works for any language since qualified_name always uses "::" separator.
147
+ */
148
+ function classQualifiedSignature(qualifiedName, signature) {
149
+ const parts = qualifiedName.split('::');
150
+ if (parts.length < 3) {
151
+ return signature; // top-level function, no class
152
+ }
153
+ const className = parts[parts.length - 2];
154
+ return `${className}.${signature}`;
155
+ }
139
156
  /**
140
157
  * Build a map of changed functions → sibling implementations.
141
158
  * A "sibling" is a function with the same method name in a class that shares
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kodus/kodus-graph",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
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
  "main": "./dist/cli.js",