gitnexus 1.6.3-rc.23 → 1.6.3-rc.25
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/cli/index.js +8 -0
- package/dist/cli/tool.d.ts +5 -0
- package/dist/cli/tool.js +42 -0
- package/dist/core/embeddings/ast-utils.js +22 -22
- package/dist/core/search/bm25-index.d.ts +1 -0
- package/dist/core/search/bm25-index.js +19 -9
- package/dist/mcp/local/local-backend.js +16 -6
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -111,6 +111,14 @@ program
|
|
|
111
111
|
.description('Execute raw Cypher query against the knowledge graph')
|
|
112
112
|
.option('-r, --repo <name>', 'Target repository')
|
|
113
113
|
.action(createLazyAction(() => import('./tool.js'), 'cypherCommand'));
|
|
114
|
+
program
|
|
115
|
+
.command('detect-changes')
|
|
116
|
+
.alias('detect_changes')
|
|
117
|
+
.description('Map git diff hunks to indexed symbols and affected execution flows')
|
|
118
|
+
.option('-s, --scope <scope>', 'What to analyze: unstaged, staged, all, or compare', 'unstaged')
|
|
119
|
+
.option('-b, --base-ref <ref>', 'Branch/commit for compare scope (e.g. main)')
|
|
120
|
+
.option('-r, --repo <name>', 'Target repository')
|
|
121
|
+
.action(createLazyAction(() => import('./tool.js'), 'detectChangesCommand'));
|
|
114
122
|
// ─── Eval Server (persistent daemon for SWE-bench) ─────────────────
|
|
115
123
|
program
|
|
116
124
|
.command('eval-server')
|
package/dist/cli/tool.d.ts
CHANGED
|
@@ -36,3 +36,8 @@ export declare function impactCommand(target: string, options?: {
|
|
|
36
36
|
export declare function cypherCommand(query: string, options?: {
|
|
37
37
|
repo?: string;
|
|
38
38
|
}): Promise<void>;
|
|
39
|
+
export declare function detectChangesCommand(options?: {
|
|
40
|
+
scope?: string;
|
|
41
|
+
baseRef?: string;
|
|
42
|
+
repo?: string;
|
|
43
|
+
}): Promise<void>;
|
package/dist/cli/tool.js
CHANGED
|
@@ -124,3 +124,45 @@ export async function cypherCommand(query, options) {
|
|
|
124
124
|
});
|
|
125
125
|
output(result);
|
|
126
126
|
}
|
|
127
|
+
function formatDetectChangesResult(result) {
|
|
128
|
+
if (result?.error)
|
|
129
|
+
return `Error: ${result.error}`;
|
|
130
|
+
const summary = result?.summary || {};
|
|
131
|
+
if ((summary.changed_count || 0) === 0) {
|
|
132
|
+
return 'No changes detected.';
|
|
133
|
+
}
|
|
134
|
+
const lines = [];
|
|
135
|
+
lines.push(`Changes: ${summary.changed_files || 0} files, ${summary.changed_count || 0} symbols`);
|
|
136
|
+
lines.push(`Affected processes: ${summary.affected_count || 0}`);
|
|
137
|
+
lines.push(`Risk level: ${summary.risk_level || 'unknown'}`);
|
|
138
|
+
lines.push('');
|
|
139
|
+
const changed = result?.changed_symbols || [];
|
|
140
|
+
if (changed.length > 0) {
|
|
141
|
+
lines.push('Changed symbols:');
|
|
142
|
+
for (const symbol of changed.slice(0, 15)) {
|
|
143
|
+
lines.push(` ${symbol.type} ${symbol.name} → ${symbol.filePath}`);
|
|
144
|
+
}
|
|
145
|
+
if (changed.length > 15) {
|
|
146
|
+
lines.push(` ... and ${changed.length - 15} more`);
|
|
147
|
+
}
|
|
148
|
+
lines.push('');
|
|
149
|
+
}
|
|
150
|
+
const affected = result?.affected_processes || [];
|
|
151
|
+
if (affected.length > 0) {
|
|
152
|
+
lines.push('Affected execution flows:');
|
|
153
|
+
for (const processInfo of affected.slice(0, 10)) {
|
|
154
|
+
const steps = (processInfo.changed_steps || []).map((s) => s.symbol).join(', ');
|
|
155
|
+
lines.push(` • ${processInfo.name} (${processInfo.step_count} steps) — changed: ${steps}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return lines.join('\n').trim();
|
|
159
|
+
}
|
|
160
|
+
export async function detectChangesCommand(options) {
|
|
161
|
+
const backend = await getBackend();
|
|
162
|
+
const result = await backend.callTool('detect_changes', {
|
|
163
|
+
scope: options?.scope || 'unstaged',
|
|
164
|
+
base_ref: options?.baseRef,
|
|
165
|
+
repo: options?.repo,
|
|
166
|
+
});
|
|
167
|
+
output(formatDetectChangesResult(result));
|
|
168
|
+
}
|
|
@@ -55,17 +55,17 @@ const FUNCTION_LIKE_TYPES = new Set([
|
|
|
55
55
|
* numbers don't apply.
|
|
56
56
|
*/
|
|
57
57
|
export const findFunctionNode = (root) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
// Iterative DFS — avoids stack overflow on deeply nested ASTs.
|
|
59
|
+
const stack = [root];
|
|
60
|
+
while (stack.length > 0) {
|
|
61
|
+
const node = stack.pop();
|
|
62
|
+
if (FUNCTION_LIKE_TYPES.has(node.type))
|
|
63
|
+
return node;
|
|
64
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
65
|
+
const child = node.namedChild(i);
|
|
66
|
+
if (child)
|
|
67
|
+
stack.push(child);
|
|
68
|
+
}
|
|
69
69
|
}
|
|
70
70
|
return null;
|
|
71
71
|
};
|
|
@@ -89,17 +89,17 @@ export const findDeclarationNode = (root) => {
|
|
|
89
89
|
'object_declaration', // Kotlin: object
|
|
90
90
|
'impl_item', // Rust: impl
|
|
91
91
|
]);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
// Iterative DFS — avoids stack overflow on deeply nested ASTs.
|
|
93
|
+
const stack = [root];
|
|
94
|
+
while (stack.length > 0) {
|
|
95
|
+
const node = stack.pop();
|
|
96
|
+
if (CLASS_LIKE_TYPES.has(node.type))
|
|
97
|
+
return node;
|
|
98
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
99
|
+
const child = node.namedChild(i);
|
|
100
|
+
if (child)
|
|
101
|
+
stack.push(child);
|
|
102
|
+
}
|
|
103
103
|
}
|
|
104
104
|
return null;
|
|
105
105
|
};
|
|
@@ -69,6 +69,7 @@ async function queryFTSViaExecutor(executor, tableName, indexName, query, limit)
|
|
|
69
69
|
return {
|
|
70
70
|
filePath: node.filePath || '',
|
|
71
71
|
score: typeof score === 'number' ? score : parseFloat(score) || 0,
|
|
72
|
+
nodeId: node.nodeId || node.id || '',
|
|
72
73
|
};
|
|
73
74
|
});
|
|
74
75
|
}
|
|
@@ -118,17 +119,13 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
|
|
|
118
119
|
methodResults = await queryFTS('Method', 'method_fts', query, limit, false).catch(() => []);
|
|
119
120
|
interfaceResults = await queryFTS('Interface', 'interface_fts', query, limit, false).catch(() => []);
|
|
120
121
|
}
|
|
121
|
-
//
|
|
122
|
-
const
|
|
122
|
+
// Collect all node scores per filePath to track which nodes actually matched
|
|
123
|
+
const fileNodeScores = new Map();
|
|
123
124
|
const addResults = (results) => {
|
|
124
125
|
for (const r of results) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
merged.set(r.filePath, { filePath: r.filePath, score: r.score });
|
|
131
|
-
}
|
|
126
|
+
if (!fileNodeScores.has(r.filePath))
|
|
127
|
+
fileNodeScores.set(r.filePath, []);
|
|
128
|
+
fileNodeScores.get(r.filePath).push({ score: r.score, nodeId: r.nodeId });
|
|
132
129
|
}
|
|
133
130
|
};
|
|
134
131
|
addResults(fileResults);
|
|
@@ -136,6 +133,18 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
|
|
|
136
133
|
addResults(classResults);
|
|
137
134
|
addResults(methodResults);
|
|
138
135
|
addResults(interfaceResults);
|
|
136
|
+
// Sum the top-3 highest-scoring nodes per file and collect their nodeIds.
|
|
137
|
+
// Summing all nodes naively inflates scores for files with many mediocre
|
|
138
|
+
// matches (e.g. test files) over files with a single highly-relevant symbol.
|
|
139
|
+
const merged = new Map();
|
|
140
|
+
for (const [filePath, entries] of fileNodeScores) {
|
|
141
|
+
const top3 = [...entries].sort((a, b) => b.score - a.score).slice(0, 3);
|
|
142
|
+
merged.set(filePath, {
|
|
143
|
+
filePath,
|
|
144
|
+
score: top3.reduce((acc, e) => acc + e.score, 0),
|
|
145
|
+
nodeIds: top3.map((e) => e.nodeId).filter((id) => id),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
139
148
|
// Sort by score descending and add rank
|
|
140
149
|
const sorted = Array.from(merged.values())
|
|
141
150
|
.sort((a, b) => b.score - a.score)
|
|
@@ -144,5 +153,6 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
|
|
|
144
153
|
filePath: r.filePath,
|
|
145
154
|
score: r.score,
|
|
146
155
|
rank: index + 1,
|
|
156
|
+
nodeIds: r.nodeIds,
|
|
147
157
|
}));
|
|
148
158
|
};
|
|
@@ -707,12 +707,22 @@ export class LocalBackend {
|
|
|
707
707
|
for (const bm25Result of bm25Results) {
|
|
708
708
|
const fullPath = bm25Result.filePath;
|
|
709
709
|
try {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
710
|
+
// Prefer direct nodeId lookup (exact FTS-matched nodes) over filePath fallback.
|
|
711
|
+
// Without this, LIMIT 3 on filePath returns arbitrary symbols rather than
|
|
712
|
+
// the nodes that actually scored highest in the BM25 index.
|
|
713
|
+
const nodeIds = bm25Result.nodeIds?.length ? bm25Result.nodeIds : null;
|
|
714
|
+
const symbols = nodeIds
|
|
715
|
+
? await executeParameterized(repo.id, `
|
|
716
|
+
MATCH (n)
|
|
717
|
+
WHERE n.id IN $nodeIds
|
|
718
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
|
|
719
|
+
`, { nodeIds })
|
|
720
|
+
: await executeParameterized(repo.id, `
|
|
721
|
+
MATCH (n)
|
|
722
|
+
WHERE n.filePath = $filePath
|
|
723
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
|
|
724
|
+
LIMIT 3
|
|
725
|
+
`, { filePath: fullPath });
|
|
716
726
|
if (symbols.length > 0) {
|
|
717
727
|
for (const sym of symbols) {
|
|
718
728
|
results.push({
|
package/package.json
CHANGED