@kodus/kodus-graph 0.2.1 → 0.2.3
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 +1 -1
- package/src/commands/context.ts +54 -1
- package/src/graph/builder.ts +37 -3
package/package.json
CHANGED
package/src/commands/context.ts
CHANGED
|
@@ -50,7 +50,24 @@ export async function executeContext(opts: ContextOptions): Promise<void> {
|
|
|
50
50
|
process.stderr.write(`Error: Invalid graph JSON: ${validated.error.message}\n`);
|
|
51
51
|
process.exit(1);
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
const changedSet = new Set(opts.files);
|
|
54
|
+
const sameBranch = detectSameBranch(validated.data.nodes, parseResult.nodes, changedSet);
|
|
55
|
+
|
|
56
|
+
if (sameBranch) {
|
|
57
|
+
// --graph was built from the same commit (e.g. kodus-ai's parse --all on PR branch).
|
|
58
|
+
// Exclude changed files from oldGraph so diff detects their functions as "added"
|
|
59
|
+
// instead of falsely marking everything "unchanged".
|
|
60
|
+
oldGraph = {
|
|
61
|
+
nodes: validated.data.nodes.filter((n: { file_path: string }) => !changedSet.has(n.file_path)),
|
|
62
|
+
edges: validated.data.edges.filter((e: { file_path: string }) => !changedSet.has(e.file_path)),
|
|
63
|
+
};
|
|
64
|
+
log.debug('Same-branch detected: excluding changed files from baseline', {
|
|
65
|
+
changedFiles: opts.files.length,
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
oldGraph = { nodes: validated.data.nodes, edges: validated.data.edges };
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
const mainGraph: MainGraphInput = {
|
|
55
72
|
repo_id: '',
|
|
56
73
|
sha: '',
|
|
@@ -84,3 +101,39 @@ export async function executeContext(opts: ContextOptions): Promise<void> {
|
|
|
84
101
|
}
|
|
85
102
|
}
|
|
86
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Detect if --graph was built from the same commit as the current repo.
|
|
107
|
+
* Compares file_hash values for changed files between the graph and the fresh parse.
|
|
108
|
+
* When hashes match, the graph can't serve as a baseline for diff — it IS the new state.
|
|
109
|
+
*/
|
|
110
|
+
function detectSameBranch(
|
|
111
|
+
graphNodes: Array<{ file_path: string; file_hash: string }>,
|
|
112
|
+
parseNodes: Array<{ file_path: string; file_hash: string }>,
|
|
113
|
+
changedFiles: Set<string>,
|
|
114
|
+
): boolean {
|
|
115
|
+
const graphHashes = new Map<string, string>();
|
|
116
|
+
for (const n of graphNodes) {
|
|
117
|
+
if (changedFiles.has(n.file_path) && n.file_hash && !graphHashes.has(n.file_path)) {
|
|
118
|
+
graphHashes.set(n.file_path, n.file_hash);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// No overlap means graph has no nodes for changed files — not same-branch scenario
|
|
123
|
+
if (graphHashes.size === 0) return false;
|
|
124
|
+
|
|
125
|
+
const parseHashes = new Map<string, string>();
|
|
126
|
+
for (const n of parseNodes) {
|
|
127
|
+
if (n.file_hash && !parseHashes.has(n.file_path)) {
|
|
128
|
+
parseHashes.set(n.file_path, n.file_hash);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// If any overlapping file has different hash → different branch
|
|
133
|
+
for (const [file, hash] of graphHashes) {
|
|
134
|
+
const parseHash = parseHashes.get(file);
|
|
135
|
+
if (parseHash && parseHash !== hash) return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return true;
|
|
139
|
+
}
|
package/src/graph/builder.ts
CHANGED
|
@@ -89,13 +89,47 @@ export function buildGraphData(
|
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
//
|
|
92
|
+
// Build file→functions index to resolve caller from line number
|
|
93
|
+
const functionsByFile = new Map<string, Array<{ qualified_name: string; line_start: number; line_end: number }>>();
|
|
94
|
+
for (const node of nodes) {
|
|
95
|
+
if (node.kind === 'Class' || node.kind === 'Interface' || node.kind === 'Enum') continue;
|
|
96
|
+
const entry = { qualified_name: node.qualified_name, line_start: node.line_start, line_end: node.line_end };
|
|
97
|
+
const list = functionsByFile.get(node.file_path);
|
|
98
|
+
if (list) list.push(entry);
|
|
99
|
+
else functionsByFile.set(node.file_path, [entry]);
|
|
100
|
+
}
|
|
101
|
+
// Sort descending by line_start so inner/nested functions match first
|
|
102
|
+
for (const list of functionsByFile.values()) {
|
|
103
|
+
list.sort((a, b) => b.line_start - a.line_start);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// CALLS edges — resolve caller function from call line number
|
|
93
107
|
for (const ce of callEdges) {
|
|
108
|
+
const sourceFile = ce.source.includes('::') ? ce.source.split('::')[0] : ce.source;
|
|
109
|
+
let sourceQualified: string;
|
|
110
|
+
|
|
111
|
+
if (ce.source.includes('::')) {
|
|
112
|
+
sourceQualified = ce.source;
|
|
113
|
+
} else {
|
|
114
|
+
// Find the innermost function containing this call line
|
|
115
|
+
const fns = functionsByFile.get(ce.source);
|
|
116
|
+
let resolved: string | undefined;
|
|
117
|
+
if (fns) {
|
|
118
|
+
for (const fn of fns) {
|
|
119
|
+
if (ce.line >= fn.line_start && ce.line <= fn.line_end) {
|
|
120
|
+
resolved = fn.qualified_name;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
sourceQualified = resolved || `${ce.source}::unknown`;
|
|
126
|
+
}
|
|
127
|
+
|
|
94
128
|
edges.push({
|
|
95
129
|
kind: 'CALLS',
|
|
96
|
-
source_qualified:
|
|
130
|
+
source_qualified: sourceQualified,
|
|
97
131
|
target_qualified: ce.target,
|
|
98
|
-
file_path:
|
|
132
|
+
file_path: sourceFile,
|
|
99
133
|
line: ce.line,
|
|
100
134
|
confidence: ce.confidence,
|
|
101
135
|
});
|