gitnexus 1.6.8-rc.34 → 1.6.8-rc.35
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/help-i18n.js +9 -0
- package/dist/cli/i18n/en.d.ts +8 -0
- package/dist/cli/i18n/en.js +8 -0
- package/dist/cli/i18n/resources.d.ts +16 -0
- package/dist/cli/i18n/zh-CN.d.ts +8 -0
- package/dist/cli/i18n/zh-CN.js +8 -0
- package/dist/cli/index.js +12 -0
- package/dist/cli/tool.d.ts +10 -0
- package/dist/cli/tool.js +45 -0
- package/dist/mcp/local/local-backend.d.ts +2 -0
- package/dist/mcp/local/local-backend.js +204 -0
- package/dist/mcp/tools.js +40 -0
- package/package.json +1 -1
- package/skills/gitnexus-debugging.md +12 -0
- package/skills/gitnexus-guide.md +11 -0
package/dist/cli/help-i18n.js
CHANGED
|
@@ -28,6 +28,7 @@ const COMMAND_DESCRIPTION_KEYS = {
|
|
|
28
28
|
cypher: 'help.command.cypher.description',
|
|
29
29
|
'detect-changes': 'help.command.detectChanges.description',
|
|
30
30
|
check: 'help.command.check.description',
|
|
31
|
+
trace: 'help.command.trace.description',
|
|
31
32
|
'eval-server': 'help.command.evalServer.description',
|
|
32
33
|
group: 'help.command.group.description',
|
|
33
34
|
'group create': 'help.command.group.create.description',
|
|
@@ -127,6 +128,14 @@ const OPTION_DESCRIPTION_KEYS = {
|
|
|
127
128
|
'check|--json': 'help.option.json',
|
|
128
129
|
'check|-r, --repo <name>': 'help.option.repo.target',
|
|
129
130
|
'check|--branch <name>': 'help.option.branch',
|
|
131
|
+
'trace|--from-uid <uid>': 'help.option.trace.fromUid',
|
|
132
|
+
'trace|--from-file <path>': 'help.option.trace.fromFile',
|
|
133
|
+
'trace|--to-uid <uid>': 'help.option.trace.toUid',
|
|
134
|
+
'trace|--to-file <path>': 'help.option.trace.toFile',
|
|
135
|
+
'trace|--depth <n>': 'help.option.trace.depth',
|
|
136
|
+
'trace|--include-tests': 'help.option.trace.includeTests',
|
|
137
|
+
'trace|-r, --repo <name>': 'help.option.repo.target',
|
|
138
|
+
'trace|--branch <name>': 'help.option.branch',
|
|
130
139
|
'eval-server|-p, --port <port>': 'help.option.port',
|
|
131
140
|
'eval-server|--host <host>': 'help.option.evalServer.host',
|
|
132
141
|
'eval-server|--idle-timeout <seconds>': 'help.option.evalServer.idleTimeout',
|
package/dist/cli/i18n/en.d.ts
CHANGED
|
@@ -51,6 +51,7 @@ export declare const en: {
|
|
|
51
51
|
readonly 'tool.usage.query': "Usage: gitnexus query <search_query>";
|
|
52
52
|
readonly 'tool.usage.context': "Usage: gitnexus context <symbol_name> [--uid <uid>] [--file <path>]";
|
|
53
53
|
readonly 'tool.usage.impact': "Usage: gitnexus impact <symbol_name> [--uid <uid>] [--file <path>] [--kind <kind>] [--direction upstream|downstream]";
|
|
54
|
+
readonly 'tool.usage.trace': "Usage: gitnexus trace <from> <to> [--from-uid <uid>] [--to-uid <uid>] [--depth <n>]";
|
|
54
55
|
readonly 'tool.usage.cypher': "Usage: gitnexus cypher <cypher_query>";
|
|
55
56
|
readonly 'tool.warn.unknownKind': "--kind '{{kind}}' is not a known symbol kind (e.g. Function, Class, Method); it will not narrow the result.";
|
|
56
57
|
readonly 'tool.detectChanges.noChanges': "No changes detected.";
|
|
@@ -124,6 +125,7 @@ export declare const en: {
|
|
|
124
125
|
readonly 'help.command.query.description': "Search the knowledge graph for execution flows related to a concept";
|
|
125
126
|
readonly 'help.command.context.description': "360-degree view of a code symbol: callers, callees, processes";
|
|
126
127
|
readonly 'help.command.impact.description': "Blast radius analysis: what breaks if you change a symbol";
|
|
128
|
+
readonly 'help.command.trace.description': "Find the shortest directed path between two symbols (call + class-member edges)";
|
|
127
129
|
readonly 'help.command.cypher.description': "Execute raw Cypher query against the knowledge graph";
|
|
128
130
|
readonly 'help.command.detectChanges.description': "Map git diff hunks to indexed symbols and affected execution flows";
|
|
129
131
|
readonly 'help.command.check.description': "Run structural checks against the indexed graph";
|
|
@@ -203,6 +205,12 @@ export declare const en: {
|
|
|
203
205
|
readonly 'help.option.impact.limit': "Max symbols per depth level (default: 100)";
|
|
204
206
|
readonly 'help.option.impact.offset': "Skip N symbols per depth level for pagination";
|
|
205
207
|
readonly 'help.option.impact.summaryOnly': "Return counts and risk only, omit symbol list";
|
|
208
|
+
readonly 'help.option.trace.fromUid': "Source symbol UID (zero-ambiguity lookup)";
|
|
209
|
+
readonly 'help.option.trace.fromFile': "Source file path to disambiguate common names";
|
|
210
|
+
readonly 'help.option.trace.toUid': "Target symbol UID (zero-ambiguity lookup)";
|
|
211
|
+
readonly 'help.option.trace.toFile': "Target file path to disambiguate common names";
|
|
212
|
+
readonly 'help.option.trace.depth': "Max path length in hops (default: 10)";
|
|
213
|
+
readonly 'help.option.trace.includeTests': "Traverse through test-file symbols (default: false)";
|
|
206
214
|
readonly 'help.option.detectChanges.scope': "What to analyze: unstaged, staged, all, or compare";
|
|
207
215
|
readonly 'help.option.detectChanges.baseRef': "Branch/commit for compare scope (e.g. main)";
|
|
208
216
|
readonly 'help.option.check.cycles': "Detect circular imports and fail when any are found";
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -51,6 +51,7 @@ export const en = {
|
|
|
51
51
|
'tool.usage.query': 'Usage: gitnexus query <search_query>',
|
|
52
52
|
'tool.usage.context': 'Usage: gitnexus context <symbol_name> [--uid <uid>] [--file <path>]',
|
|
53
53
|
'tool.usage.impact': 'Usage: gitnexus impact <symbol_name> [--uid <uid>] [--file <path>] [--kind <kind>] [--direction upstream|downstream]',
|
|
54
|
+
'tool.usage.trace': 'Usage: gitnexus trace <from> <to> [--from-uid <uid>] [--to-uid <uid>] [--depth <n>]',
|
|
54
55
|
'tool.usage.cypher': 'Usage: gitnexus cypher <cypher_query>',
|
|
55
56
|
'tool.warn.unknownKind': "--kind '{{kind}}' is not a known symbol kind (e.g. Function, Class, Method); it will not narrow the result.",
|
|
56
57
|
'tool.detectChanges.noChanges': 'No changes detected.',
|
|
@@ -124,6 +125,7 @@ export const en = {
|
|
|
124
125
|
'help.command.query.description': 'Search the knowledge graph for execution flows related to a concept',
|
|
125
126
|
'help.command.context.description': '360-degree view of a code symbol: callers, callees, processes',
|
|
126
127
|
'help.command.impact.description': 'Blast radius analysis: what breaks if you change a symbol',
|
|
128
|
+
'help.command.trace.description': 'Find the shortest directed path between two symbols (call + class-member edges)',
|
|
127
129
|
'help.command.cypher.description': 'Execute raw Cypher query against the knowledge graph',
|
|
128
130
|
'help.command.detectChanges.description': 'Map git diff hunks to indexed symbols and affected execution flows',
|
|
129
131
|
'help.command.check.description': 'Run structural checks against the indexed graph',
|
|
@@ -203,6 +205,12 @@ export const en = {
|
|
|
203
205
|
'help.option.impact.limit': 'Max symbols per depth level (default: 100)',
|
|
204
206
|
'help.option.impact.offset': 'Skip N symbols per depth level for pagination',
|
|
205
207
|
'help.option.impact.summaryOnly': 'Return counts and risk only, omit symbol list',
|
|
208
|
+
'help.option.trace.fromUid': 'Source symbol UID (zero-ambiguity lookup)',
|
|
209
|
+
'help.option.trace.fromFile': 'Source file path to disambiguate common names',
|
|
210
|
+
'help.option.trace.toUid': 'Target symbol UID (zero-ambiguity lookup)',
|
|
211
|
+
'help.option.trace.toFile': 'Target file path to disambiguate common names',
|
|
212
|
+
'help.option.trace.depth': 'Max path length in hops (default: 10)',
|
|
213
|
+
'help.option.trace.includeTests': 'Traverse through test-file symbols (default: false)',
|
|
206
214
|
'help.option.detectChanges.scope': 'What to analyze: unstaged, staged, all, or compare',
|
|
207
215
|
'help.option.detectChanges.baseRef': 'Branch/commit for compare scope (e.g. main)',
|
|
208
216
|
'help.option.check.cycles': 'Detect circular imports and fail when any are found',
|
|
@@ -52,6 +52,7 @@ export declare const cliResources: {
|
|
|
52
52
|
readonly 'tool.usage.query': "Usage: gitnexus query <search_query>";
|
|
53
53
|
readonly 'tool.usage.context': "Usage: gitnexus context <symbol_name> [--uid <uid>] [--file <path>]";
|
|
54
54
|
readonly 'tool.usage.impact': "Usage: gitnexus impact <symbol_name> [--uid <uid>] [--file <path>] [--kind <kind>] [--direction upstream|downstream]";
|
|
55
|
+
readonly 'tool.usage.trace': "Usage: gitnexus trace <from> <to> [--from-uid <uid>] [--to-uid <uid>] [--depth <n>]";
|
|
55
56
|
readonly 'tool.usage.cypher': "Usage: gitnexus cypher <cypher_query>";
|
|
56
57
|
readonly 'tool.warn.unknownKind': "--kind '{{kind}}' is not a known symbol kind (e.g. Function, Class, Method); it will not narrow the result.";
|
|
57
58
|
readonly 'tool.detectChanges.noChanges': "No changes detected.";
|
|
@@ -125,6 +126,7 @@ export declare const cliResources: {
|
|
|
125
126
|
readonly 'help.command.query.description': "Search the knowledge graph for execution flows related to a concept";
|
|
126
127
|
readonly 'help.command.context.description': "360-degree view of a code symbol: callers, callees, processes";
|
|
127
128
|
readonly 'help.command.impact.description': "Blast radius analysis: what breaks if you change a symbol";
|
|
129
|
+
readonly 'help.command.trace.description': "Find the shortest directed path between two symbols (call + class-member edges)";
|
|
128
130
|
readonly 'help.command.cypher.description': "Execute raw Cypher query against the knowledge graph";
|
|
129
131
|
readonly 'help.command.detectChanges.description': "Map git diff hunks to indexed symbols and affected execution flows";
|
|
130
132
|
readonly 'help.command.check.description': "Run structural checks against the indexed graph";
|
|
@@ -204,6 +206,12 @@ export declare const cliResources: {
|
|
|
204
206
|
readonly 'help.option.impact.limit': "Max symbols per depth level (default: 100)";
|
|
205
207
|
readonly 'help.option.impact.offset': "Skip N symbols per depth level for pagination";
|
|
206
208
|
readonly 'help.option.impact.summaryOnly': "Return counts and risk only, omit symbol list";
|
|
209
|
+
readonly 'help.option.trace.fromUid': "Source symbol UID (zero-ambiguity lookup)";
|
|
210
|
+
readonly 'help.option.trace.fromFile': "Source file path to disambiguate common names";
|
|
211
|
+
readonly 'help.option.trace.toUid': "Target symbol UID (zero-ambiguity lookup)";
|
|
212
|
+
readonly 'help.option.trace.toFile': "Target file path to disambiguate common names";
|
|
213
|
+
readonly 'help.option.trace.depth': "Max path length in hops (default: 10)";
|
|
214
|
+
readonly 'help.option.trace.includeTests': "Traverse through test-file symbols (default: false)";
|
|
207
215
|
readonly 'help.option.detectChanges.scope': "What to analyze: unstaged, staged, all, or compare";
|
|
208
216
|
readonly 'help.option.detectChanges.baseRef': "Branch/commit for compare scope (e.g. main)";
|
|
209
217
|
readonly 'help.option.check.cycles': "Detect circular imports and fail when any are found";
|
|
@@ -282,6 +290,7 @@ export declare const cliResources: {
|
|
|
282
290
|
'tool.usage.query': string;
|
|
283
291
|
'tool.usage.context': string;
|
|
284
292
|
'tool.usage.impact': string;
|
|
293
|
+
'tool.usage.trace': string;
|
|
285
294
|
'tool.usage.cypher': string;
|
|
286
295
|
'tool.warn.unknownKind': string;
|
|
287
296
|
'tool.detectChanges.noChanges': string;
|
|
@@ -355,6 +364,7 @@ export declare const cliResources: {
|
|
|
355
364
|
'help.command.query.description': string;
|
|
356
365
|
'help.command.context.description': string;
|
|
357
366
|
'help.command.impact.description': string;
|
|
367
|
+
'help.command.trace.description': string;
|
|
358
368
|
'help.command.cypher.description': string;
|
|
359
369
|
'help.command.detectChanges.description': string;
|
|
360
370
|
'help.command.check.description': string;
|
|
@@ -434,6 +444,12 @@ export declare const cliResources: {
|
|
|
434
444
|
'help.option.impact.limit': string;
|
|
435
445
|
'help.option.impact.offset': string;
|
|
436
446
|
'help.option.impact.summaryOnly': string;
|
|
447
|
+
'help.option.trace.fromUid': string;
|
|
448
|
+
'help.option.trace.fromFile': string;
|
|
449
|
+
'help.option.trace.toUid': string;
|
|
450
|
+
'help.option.trace.toFile': string;
|
|
451
|
+
'help.option.trace.depth': string;
|
|
452
|
+
'help.option.trace.includeTests': string;
|
|
437
453
|
'help.option.detectChanges.scope': string;
|
|
438
454
|
'help.option.detectChanges.baseRef': string;
|
|
439
455
|
'help.option.check.cycles': string;
|
package/dist/cli/i18n/zh-CN.d.ts
CHANGED
|
@@ -51,6 +51,7 @@ export declare const zhCN: {
|
|
|
51
51
|
'tool.usage.query': string;
|
|
52
52
|
'tool.usage.context': string;
|
|
53
53
|
'tool.usage.impact': string;
|
|
54
|
+
'tool.usage.trace': string;
|
|
54
55
|
'tool.usage.cypher': string;
|
|
55
56
|
'tool.warn.unknownKind': string;
|
|
56
57
|
'tool.detectChanges.noChanges': string;
|
|
@@ -124,6 +125,7 @@ export declare const zhCN: {
|
|
|
124
125
|
'help.command.query.description': string;
|
|
125
126
|
'help.command.context.description': string;
|
|
126
127
|
'help.command.impact.description': string;
|
|
128
|
+
'help.command.trace.description': string;
|
|
127
129
|
'help.command.cypher.description': string;
|
|
128
130
|
'help.command.detectChanges.description': string;
|
|
129
131
|
'help.command.check.description': string;
|
|
@@ -203,6 +205,12 @@ export declare const zhCN: {
|
|
|
203
205
|
'help.option.impact.limit': string;
|
|
204
206
|
'help.option.impact.offset': string;
|
|
205
207
|
'help.option.impact.summaryOnly': string;
|
|
208
|
+
'help.option.trace.fromUid': string;
|
|
209
|
+
'help.option.trace.fromFile': string;
|
|
210
|
+
'help.option.trace.toUid': string;
|
|
211
|
+
'help.option.trace.toFile': string;
|
|
212
|
+
'help.option.trace.depth': string;
|
|
213
|
+
'help.option.trace.includeTests': string;
|
|
206
214
|
'help.option.detectChanges.scope': string;
|
|
207
215
|
'help.option.detectChanges.baseRef': string;
|
|
208
216
|
'help.option.check.cycles': string;
|
package/dist/cli/i18n/zh-CN.js
CHANGED
|
@@ -51,6 +51,7 @@ export const zhCN = {
|
|
|
51
51
|
'tool.usage.query': '用法:gitnexus query <搜索词>',
|
|
52
52
|
'tool.usage.context': '用法:gitnexus context <符号名> [--uid <uid>] [--file <路径>]',
|
|
53
53
|
'tool.usage.impact': '用法:gitnexus impact <符号名> [--uid <uid>] [--file <路径>] [--kind <类型>] [--direction upstream|downstream]',
|
|
54
|
+
'tool.usage.trace': '用法:gitnexus trace <起点> <终点> [--from-uid <uid>] [--to-uid <uid>] [--depth <n>]',
|
|
54
55
|
'tool.usage.cypher': '用法:gitnexus cypher <Cypher 查询>',
|
|
55
56
|
'tool.warn.unknownKind': "--kind '{{kind}}' 不是已知的符号类型(如 Function、Class、Method),不会用于缩小结果范围。",
|
|
56
57
|
'tool.detectChanges.noChanges': '未检测到变更。',
|
|
@@ -124,6 +125,7 @@ export const zhCN = {
|
|
|
124
125
|
'help.command.query.description': '搜索知识图谱中与概念相关的执行流程',
|
|
125
126
|
'help.command.context.description': '查看代码符号的 360 度视图:调用者、被调用者、流程',
|
|
126
127
|
'help.command.impact.description': '影响面分析:修改符号会影响什么',
|
|
128
|
+
'help.command.trace.description': '查找两个符号之间的最短有向路径(调用与类成员边)',
|
|
127
129
|
'help.command.cypher.description': '对知识图谱执行原始 Cypher 查询',
|
|
128
130
|
'help.command.detectChanges.description': '将 git diff hunk 映射到已索引符号和受影响执行流程',
|
|
129
131
|
'help.command.check.description': '对已索引图谱运行结构检查',
|
|
@@ -203,6 +205,12 @@ export const zhCN = {
|
|
|
203
205
|
'help.option.impact.limit': '每层深度最大符号数(默认:100)',
|
|
204
206
|
'help.option.impact.offset': '每层深度跳过 N 个符号(分页用)',
|
|
205
207
|
'help.option.impact.summaryOnly': '仅返回计数和风险等级,省略符号列表',
|
|
208
|
+
'help.option.trace.fromUid': '源符号 UID(零歧义查找)',
|
|
209
|
+
'help.option.trace.fromFile': '源文件路径,用于消除常见名称歧义',
|
|
210
|
+
'help.option.trace.toUid': '目标符号 UID(零歧义查找)',
|
|
211
|
+
'help.option.trace.toFile': '目标文件路径,用于消除常见名称歧义',
|
|
212
|
+
'help.option.trace.depth': '最大路径跳数(默认:10)',
|
|
213
|
+
'help.option.trace.includeTests': '遍历时包含测试文件中的符号(默认:false)',
|
|
206
214
|
'help.option.detectChanges.scope': '分析范围:unstaged、staged、all 或 compare',
|
|
207
215
|
'help.option.detectChanges.baseRef': 'compare 范围的分支/提交(例如 main)',
|
|
208
216
|
'help.option.check.cycles': '检测循环导入,并在发现循环时失败',
|
package/dist/cli/index.js
CHANGED
|
@@ -235,6 +235,18 @@ program
|
|
|
235
235
|
.option('--offset <n>', 'Skip N symbols per depth level for pagination')
|
|
236
236
|
.option('--summary-only', 'Return counts and risk only, omit symbol list')
|
|
237
237
|
.action(createLbugLazyAction(() => import('./tool.js'), 'impactCommand'));
|
|
238
|
+
program
|
|
239
|
+
.command('trace <from> <to>')
|
|
240
|
+
.description('Find the shortest directed path between two symbols (call + class-member edges)')
|
|
241
|
+
.option('--from-uid <uid>', 'Source symbol UID (zero-ambiguity)')
|
|
242
|
+
.option('--from-file <path>', 'Source file path hint')
|
|
243
|
+
.option('--to-uid <uid>', 'Target symbol UID (zero-ambiguity)')
|
|
244
|
+
.option('--to-file <path>', 'Target file path hint')
|
|
245
|
+
.option('--depth <n>', 'Max path length in hops (default: 10)')
|
|
246
|
+
.option('--include-tests', 'Include test files in results')
|
|
247
|
+
.option('-r, --repo <name>', 'Target repository')
|
|
248
|
+
.option('--branch <name>', 'Scope to a specific branch index')
|
|
249
|
+
.action(createLbugLazyAction(() => import('./tool.js'), 'traceCommand'));
|
|
238
250
|
program
|
|
239
251
|
.command('cypher <query>')
|
|
240
252
|
.description('Execute raw Cypher query against the knowledge graph')
|
package/dist/cli/tool.d.ts
CHANGED
|
@@ -58,3 +58,13 @@ export declare function checkCommand(options?: {
|
|
|
58
58
|
repo?: string;
|
|
59
59
|
branch?: string;
|
|
60
60
|
}): Promise<void>;
|
|
61
|
+
export declare function traceCommand(from?: string, to?: string, options?: {
|
|
62
|
+
fromUid?: string;
|
|
63
|
+
fromFile?: string;
|
|
64
|
+
toUid?: string;
|
|
65
|
+
toFile?: string;
|
|
66
|
+
depth?: string;
|
|
67
|
+
repo?: string;
|
|
68
|
+
branch?: string;
|
|
69
|
+
includeTests?: boolean;
|
|
70
|
+
}): Promise<void>;
|
package/dist/cli/tool.js
CHANGED
|
@@ -207,3 +207,48 @@ export async function checkCommand(options) {
|
|
|
207
207
|
process.exitCode = 1;
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
+
export async function traceCommand(from, to, options) {
|
|
211
|
+
if (options?.fromUid?.startsWith('--') || options?.toUid?.startsWith('--')) {
|
|
212
|
+
cliErrorKey('tool.usage.trace');
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
if ((!from?.trim() && !options?.fromUid) || (!to?.trim() && !options?.toUid)) {
|
|
216
|
+
cliErrorKey('tool.usage.trace');
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
// Reject a non-numeric / non-positive --depth up front rather than forwarding
|
|
220
|
+
// NaN (which the backend would silently treat as the default).
|
|
221
|
+
if (options?.depth !== undefined) {
|
|
222
|
+
const parsedDepth = Number(options.depth);
|
|
223
|
+
if (!Number.isInteger(parsedDepth) || parsedDepth < 1) {
|
|
224
|
+
cliErrorKey('tool.usage.trace');
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const backend = await getBackend();
|
|
230
|
+
const result = await backend.callTool('trace', {
|
|
231
|
+
from: from || undefined,
|
|
232
|
+
from_uid: options?.fromUid,
|
|
233
|
+
from_file: options?.fromFile,
|
|
234
|
+
to: to || undefined,
|
|
235
|
+
to_uid: options?.toUid,
|
|
236
|
+
to_file: options?.toFile,
|
|
237
|
+
maxDepth: options?.depth ? parseInt(options.depth, 10) : undefined,
|
|
238
|
+
includeTests: options?.includeTests ?? false,
|
|
239
|
+
repo: options?.repo,
|
|
240
|
+
branch: options?.branch,
|
|
241
|
+
});
|
|
242
|
+
output(result);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
output({
|
|
246
|
+
status: 'error',
|
|
247
|
+
error: (err instanceof Error ? err.message : String(err)) || 'Trace analysis failed unexpectedly',
|
|
248
|
+
from: { name: from },
|
|
249
|
+
to: { name: to },
|
|
250
|
+
suggestion: 'Try gitnexus context <symbol> to see connections, or check if an interface bridges them.',
|
|
251
|
+
});
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -1101,6 +1101,8 @@ export class LocalBackend {
|
|
|
1101
1101
|
return this.toolMap(repo, params);
|
|
1102
1102
|
case 'api_impact':
|
|
1103
1103
|
return this.apiImpact(repo, params);
|
|
1104
|
+
case 'trace':
|
|
1105
|
+
return this.trace(repo, params);
|
|
1104
1106
|
default:
|
|
1105
1107
|
throw new Error(`Unknown tool: ${method}`);
|
|
1106
1108
|
}
|
|
@@ -3261,6 +3263,208 @@ export class LocalBackend {
|
|
|
3261
3263
|
applied: !dry_run,
|
|
3262
3264
|
};
|
|
3263
3265
|
}
|
|
3266
|
+
async trace(repo, params) {
|
|
3267
|
+
try {
|
|
3268
|
+
return await this._traceImpl(repo, params);
|
|
3269
|
+
}
|
|
3270
|
+
catch (err) {
|
|
3271
|
+
return {
|
|
3272
|
+
status: 'error',
|
|
3273
|
+
error: (err instanceof Error ? err.message : String(err)) || 'Trace analysis failed',
|
|
3274
|
+
from: { name: params.from },
|
|
3275
|
+
to: { name: params.to },
|
|
3276
|
+
suggestion: 'The graph query failed — try gitnexus context <symbol> to see connections, ' +
|
|
3277
|
+
'or check if an interface bridges them.',
|
|
3278
|
+
...(isWalCorruptionError(err) ? { recoverySuggestion: WAL_RECOVERY_SUGGESTION } : {}),
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
async _traceImpl(repo, params) {
|
|
3283
|
+
await this.ensureInitialized(repo);
|
|
3284
|
+
// resolveSymbolCandidates feeds `from`/`to` into string operations
|
|
3285
|
+
// (e.g. name.includes), so a non-string param would surface a low-level
|
|
3286
|
+
// "x.includes is not a function". Reject it with a clear message instead.
|
|
3287
|
+
const isStringOrAbsent = (v) => v === undefined || typeof v === 'string';
|
|
3288
|
+
if (!isStringOrAbsent(params.from) ||
|
|
3289
|
+
!isStringOrAbsent(params.to) ||
|
|
3290
|
+
!isStringOrAbsent(params.from_uid) ||
|
|
3291
|
+
!isStringOrAbsent(params.to_uid)) {
|
|
3292
|
+
return {
|
|
3293
|
+
status: 'error',
|
|
3294
|
+
error: "'from', 'to', and their *_uid variants must be strings.",
|
|
3295
|
+
suggestion: 'Pass symbol names or UIDs as strings, e.g. trace from="A" to="B".',
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3298
|
+
const fromOutcome = await this.resolveSymbolCandidates(repo, { uid: params.from_uid, name: params.from }, { file_path: params.from_file });
|
|
3299
|
+
if (fromOutcome.kind === 'not_found') {
|
|
3300
|
+
return {
|
|
3301
|
+
status: 'not_found',
|
|
3302
|
+
error: `Source symbol '${params.from_uid ?? params.from}' not found.`,
|
|
3303
|
+
suggestion: 'Check the symbol name or use --from-uid for zero-ambiguity.',
|
|
3304
|
+
};
|
|
3305
|
+
}
|
|
3306
|
+
if (fromOutcome.kind === 'ambiguous') {
|
|
3307
|
+
return {
|
|
3308
|
+
status: 'ambiguous',
|
|
3309
|
+
role: 'from',
|
|
3310
|
+
message: `Found ${fromOutcome.candidates.length} symbols matching '${params.from}'. Disambiguate with --from-uid.`,
|
|
3311
|
+
candidates: fromOutcome.candidates,
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
3314
|
+
const toOutcome = await this.resolveSymbolCandidates(repo, { uid: params.to_uid, name: params.to }, { file_path: params.to_file });
|
|
3315
|
+
if (toOutcome.kind === 'not_found') {
|
|
3316
|
+
return {
|
|
3317
|
+
status: 'not_found',
|
|
3318
|
+
error: `Target symbol '${params.to_uid ?? params.to}' not found.`,
|
|
3319
|
+
suggestion: 'Check the symbol name or use --to-uid for zero-ambiguity.',
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
if (toOutcome.kind === 'ambiguous') {
|
|
3323
|
+
return {
|
|
3324
|
+
status: 'ambiguous',
|
|
3325
|
+
role: 'to',
|
|
3326
|
+
message: `Found ${toOutcome.candidates.length} symbols matching '${params.to}'. Disambiguate with --to-uid.`,
|
|
3327
|
+
candidates: toOutcome.candidates,
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
const fromSym = fromOutcome.symbol;
|
|
3331
|
+
const toSym = toOutcome.symbol;
|
|
3332
|
+
if (fromSym.id === toSym.id) {
|
|
3333
|
+
return {
|
|
3334
|
+
status: 'ok',
|
|
3335
|
+
from: { name: fromSym.name, filePath: fromSym.filePath, startLine: fromSym.startLine },
|
|
3336
|
+
to: { name: toSym.name, filePath: toSym.filePath, startLine: toSym.startLine },
|
|
3337
|
+
hopCount: 0,
|
|
3338
|
+
hops: [{ name: fromSym.name, filePath: fromSym.filePath, startLine: fromSym.startLine }],
|
|
3339
|
+
edges: [],
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
// Sanitize maxDepth at the real boundary: the MCP inputSchema's
|
|
3343
|
+
// minimum/maximum is advisory only (callTool is reachable directly), so a
|
|
3344
|
+
// caller can pass 0, a negative, NaN, or a non-integer. `??` does NOT
|
|
3345
|
+
// recover 0/NaN, and Math.min has no lower bound — left unguarded, any of
|
|
3346
|
+
// those makes the BFS loop run zero iterations and return a false no_path.
|
|
3347
|
+
const DEFAULT_TRACE_DEPTH = 10;
|
|
3348
|
+
const MAX_TRACE_DEPTH = 30;
|
|
3349
|
+
const requestedDepth = Number.isInteger(params.maxDepth) && params.maxDepth > 0
|
|
3350
|
+
? params.maxDepth
|
|
3351
|
+
: DEFAULT_TRACE_DEPTH;
|
|
3352
|
+
const maxDepth = Math.min(requestedDepth, MAX_TRACE_DEPTH);
|
|
3353
|
+
const includeTests = params.includeTests ?? false;
|
|
3354
|
+
// Traversal vocabulary: CALLS for actual calls, HAS_METHOD so a class-rooted
|
|
3355
|
+
// trace can descend into its methods. Not "calls only" — per-hop edge type is
|
|
3356
|
+
// surfaced in edges[] so containment hops stay distinguishable.
|
|
3357
|
+
const TRAVERSAL_EDGE_TYPES = ['CALLS', 'HAS_METHOD'];
|
|
3358
|
+
// Bound the traversal so a high-fanout hub (a logger/util reached by many
|
|
3359
|
+
// symbols) can't materialize an unbounded frontier. Per-level rows are
|
|
3360
|
+
// capped and the total visited set is capped; either cap sets `truncated`
|
|
3361
|
+
// so a resulting no_path is never reported as if the graph was exhausted.
|
|
3362
|
+
const PER_NODE_FANOUT_CAP = 200;
|
|
3363
|
+
const ABS_ROW_CAP = 5000;
|
|
3364
|
+
const MAX_VISITED = 50000;
|
|
3365
|
+
let truncated = false;
|
|
3366
|
+
const visited = new Set([fromSym.id]);
|
|
3367
|
+
let frontier = [fromSym.id];
|
|
3368
|
+
const parent = new Map();
|
|
3369
|
+
let found = false;
|
|
3370
|
+
// The last node discovered at the deepest reached level — surfaced as
|
|
3371
|
+
// `furthest` in the no_path response to hint where the chain breaks.
|
|
3372
|
+
let lastReached = null;
|
|
3373
|
+
let reachedDepth = 0;
|
|
3374
|
+
for (let depth = 1; depth <= maxDepth && frontier.length > 0 && !found; depth++) {
|
|
3375
|
+
const nextFrontier = [];
|
|
3376
|
+
// LadybugDB/Kuzu does not support a parameterized LIMIT, so the cap is
|
|
3377
|
+
// interpolated (it is a derived integer, not user input).
|
|
3378
|
+
const rowCap = Math.min(frontier.length * PER_NODE_FANOUT_CAP, ABS_ROW_CAP);
|
|
3379
|
+
const rows = await executeParameterized(repo.lbugPath, `MATCH (n)-[r:CodeRelation]->(m)
|
|
3380
|
+
WHERE n.id IN $frontierIds AND r.type IN $edgeTypes
|
|
3381
|
+
RETURN n.id AS sourceId, m.id AS id, m.name AS name, labels(m)[0] AS type,
|
|
3382
|
+
m.filePath AS filePath, m.startLine AS startLine,
|
|
3383
|
+
r.type AS edgeType, r.confidence AS confidence
|
|
3384
|
+
LIMIT ${rowCap}`, { frontierIds: frontier, edgeTypes: TRAVERSAL_EDGE_TYPES });
|
|
3385
|
+
// A clipped level may have dropped a node that lies on the only shortest
|
|
3386
|
+
// path, so any subsequent no_path is not authoritative.
|
|
3387
|
+
if (rows.length >= rowCap)
|
|
3388
|
+
truncated = true;
|
|
3389
|
+
for (const row of rows) {
|
|
3390
|
+
// Decode once. The `?? row[N]` fallback handles LadybugDB tuple-mode
|
|
3391
|
+
// returns; the positional indices mirror the RETURN column order above.
|
|
3392
|
+
const nodeId = (row.id ?? row[1]);
|
|
3393
|
+
const sourceId = (row.sourceId ?? row[0]);
|
|
3394
|
+
const name = (row.name ?? row[2]);
|
|
3395
|
+
const filePath = (row.filePath ?? row[4]);
|
|
3396
|
+
const startLine = (row.startLine ?? row[5]);
|
|
3397
|
+
const edgeType = (row.edgeType ?? row[6]);
|
|
3398
|
+
const storedConfidence = row.confidence ?? row[7];
|
|
3399
|
+
const confidence = typeof storedConfidence === 'number' && storedConfidence > 0
|
|
3400
|
+
? storedConfidence
|
|
3401
|
+
: confidenceForRelType(edgeType);
|
|
3402
|
+
// Match the explicitly-requested target before the test-file filter.
|
|
3403
|
+
// resolveSymbolCandidates does not exclude test-file symbols, so a
|
|
3404
|
+
// target (or a required hop) that lives in a test file would otherwise
|
|
3405
|
+
// be dropped by the includeTests guard below and produce a false
|
|
3406
|
+
// no_path even when a direct edge exists.
|
|
3407
|
+
if (nodeId === toSym.id) {
|
|
3408
|
+
parent.set(nodeId, { from: sourceId, name, filePath, startLine, edgeType, confidence });
|
|
3409
|
+
found = true;
|
|
3410
|
+
break;
|
|
3411
|
+
}
|
|
3412
|
+
// Skip non-target nodes that live in test files unless includeTests.
|
|
3413
|
+
if (!includeTests && isTestFilePath(filePath))
|
|
3414
|
+
continue;
|
|
3415
|
+
if (!visited.has(nodeId)) {
|
|
3416
|
+
visited.add(nodeId);
|
|
3417
|
+
parent.set(nodeId, { from: sourceId, name, filePath, startLine, edgeType, confidence });
|
|
3418
|
+
nextFrontier.push(nodeId);
|
|
3419
|
+
lastReached = { name, filePath, startLine };
|
|
3420
|
+
reachedDepth = depth;
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
frontier = nextFrontier;
|
|
3424
|
+
if (visited.size >= MAX_VISITED) {
|
|
3425
|
+
truncated = true;
|
|
3426
|
+
break;
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
if (found) {
|
|
3430
|
+
const path = [];
|
|
3431
|
+
const edges = [];
|
|
3432
|
+
let current = toSym.id;
|
|
3433
|
+
while (current !== fromSym.id) {
|
|
3434
|
+
const info = parent.get(current);
|
|
3435
|
+
path.unshift({ name: info.name, filePath: info.filePath, startLine: info.startLine });
|
|
3436
|
+
edges.unshift({ relType: info.edgeType, confidence: info.confidence });
|
|
3437
|
+
current = info.from;
|
|
3438
|
+
}
|
|
3439
|
+
path.unshift({
|
|
3440
|
+
name: fromSym.name,
|
|
3441
|
+
filePath: fromSym.filePath,
|
|
3442
|
+
startLine: fromSym.startLine,
|
|
3443
|
+
});
|
|
3444
|
+
return {
|
|
3445
|
+
status: 'ok',
|
|
3446
|
+
from: { name: fromSym.name, filePath: fromSym.filePath, startLine: fromSym.startLine },
|
|
3447
|
+
to: { name: toSym.name, filePath: toSym.filePath, startLine: toSym.startLine },
|
|
3448
|
+
hopCount: edges.length,
|
|
3449
|
+
hops: path,
|
|
3450
|
+
edges,
|
|
3451
|
+
};
|
|
3452
|
+
}
|
|
3453
|
+
return {
|
|
3454
|
+
status: 'no_path',
|
|
3455
|
+
from: { name: fromSym.name, filePath: fromSym.filePath, startLine: fromSym.startLine },
|
|
3456
|
+
to: { name: toSym.name, filePath: toSym.filePath, startLine: toSym.startLine },
|
|
3457
|
+
furthest: lastReached ? { ...lastReached, depth: reachedDepth } : null,
|
|
3458
|
+
...(truncated ? { truncated: true } : {}),
|
|
3459
|
+
suggestion: truncated
|
|
3460
|
+
? 'Search was truncated at a traversal cap before exhausting the graph — a path ' +
|
|
3461
|
+
'may still exist. Narrow the search (a lower --depth, or trace from a more ' +
|
|
3462
|
+
'specific symbol), or use gitnexus context <symbol> to inspect connections.'
|
|
3463
|
+
: 'No directed path found. The call chain likely breaks at dynamic dispatch, ' +
|
|
3464
|
+
'reflection, or an external API boundary. Try gitnexus context <symbol> to see ' +
|
|
3465
|
+
"both symbols' connections, or check if an interface/abstraction bridges them.",
|
|
3466
|
+
};
|
|
3467
|
+
}
|
|
3264
3468
|
async impact(repo, params) {
|
|
3265
3469
|
try {
|
|
3266
3470
|
return await this._impactImpl(repo, params);
|
package/dist/mcp/tools.js
CHANGED
|
@@ -703,6 +703,45 @@ WHEN TO USE: After changing group.yaml or re-indexing member repos.`,
|
|
|
703
703
|
required: ['name'],
|
|
704
704
|
},
|
|
705
705
|
},
|
|
706
|
+
{
|
|
707
|
+
name: 'trace',
|
|
708
|
+
description: `Find the shortest directed path between two symbols over call and class-member edges.
|
|
709
|
+
|
|
710
|
+
WHEN TO USE: Debugging "how does A reach B?" — answers in one call what would take 3-8 manual context/impact hops. Shows the exact chain with file:line positions plus a per-hop edge type and confidence.
|
|
711
|
+
|
|
712
|
+
Traverses CALLS edges plus HAS_METHOD (class → member) edges, so a trace can descend from a class into its methods. Each hop's edge type is reported in edges[], so call hops and containment hops remain distinguishable.
|
|
713
|
+
|
|
714
|
+
Returns: ordered hops with file:line, and an aligned edges[] of edge type + confidence. When no path exists, reports the furthest reachable node so you know where the chain breaks (and truncated: true if a traversal cap was hit first).`,
|
|
715
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
716
|
+
inputSchema: {
|
|
717
|
+
type: 'object',
|
|
718
|
+
properties: {
|
|
719
|
+
from: { type: 'string', description: 'Source symbol name' },
|
|
720
|
+
from_uid: { type: 'string', description: 'Source symbol UID (zero-ambiguity)' },
|
|
721
|
+
from_file: { type: 'string', description: 'Source file path hint for disambiguation' },
|
|
722
|
+
to: { type: 'string', description: 'Target symbol name' },
|
|
723
|
+
to_uid: { type: 'string', description: 'Target symbol UID (zero-ambiguity)' },
|
|
724
|
+
to_file: { type: 'string', description: 'Target file path hint for disambiguation' },
|
|
725
|
+
maxDepth: {
|
|
726
|
+
type: 'number',
|
|
727
|
+
description: 'Maximum path length in hops (default: 10)',
|
|
728
|
+
default: 10,
|
|
729
|
+
minimum: 1,
|
|
730
|
+
maximum: 30,
|
|
731
|
+
},
|
|
732
|
+
includeTests: {
|
|
733
|
+
type: 'boolean',
|
|
734
|
+
description: 'Include test-file symbols in traversal (default: false)',
|
|
735
|
+
default: false,
|
|
736
|
+
},
|
|
737
|
+
repo: {
|
|
738
|
+
type: 'string',
|
|
739
|
+
description: 'Repository name or path. Omit if only one repo is indexed.',
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
required: [],
|
|
743
|
+
},
|
|
744
|
+
},
|
|
706
745
|
];
|
|
707
746
|
/**
|
|
708
747
|
* Per-repo tools that accept an optional `branch` scope (#2106). Single source
|
|
@@ -725,6 +764,7 @@ const BRANCH_SCOPED_TOOLS = new Set([
|
|
|
725
764
|
'tool_map',
|
|
726
765
|
'shape_check',
|
|
727
766
|
'api_impact',
|
|
767
|
+
'trace',
|
|
728
768
|
]);
|
|
729
769
|
for (const tool of GITNEXUS_TOOLS) {
|
|
730
770
|
if (!BRANCH_SCOPED_TOOLS.has(tool.name))
|
package/package.json
CHANGED
|
@@ -45,6 +45,7 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
|
|
|
45
45
|
| Intermittent failure | `context` → look for external calls, async deps |
|
|
46
46
|
| Performance issue | `context` → find symbols with many callers (hot paths) |
|
|
47
47
|
| Recent regression | `detect_changes` to see what your changes affect |
|
|
48
|
+
| "How does A reach B?" | `trace` between the two symbols — shortest call chain in one call |
|
|
48
49
|
|
|
49
50
|
## Tools
|
|
50
51
|
|
|
@@ -72,6 +73,17 @@ MATCH path = (a)-[:CodeRelation {type: 'CALLS'}*1..2]->(b:Function {name: "valid
|
|
|
72
73
|
RETURN [n IN nodes(path) | n.name] AS chain
|
|
73
74
|
```
|
|
74
75
|
|
|
76
|
+
**trace** — shortest call chain between two symbols ("how does A reach B?"), one call instead of chaining `context` hops:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
trace({ from: "processCheckout", to: "fetchRates" })
|
|
80
|
+
→ status: ok, hopCount: 3
|
|
81
|
+
→ hops: processCheckout → validatePayment → verifyCard → fetchRates
|
|
82
|
+
→ edges: CALLS (1.0), CALLS (0.95), CALLS (1.0)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
When no path exists, `trace` reports the furthest reachable node — exactly where the chain breaks (dynamic dispatch, reflection, or an external boundary).
|
|
86
|
+
|
|
75
87
|
## Example: "Payment endpoint returns 500 intermittently"
|
|
76
88
|
|
|
77
89
|
```
|
package/skills/gitnexus-guide.md
CHANGED
|
@@ -35,6 +35,7 @@ For any task involving code understanding, debugging, impact analysis, or refact
|
|
|
35
35
|
| `query` | Process-grouped code intelligence — execution flows related to a concept |
|
|
36
36
|
| `context` | 360-degree symbol view — categorized refs, processes it participates in |
|
|
37
37
|
| `impact` | Symbol blast radius — what breaks at depth 1/2/3 with confidence |
|
|
38
|
+
| `trace` | Shortest path between two symbols — "how does A reach B?" in one call |
|
|
38
39
|
| `detect_changes` | Git-diff impact — what do your current changes affect |
|
|
39
40
|
| `rename` | Multi-file coordinated rename with confidence-tagged edits |
|
|
40
41
|
| `cypher` | Raw graph queries (read `gitnexus://repo/{name}/schema` first) |
|
|
@@ -93,6 +94,16 @@ A repo indexed without `--pdg` returns a clear "no taint layer" note. Caveats: f
|
|
|
93
94
|
|
|
94
95
|
A repo indexed without `--pdg` returns a "no PDG layer" note (or "status unknown" when the layer can't be confirmed). Intra-procedural only — cross-function flow is taint's domain (`explain`). The raw CDG/REACHING_DEF edges are also queryable via `cypher`. See the `gitnexus-pdg-query` skill for the full query surface.
|
|
95
96
|
|
|
97
|
+
### Shortest path between two symbols (`trace`)
|
|
98
|
+
|
|
99
|
+
`trace` answers "how does A reach B?" in one call — the shortest directed path over `CALLS` (plus `HAS_METHOD`, so a class-rooted trace descends into its methods) instead of chaining 3–8 `context`/`impact` hops by hand.
|
|
100
|
+
|
|
101
|
+
- `trace { from: "validateUser", to: "executeQuery" }` — shortest path between two symbols.
|
|
102
|
+
- Disambiguate common names with `from_uid`/`to_uid` (zero-ambiguity) or `from_file`/`to_file`; an ambiguous name returns ranked candidates.
|
|
103
|
+
- `maxDepth` (default 10, max 30) bounds the search; `includeTests` (default false) lets the traversal pass through test-file symbols.
|
|
104
|
+
|
|
105
|
+
Returns ordered `hops` (each `{ name, filePath, startLine }`) and an aligned `edges[]` of `{ relType, confidence }`, so call hops and containment (`HAS_METHOD`) hops stay distinguishable. When no path exists it reports the **furthest** reachable node (where the chain breaks) and sets `truncated: true` if a traversal cap was hit first. Every result carries a `status`: `ok` / `no_path` / `ambiguous` / `not_found` / `error`.
|
|
106
|
+
|
|
96
107
|
## Resources Reference
|
|
97
108
|
|
|
98
109
|
Lightweight reads (~100-500 tokens) for navigation:
|