@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.
- package/dist/analysis/enrich.js +43 -15
- package/dist/analysis/prompt-formatter.js +20 -3
- package/package.json +1 -1
package/dist/analysis/enrich.js
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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(` ${
|
|
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