@veewo/gitnexus 1.4.8 → 1.4.9
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/ai-context.js +1 -2
- package/dist/cli/analyze.js +2 -1
- package/dist/cli/index.js +8 -0
- package/dist/cli/repo-manager-alias.test.js +2 -0
- package/dist/cli/tool.d.ts +11 -0
- package/dist/cli/tool.js +59 -4
- package/dist/cli/unity-ui-trace.test.d.ts +1 -0
- package/dist/cli/unity-ui-trace.test.js +24 -0
- package/dist/core/ingestion/call-processor.js +9 -1
- package/dist/core/ingestion/type-env.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/csharp.js +17 -2
- package/dist/core/ingestion/type-extractors/types.d.ts +4 -1
- package/dist/core/unity/csharp-selector-binding.d.ts +7 -0
- package/dist/core/unity/csharp-selector-binding.js +32 -0
- package/dist/core/unity/csharp-selector-binding.test.d.ts +1 -0
- package/dist/core/unity/csharp-selector-binding.test.js +14 -0
- package/dist/core/unity/scan-context.d.ts +2 -0
- package/dist/core/unity/scan-context.js +17 -0
- package/dist/core/unity/ui-asset-ref-scanner.d.ts +14 -0
- package/dist/core/unity/ui-asset-ref-scanner.js +213 -0
- package/dist/core/unity/ui-asset-ref-scanner.test.d.ts +1 -0
- package/dist/core/unity/ui-asset-ref-scanner.test.js +44 -0
- package/dist/core/unity/ui-meta-index.d.ts +8 -0
- package/dist/core/unity/ui-meta-index.js +60 -0
- package/dist/core/unity/ui-meta-index.test.d.ts +1 -0
- package/dist/core/unity/ui-meta-index.test.js +18 -0
- package/dist/core/unity/ui-trace-storage-guard.test.d.ts +1 -0
- package/dist/core/unity/ui-trace-storage-guard.test.js +10 -0
- package/dist/core/unity/ui-trace.acceptance.test.d.ts +1 -0
- package/dist/core/unity/ui-trace.acceptance.test.js +38 -0
- package/dist/core/unity/ui-trace.d.ts +31 -0
- package/dist/core/unity/ui-trace.js +363 -0
- package/dist/core/unity/ui-trace.test.d.ts +1 -0
- package/dist/core/unity/ui-trace.test.js +183 -0
- package/dist/core/unity/uss-selector-parser.d.ts +6 -0
- package/dist/core/unity/uss-selector-parser.js +21 -0
- package/dist/core/unity/uss-selector-parser.test.d.ts +1 -0
- package/dist/core/unity/uss-selector-parser.test.js +13 -0
- package/dist/core/unity/uxml-ref-parser.d.ts +10 -0
- package/dist/core/unity/uxml-ref-parser.js +22 -0
- package/dist/core/unity/uxml-ref-parser.test.d.ts +1 -0
- package/dist/core/unity/uxml-ref-parser.test.js +31 -0
- package/dist/mcp/local/local-backend.d.ts +13 -0
- package/dist/mcp/local/local-backend.js +298 -25
- package/dist/mcp/tools.js +41 -0
- package/dist/storage/repo-manager.d.ts +1 -0
- package/package.json +2 -1
- package/skills/gitnexus-cli.md +29 -0
- package/skills/gitnexus-guide.md +23 -0
|
@@ -13,6 +13,18 @@ import { type RegistryEntry } from '../../storage/repo-manager.js';
|
|
|
13
13
|
* Matches common test file patterns across all supported languages.
|
|
14
14
|
*/
|
|
15
15
|
export declare function isTestFilePath(filePath: string): boolean;
|
|
16
|
+
export interface ExpandedSymbolCandidate {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
type: string;
|
|
20
|
+
filePath: string;
|
|
21
|
+
startLine?: number;
|
|
22
|
+
endLine?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function filterBm25ResultsByScopePreset<T extends {
|
|
25
|
+
filePath?: string;
|
|
26
|
+
}>(rows: T[], scopePreset?: string): T[];
|
|
27
|
+
export declare function rankExpandedSymbolsForQuery(symbols: ExpandedSymbolCandidate[], query: string, limit?: number, scopePreset?: string): ExpandedSymbolCandidate[];
|
|
16
28
|
export declare function mergeUnityBindings(baseBindings: ResolvedUnityBinding[], resolvedByPath: Map<string, ResolvedUnityBinding[]>): ResolvedUnityBinding[];
|
|
17
29
|
export declare function mergeParityUnityBindings(baseNonLightweightBindings: ResolvedUnityBinding[], resolvedBindings: ResolvedUnityBinding[]): ResolvedUnityBinding[];
|
|
18
30
|
export declare function attachUnityHydrationMeta(payload: UnityContextPayload, input: Pick<UnityHydrationMeta, 'requestedMode' | 'effectiveMode' | 'elapsedMs' | 'fallbackToCompact'> & {
|
|
@@ -97,6 +109,7 @@ export declare class LocalBackend {
|
|
|
97
109
|
stats?: any;
|
|
98
110
|
}>>;
|
|
99
111
|
callTool(method: string, params: any): Promise<any>;
|
|
112
|
+
private unityUiTrace;
|
|
100
113
|
/**
|
|
101
114
|
* Query tool — process-grouped search.
|
|
102
115
|
*
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { initLbug, executeQuery, executeParameterized, closeLbug, isLbugReady } from '../core/lbug-adapter.js';
|
|
11
|
+
import { runUnityUiTrace } from '../../core/unity/ui-trace.js';
|
|
11
12
|
// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
|
|
12
13
|
// at MCP server startup — crashes on unsupported Node ABI versions (#89)
|
|
13
14
|
// git utilities available if needed
|
|
@@ -32,6 +33,120 @@ export function isTestFilePath(filePath) {
|
|
|
32
33
|
function normalizePath(filePath) {
|
|
33
34
|
return String(filePath || '').replace(/\\/g, '/');
|
|
34
35
|
}
|
|
36
|
+
const UNITY_GAMEPLAY_INCLUDE_PREFIXES = ['assets/'];
|
|
37
|
+
const UNITY_GAMEPLAY_EXCLUDE_PREFIXES = [
|
|
38
|
+
'assets/plugins/',
|
|
39
|
+
'packages/',
|
|
40
|
+
'library/',
|
|
41
|
+
'projectsettings/',
|
|
42
|
+
'usersettings/',
|
|
43
|
+
'temp/',
|
|
44
|
+
];
|
|
45
|
+
const UNITY_PLUGIN_INTENT_TOKENS = new Set(['plugin', 'plugins', 'fmod', 'steam', 'crash', 'sdk', 'package']);
|
|
46
|
+
const QUERY_STOP_WORDS = new Set([
|
|
47
|
+
'the', 'and', 'for', 'from', 'with', 'that', 'this', 'into',
|
|
48
|
+
'using', 'use', 'in', 'on', 'of', 'to', 'a', 'an',
|
|
49
|
+
]);
|
|
50
|
+
function tokenizeQuery(query) {
|
|
51
|
+
const normalized = String(query || '').toLowerCase();
|
|
52
|
+
return normalized
|
|
53
|
+
.split(/[^a-z0-9_]+/)
|
|
54
|
+
.map((token) => token.trim())
|
|
55
|
+
.filter((token) => token.length >= 2 && !QUERY_STOP_WORDS.has(token));
|
|
56
|
+
}
|
|
57
|
+
function matchesAnyPrefix(pathLower, prefixes) {
|
|
58
|
+
return prefixes.some((prefix) => pathLower.startsWith(prefix));
|
|
59
|
+
}
|
|
60
|
+
function isUnityPluginPath(filePath) {
|
|
61
|
+
const p = normalizePath(filePath).toLowerCase();
|
|
62
|
+
return p.startsWith('assets/plugins/') || p.startsWith('packages/') || p.startsWith('library/packagecache/');
|
|
63
|
+
}
|
|
64
|
+
function isUnityGameplayPath(filePath) {
|
|
65
|
+
const p = normalizePath(filePath).toLowerCase();
|
|
66
|
+
return p.startsWith('assets/') && !isUnityPluginPath(p);
|
|
67
|
+
}
|
|
68
|
+
function resolveQueryScopePreset(scopePreset) {
|
|
69
|
+
if (!scopePreset)
|
|
70
|
+
return undefined;
|
|
71
|
+
const normalized = String(scopePreset).trim().toLowerCase();
|
|
72
|
+
if (normalized === 'unity-gameplay' || normalized === 'unity-all') {
|
|
73
|
+
return normalized;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
export function filterBm25ResultsByScopePreset(rows, scopePreset) {
|
|
78
|
+
const preset = resolveQueryScopePreset(scopePreset);
|
|
79
|
+
if (!preset || preset === 'unity-all')
|
|
80
|
+
return rows;
|
|
81
|
+
if (preset === 'unity-gameplay') {
|
|
82
|
+
return rows.filter((row) => {
|
|
83
|
+
const p = normalizePath(row.filePath || '').toLowerCase();
|
|
84
|
+
if (!p)
|
|
85
|
+
return false;
|
|
86
|
+
if (!matchesAnyPrefix(p, UNITY_GAMEPLAY_INCLUDE_PREFIXES))
|
|
87
|
+
return false;
|
|
88
|
+
if (matchesAnyPrefix(p, UNITY_GAMEPLAY_EXCLUDE_PREFIXES))
|
|
89
|
+
return false;
|
|
90
|
+
return true;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return rows;
|
|
94
|
+
}
|
|
95
|
+
function scoreExpandedSymbolForQuery(symbol, queryTokens, scopePreset) {
|
|
96
|
+
const name = String(symbol.name || '').toLowerCase();
|
|
97
|
+
const filePath = normalizePath(symbol.filePath || '').toLowerCase();
|
|
98
|
+
const pathText = filePath.replace(/[^a-z0-9_]+/g, ' ');
|
|
99
|
+
const hasPluginIntent = queryTokens.some((token) => UNITY_PLUGIN_INTENT_TOKENS.has(token));
|
|
100
|
+
let score = 0;
|
|
101
|
+
for (const token of queryTokens) {
|
|
102
|
+
if (name === token)
|
|
103
|
+
score += 8;
|
|
104
|
+
else if (name.includes(token))
|
|
105
|
+
score += 3;
|
|
106
|
+
if (pathText.includes(token))
|
|
107
|
+
score += 1;
|
|
108
|
+
}
|
|
109
|
+
if (queryTokens.length > 0) {
|
|
110
|
+
const fileName = filePath.split('/').pop() || '';
|
|
111
|
+
const fileNameNoExt = fileName.replace(/\.[a-z0-9]+$/i, '');
|
|
112
|
+
if (queryTokens.includes(fileNameNoExt)) {
|
|
113
|
+
score += 2;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (isUnityPluginPath(filePath) && !hasPluginIntent) {
|
|
117
|
+
score -= scopePreset === 'unity-gameplay' ? 10 : 4;
|
|
118
|
+
}
|
|
119
|
+
else if (scopePreset === 'unity-gameplay' && isUnityGameplayPath(filePath)) {
|
|
120
|
+
score += 2;
|
|
121
|
+
}
|
|
122
|
+
return score;
|
|
123
|
+
}
|
|
124
|
+
export function rankExpandedSymbolsForQuery(symbols, query, limit = 3, scopePreset) {
|
|
125
|
+
const queryTokens = tokenizeQuery(query);
|
|
126
|
+
return [...symbols]
|
|
127
|
+
.sort((a, b) => {
|
|
128
|
+
const scoreDelta = scoreExpandedSymbolForQuery(b, queryTokens, scopePreset)
|
|
129
|
+
- scoreExpandedSymbolForQuery(a, queryTokens, scopePreset);
|
|
130
|
+
if (scoreDelta !== 0)
|
|
131
|
+
return scoreDelta;
|
|
132
|
+
const aLine = a.startLine ?? Number.MAX_SAFE_INTEGER;
|
|
133
|
+
const bLine = b.startLine ?? Number.MAX_SAFE_INTEGER;
|
|
134
|
+
if (aLine !== bLine)
|
|
135
|
+
return aLine - bLine;
|
|
136
|
+
return String(a.name || '').localeCompare(String(b.name || ''));
|
|
137
|
+
})
|
|
138
|
+
.slice(0, Math.max(1, limit));
|
|
139
|
+
}
|
|
140
|
+
function getUnityPathScoreMultiplier(filePath, queryTokens, scopePreset) {
|
|
141
|
+
const hasPluginIntent = queryTokens.some((token) => UNITY_PLUGIN_INTENT_TOKENS.has(token));
|
|
142
|
+
if (isUnityPluginPath(filePath) && !hasPluginIntent) {
|
|
143
|
+
return scopePreset === 'unity-gameplay' ? 0.1 : 0.45;
|
|
144
|
+
}
|
|
145
|
+
if (scopePreset === 'unity-gameplay' && isUnityGameplayPath(filePath)) {
|
|
146
|
+
return 1.15;
|
|
147
|
+
}
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
35
150
|
function bindingIdentity(binding) {
|
|
36
151
|
return [
|
|
37
152
|
normalizePath(binding.resourcePath),
|
|
@@ -330,6 +445,8 @@ export class LocalBackend {
|
|
|
330
445
|
return this.context(repo, params);
|
|
331
446
|
case 'impact':
|
|
332
447
|
return this.impact(repo, params);
|
|
448
|
+
case 'unity_ui_trace':
|
|
449
|
+
return this.unityUiTrace(repo, params);
|
|
333
450
|
case 'detect_changes':
|
|
334
451
|
return this.detectChanges(repo, params);
|
|
335
452
|
case 'rename':
|
|
@@ -345,6 +462,31 @@ export class LocalBackend {
|
|
|
345
462
|
throw new Error(`Unknown tool: ${method}`);
|
|
346
463
|
}
|
|
347
464
|
}
|
|
465
|
+
async unityUiTrace(repo, params) {
|
|
466
|
+
const target = String(params?.target || '').trim();
|
|
467
|
+
const goal = params?.goal;
|
|
468
|
+
const selectorMode = params?.selector_mode || 'balanced';
|
|
469
|
+
if (!target) {
|
|
470
|
+
return { error: 'target parameter is required and cannot be empty.' };
|
|
471
|
+
}
|
|
472
|
+
if (goal !== 'asset_refs' && goal !== 'template_refs' && goal !== 'selector_bindings') {
|
|
473
|
+
return { error: 'goal must be one of: asset_refs, template_refs, selector_bindings.' };
|
|
474
|
+
}
|
|
475
|
+
if (selectorMode !== 'strict' && selectorMode !== 'balanced') {
|
|
476
|
+
return { error: 'selector_mode must be one of: strict, balanced.' };
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
return await runUnityUiTrace({
|
|
480
|
+
repoRoot: repo.repoPath,
|
|
481
|
+
target,
|
|
482
|
+
goal,
|
|
483
|
+
selectorMode,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
catch (err) {
|
|
487
|
+
return { error: err?.message || 'unity_ui_trace failed' };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
348
490
|
// ─── Tool Implementations ────────────────────────────────────────
|
|
349
491
|
/**
|
|
350
492
|
* Query tool — process-grouped search.
|
|
@@ -366,7 +508,7 @@ export class LocalBackend {
|
|
|
366
508
|
// Step 1: Run hybrid search to get matching symbols
|
|
367
509
|
const searchLimit = processLimit * maxSymbolsPerProcess; // fetch enough raw results
|
|
368
510
|
const [bm25Results, semanticResults] = await Promise.all([
|
|
369
|
-
this.bm25Search(repo, searchQuery, searchLimit),
|
|
511
|
+
this.bm25Search(repo, searchQuery, searchLimit, params.scope_preset),
|
|
370
512
|
this.semanticSearch(repo, searchQuery, searchLimit),
|
|
371
513
|
]);
|
|
372
514
|
// Merge via reciprocal rank fusion
|
|
@@ -540,8 +682,9 @@ export class LocalBackend {
|
|
|
540
682
|
/**
|
|
541
683
|
* BM25 keyword search helper - uses LadybugDB FTS for always-fresh results
|
|
542
684
|
*/
|
|
543
|
-
async bm25Search(repo, query, limit) {
|
|
685
|
+
async bm25Search(repo, query, limit, scopePreset) {
|
|
544
686
|
const { searchFTSFromLbug } = await import('../../core/search/bm25-index.js');
|
|
687
|
+
const queryTokens = tokenizeQuery(query);
|
|
545
688
|
let bm25Results;
|
|
546
689
|
try {
|
|
547
690
|
bm25Results = await searchFTSFromLbug(query, limit, repo.id);
|
|
@@ -550,26 +693,36 @@ export class LocalBackend {
|
|
|
550
693
|
console.error('GitNexus: BM25/FTS search failed (FTS indexes may not exist) -', err.message);
|
|
551
694
|
return [];
|
|
552
695
|
}
|
|
696
|
+
bm25Results = filterBm25ResultsByScopePreset(bm25Results, scopePreset);
|
|
553
697
|
const results = [];
|
|
554
698
|
for (const bm25Result of bm25Results) {
|
|
555
699
|
const fullPath = bm25Result.filePath;
|
|
700
|
+
const adjustedScore = Number(bm25Result.score || 0) * getUnityPathScoreMultiplier(fullPath, queryTokens, scopePreset);
|
|
556
701
|
try {
|
|
557
702
|
const symbols = await executeParameterized(repo.id, `
|
|
558
703
|
MATCH (n)
|
|
559
704
|
WHERE n.filePath = $filePath
|
|
560
705
|
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
|
|
561
|
-
LIMIT
|
|
706
|
+
LIMIT 50
|
|
562
707
|
`, { filePath: fullPath });
|
|
563
708
|
if (symbols.length > 0) {
|
|
564
|
-
|
|
709
|
+
const rankedSymbols = rankExpandedSymbolsForQuery(symbols.map((sym) => ({
|
|
710
|
+
id: sym.id || sym[0],
|
|
711
|
+
name: sym.name || sym[1],
|
|
712
|
+
type: sym.type || sym[2],
|
|
713
|
+
filePath: sym.filePath || sym[3],
|
|
714
|
+
startLine: sym.startLine || sym[4],
|
|
715
|
+
endLine: sym.endLine || sym[5],
|
|
716
|
+
})), query, 3, scopePreset);
|
|
717
|
+
for (const sym of rankedSymbols) {
|
|
565
718
|
results.push({
|
|
566
|
-
nodeId: sym.id
|
|
567
|
-
name: sym.name
|
|
568
|
-
type: sym.type
|
|
569
|
-
filePath: sym.filePath
|
|
570
|
-
startLine: sym.startLine
|
|
571
|
-
endLine: sym.endLine
|
|
572
|
-
bm25Score:
|
|
719
|
+
nodeId: sym.id,
|
|
720
|
+
name: sym.name,
|
|
721
|
+
type: sym.type,
|
|
722
|
+
filePath: sym.filePath,
|
|
723
|
+
startLine: sym.startLine,
|
|
724
|
+
endLine: sym.endLine,
|
|
725
|
+
bm25Score: adjustedScore,
|
|
573
726
|
});
|
|
574
727
|
}
|
|
575
728
|
}
|
|
@@ -579,7 +732,7 @@ export class LocalBackend {
|
|
|
579
732
|
name: fileName,
|
|
580
733
|
type: 'File',
|
|
581
734
|
filePath: bm25Result.filePath,
|
|
582
|
-
bm25Score:
|
|
735
|
+
bm25Score: adjustedScore,
|
|
583
736
|
});
|
|
584
737
|
}
|
|
585
738
|
}
|
|
@@ -589,11 +742,11 @@ export class LocalBackend {
|
|
|
589
742
|
name: fileName,
|
|
590
743
|
type: 'File',
|
|
591
744
|
filePath: bm25Result.filePath,
|
|
592
|
-
bm25Score:
|
|
745
|
+
bm25Score: adjustedScore,
|
|
593
746
|
});
|
|
594
747
|
}
|
|
595
748
|
}
|
|
596
|
-
return results;
|
|
749
|
+
return results.sort((a, b) => (Number(b.bm25Score || 0) - Number(a.bm25Score || 0)));
|
|
597
750
|
}
|
|
598
751
|
/**
|
|
599
752
|
* Semantic vector search helper
|
|
@@ -858,20 +1011,63 @@ export class LocalBackend {
|
|
|
858
1011
|
// Step 3: Build full context
|
|
859
1012
|
const sym = symbols[0];
|
|
860
1013
|
const symId = sym.id || sym[0];
|
|
861
|
-
//
|
|
862
|
-
const
|
|
1014
|
+
// Direct incoming refs for the selected symbol.
|
|
1015
|
+
const directIncomingRows = await executeParameterized(repo.id, `
|
|
863
1016
|
MATCH (caller)-[r:CodeRelation]->(n {id: $symId})
|
|
864
1017
|
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
865
1018
|
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
|
866
1019
|
LIMIT 30
|
|
867
1020
|
`, { symId });
|
|
868
|
-
//
|
|
869
|
-
const
|
|
1021
|
+
// Direct outgoing refs for the selected symbol.
|
|
1022
|
+
const directOutgoingRows = await executeParameterized(repo.id, `
|
|
870
1023
|
MATCH (n {id: $symId})-[r:CodeRelation]->(target)
|
|
871
1024
|
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
872
1025
|
RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
|
|
873
1026
|
LIMIT 30
|
|
874
1027
|
`, { symId });
|
|
1028
|
+
const kind = sym.type || sym[2];
|
|
1029
|
+
const symIdLower = String(symId).toLowerCase();
|
|
1030
|
+
const isMethodContainer = new Set(['Class', 'Interface', 'Struct', 'Trait', 'Impl', 'Record']).has(kind)
|
|
1031
|
+
|| symIdLower.startsWith('class:')
|
|
1032
|
+
|| symIdLower.startsWith('interface:')
|
|
1033
|
+
|| symIdLower.startsWith('struct:')
|
|
1034
|
+
|| symIdLower.startsWith('trait:')
|
|
1035
|
+
|| symIdLower.startsWith('impl:')
|
|
1036
|
+
|| symIdLower.startsWith('record:');
|
|
1037
|
+
let incomingRows = [...directIncomingRows];
|
|
1038
|
+
let outgoingRows = [...directOutgoingRows];
|
|
1039
|
+
if (isMethodContainer) {
|
|
1040
|
+
const methodIncomingRows = await executeParameterized(repo.id, `
|
|
1041
|
+
MATCH (n {id: $symId})-[:CodeRelation {type: 'HAS_METHOD'}]->(m)
|
|
1042
|
+
MATCH (caller)-[r:CodeRelation]->(m)
|
|
1043
|
+
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
1044
|
+
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
|
1045
|
+
LIMIT 60
|
|
1046
|
+
`, { symId });
|
|
1047
|
+
const methodOutgoingRows = await executeParameterized(repo.id, `
|
|
1048
|
+
MATCH (n {id: $symId})-[:CodeRelation {type: 'HAS_METHOD'}]->(m)
|
|
1049
|
+
MATCH (m)-[r:CodeRelation]->(target)
|
|
1050
|
+
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
1051
|
+
RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
|
|
1052
|
+
LIMIT 60
|
|
1053
|
+
`, { symId });
|
|
1054
|
+
const dedupe = (rows) => {
|
|
1055
|
+
const seen = new Set();
|
|
1056
|
+
const out = [];
|
|
1057
|
+
for (const row of rows) {
|
|
1058
|
+
const relType = row.relType || row[0] || '';
|
|
1059
|
+
const uidVal = row.uid || row[1] || '';
|
|
1060
|
+
const key = `${relType}:${uidVal}`;
|
|
1061
|
+
if (seen.has(key))
|
|
1062
|
+
continue;
|
|
1063
|
+
seen.add(key);
|
|
1064
|
+
out.push(row);
|
|
1065
|
+
}
|
|
1066
|
+
return out;
|
|
1067
|
+
};
|
|
1068
|
+
incomingRows = dedupe([...directIncomingRows, ...methodIncomingRows]);
|
|
1069
|
+
outgoingRows = dedupe([...directOutgoingRows, ...methodOutgoingRows]);
|
|
1070
|
+
}
|
|
875
1071
|
// Process participation
|
|
876
1072
|
let processRows = [];
|
|
877
1073
|
try {
|
|
@@ -905,7 +1101,7 @@ export class LocalBackend {
|
|
|
905
1101
|
symbol: {
|
|
906
1102
|
uid: sym.id || sym[0],
|
|
907
1103
|
name: sym.name || sym[1],
|
|
908
|
-
kind
|
|
1104
|
+
kind,
|
|
909
1105
|
filePath: sym.filePath || sym[3],
|
|
910
1106
|
startLine: sym.startLine || sym[4],
|
|
911
1107
|
endLine: sym.endLine || sym[5],
|
|
@@ -913,6 +1109,8 @@ export class LocalBackend {
|
|
|
913
1109
|
},
|
|
914
1110
|
incoming: categorize(incomingRows),
|
|
915
1111
|
outgoing: categorize(outgoingRows),
|
|
1112
|
+
directIncoming: categorize(directIncomingRows),
|
|
1113
|
+
directOutgoing: categorize(directOutgoingRows),
|
|
916
1114
|
processes: processRows.map((r) => ({
|
|
917
1115
|
id: r.pid || r[0],
|
|
918
1116
|
name: r.label || r[1],
|
|
@@ -1279,22 +1477,41 @@ export class LocalBackend {
|
|
|
1279
1477
|
}
|
|
1280
1478
|
async _impactImpl(repo, params) {
|
|
1281
1479
|
await this.ensureInitialized(repo.id);
|
|
1282
|
-
const { target, direction } = params;
|
|
1480
|
+
const { target, target_uid, file_path, direction } = params;
|
|
1283
1481
|
const maxDepth = params.maxDepth || 3;
|
|
1482
|
+
const usesDefaultRelationTypes = !params.relationTypes || params.relationTypes.length === 0;
|
|
1284
1483
|
const rawRelTypes = params.relationTypes && params.relationTypes.length > 0
|
|
1285
1484
|
? params.relationTypes.filter(t => VALID_RELATION_TYPES.has(t))
|
|
1286
1485
|
: ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS'];
|
|
1287
1486
|
const relationTypes = rawRelTypes.length > 0 ? rawRelTypes : ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS'];
|
|
1288
1487
|
const includeTests = params.includeTests ?? false;
|
|
1289
1488
|
const minConfidence = params.minConfidence ?? 0;
|
|
1489
|
+
const shouldBridgeClassMethods = usesDefaultRelationTypes;
|
|
1290
1490
|
const relTypeFilter = relationTypes.map(t => `'${t}'`).join(', ');
|
|
1291
1491
|
const confidenceFilter = minConfidence > 0 ? ` AND r.confidence >= ${minConfidence}` : '';
|
|
1492
|
+
const targetQueryParts = [];
|
|
1493
|
+
const targetParams = { targetName: target };
|
|
1494
|
+
if (target_uid) {
|
|
1495
|
+
targetQueryParts.push('n.id = $targetUid');
|
|
1496
|
+
targetParams.targetUid = target_uid;
|
|
1497
|
+
}
|
|
1498
|
+
else if (file_path) {
|
|
1499
|
+
targetQueryParts.push('n.name = $targetName');
|
|
1500
|
+
targetQueryParts.push('n.filePath CONTAINS $filePath');
|
|
1501
|
+
targetParams.filePath = file_path;
|
|
1502
|
+
}
|
|
1503
|
+
else if (target.includes('/') || target.includes(':')) {
|
|
1504
|
+
targetQueryParts.push('(n.id = $targetName OR n.name = $targetName)');
|
|
1505
|
+
}
|
|
1506
|
+
else {
|
|
1507
|
+
targetQueryParts.push('n.name = $targetName');
|
|
1508
|
+
}
|
|
1292
1509
|
const targets = await executeParameterized(repo.id, `
|
|
1293
1510
|
MATCH (n)
|
|
1294
|
-
WHERE
|
|
1511
|
+
WHERE ${targetQueryParts.join(' AND ')}
|
|
1295
1512
|
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
1296
|
-
LIMIT
|
|
1297
|
-
`,
|
|
1513
|
+
LIMIT 10
|
|
1514
|
+
`, targetParams);
|
|
1298
1515
|
if (targets.length === 0)
|
|
1299
1516
|
return { error: `Target '${target}' not found` };
|
|
1300
1517
|
const sym = targets[0];
|
|
@@ -1302,11 +1519,63 @@ export class LocalBackend {
|
|
|
1302
1519
|
const impacted = [];
|
|
1303
1520
|
const visited = new Set([symId]);
|
|
1304
1521
|
let frontier = [symId];
|
|
1522
|
+
let frontierKindById = new Map([[symId, sym.type || sym[2] || '']]);
|
|
1305
1523
|
let traversalComplete = true;
|
|
1306
1524
|
for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
|
|
1307
1525
|
const nextFrontier = [];
|
|
1526
|
+
let traversalFrontier = [...frontier];
|
|
1527
|
+
// Bridge class-like symbols through HAS_METHOD so class-level impact includes method-level dependencies.
|
|
1528
|
+
if (shouldBridgeClassMethods) {
|
|
1529
|
+
if (frontier.length > 0) {
|
|
1530
|
+
const bridgeIdList = frontier.map(id => `'${id.replace(/'/g, "''")}'`).join(', ');
|
|
1531
|
+
try {
|
|
1532
|
+
const bridgeRows = await executeQuery(repo.id, `
|
|
1533
|
+
MATCH (container)-[r:CodeRelation {type: 'HAS_METHOD'}]->(method)
|
|
1534
|
+
WHERE container.id IN [${bridgeIdList}]${confidenceFilter}
|
|
1535
|
+
RETURN container.id AS containerId, method.id AS methodId, method.name AS methodName, labels(method)[0] AS methodType, method.filePath AS methodFilePath
|
|
1536
|
+
`);
|
|
1537
|
+
for (const bridge of bridgeRows) {
|
|
1538
|
+
const methodId = bridge.methodId || bridge[1];
|
|
1539
|
+
const methodType = bridge.methodType || bridge[3];
|
|
1540
|
+
if (!methodId)
|
|
1541
|
+
continue;
|
|
1542
|
+
if (!frontierKindById.has(methodId)) {
|
|
1543
|
+
frontierKindById.set(methodId, methodType || '');
|
|
1544
|
+
}
|
|
1545
|
+
if (direction === 'upstream') {
|
|
1546
|
+
traversalFrontier.push(methodId);
|
|
1547
|
+
}
|
|
1548
|
+
else {
|
|
1549
|
+
const methodFilePath = bridge.methodFilePath || bridge[4] || '';
|
|
1550
|
+
if (!includeTests && isTestFilePath(methodFilePath))
|
|
1551
|
+
continue;
|
|
1552
|
+
if (!visited.has(methodId)) {
|
|
1553
|
+
visited.add(methodId);
|
|
1554
|
+
nextFrontier.push(methodId);
|
|
1555
|
+
impacted.push({
|
|
1556
|
+
depth,
|
|
1557
|
+
id: methodId,
|
|
1558
|
+
name: bridge.methodName || bridge[2],
|
|
1559
|
+
type: methodType,
|
|
1560
|
+
filePath: methodFilePath,
|
|
1561
|
+
relationType: 'HAS_METHOD',
|
|
1562
|
+
confidence: 1.0,
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
catch (e) {
|
|
1569
|
+
logQueryError('impact:class-method-bridge', e);
|
|
1570
|
+
traversalComplete = false;
|
|
1571
|
+
break;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
// de-dupe traversal frontier (class + bridged methods)
|
|
1576
|
+
traversalFrontier = [...new Set(traversalFrontier)];
|
|
1308
1577
|
// Batch frontier nodes into a single Cypher query per depth level
|
|
1309
|
-
const idList =
|
|
1578
|
+
const idList = traversalFrontier.map(id => `'${id.replace(/'/g, "''")}'`).join(', ');
|
|
1310
1579
|
const query = direction === 'upstream'
|
|
1311
1580
|
? `MATCH (caller)-[r:CodeRelation]->(n) WHERE n.id IN [${idList}] AND r.type IN [${relTypeFilter}]${confidenceFilter} RETURN n.id AS sourceId, caller.id AS id, caller.name AS name, labels(caller)[0] AS type, caller.filePath AS filePath, r.type AS relType, r.confidence AS confidence`
|
|
1312
1581
|
: `MATCH (n)-[r:CodeRelation]->(callee) WHERE n.id IN [${idList}] AND r.type IN [${relTypeFilter}]${confidenceFilter} RETURN n.id AS sourceId, callee.id AS id, callee.name AS name, labels(callee)[0] AS type, callee.filePath AS filePath, r.type AS relType, r.confidence AS confidence`;
|
|
@@ -1320,11 +1589,15 @@ export class LocalBackend {
|
|
|
1320
1589
|
if (!visited.has(relId)) {
|
|
1321
1590
|
visited.add(relId);
|
|
1322
1591
|
nextFrontier.push(relId);
|
|
1592
|
+
const relType = rel.type || rel[3];
|
|
1593
|
+
if (!frontierKindById.has(relId)) {
|
|
1594
|
+
frontierKindById.set(relId, relType || '');
|
|
1595
|
+
}
|
|
1323
1596
|
impacted.push({
|
|
1324
1597
|
depth,
|
|
1325
1598
|
id: relId,
|
|
1326
1599
|
name: rel.name || rel[2],
|
|
1327
|
-
type:
|
|
1600
|
+
type: relType,
|
|
1328
1601
|
filePath,
|
|
1329
1602
|
relationType: rel.relType || rel[5],
|
|
1330
1603
|
confidence: rel.confidence || rel[6] || 1.0,
|
package/dist/mcp/tools.js
CHANGED
|
@@ -36,6 +36,9 @@ Returns results grouped by process (execution flow):
|
|
|
36
36
|
- definitions: standalone types/interfaces not in any process
|
|
37
37
|
|
|
38
38
|
Hybrid ranking: BM25 keyword + semantic vector search, ranked by Reciprocal Rank Fusion.
|
|
39
|
+
Supports optional scope controls for noisy codebases:
|
|
40
|
+
- scope_preset=unity-gameplay to prioritize project gameplay code and suppress plugin-heavy paths.
|
|
41
|
+
- scope_preset=unity-all (default behavior) to keep full Unity search scope.
|
|
39
42
|
|
|
40
43
|
Includes optional Unity retrieval contract:
|
|
41
44
|
- Set unity_resources=on|auto to include Unity resource evidence.
|
|
@@ -50,6 +53,11 @@ Includes optional Unity retrieval contract:
|
|
|
50
53
|
limit: { type: 'number', description: 'Max processes to return (default: 5)', default: 5 },
|
|
51
54
|
max_symbols: { type: 'number', description: 'Max symbols per process (default: 10)', default: 10 },
|
|
52
55
|
include_content: { type: 'boolean', description: 'Include full symbol source code (default: false)', default: false },
|
|
56
|
+
scope_preset: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
enum: ['unity-gameplay', 'unity-all'],
|
|
59
|
+
description: 'Optional retrieval preset. unity-gameplay reduces plugin/package noise in Unity projects.',
|
|
60
|
+
},
|
|
53
61
|
unity_resources: {
|
|
54
62
|
type: 'string',
|
|
55
63
|
enum: ['off', 'on', 'auto'],
|
|
@@ -197,6 +205,39 @@ Each edit is tagged with confidence:
|
|
|
197
205
|
required: ['new_name'],
|
|
198
206
|
},
|
|
199
207
|
},
|
|
208
|
+
{
|
|
209
|
+
name: 'unity_ui_trace',
|
|
210
|
+
description: `Resolve Unity UI evidence chains (query-time only, no graph writes).
|
|
211
|
+
|
|
212
|
+
Supports three goals:
|
|
213
|
+
- asset_refs: which prefab/asset points to a target UXML
|
|
214
|
+
- template_refs: which UXML templates are referenced by a target UXML
|
|
215
|
+
- selector_bindings: static C# selector bindings traced to USS selectors
|
|
216
|
+
|
|
217
|
+
Selector matching modes for selector_bindings:
|
|
218
|
+
- balanced (default): match class tokens inside composite selectors (higher recall)
|
|
219
|
+
- strict: only exact \`.className\` selectors (higher precision)
|
|
220
|
+
|
|
221
|
+
Output enforces unique-result policy and includes path+line evidence hops.`,
|
|
222
|
+
inputSchema: {
|
|
223
|
+
type: 'object',
|
|
224
|
+
properties: {
|
|
225
|
+
target: { type: 'string', description: 'Target C# class or UXML path' },
|
|
226
|
+
goal: {
|
|
227
|
+
type: 'string',
|
|
228
|
+
enum: ['asset_refs', 'template_refs', 'selector_bindings'],
|
|
229
|
+
description: 'Trace goal',
|
|
230
|
+
},
|
|
231
|
+
selector_mode: {
|
|
232
|
+
type: 'string',
|
|
233
|
+
enum: ['strict', 'balanced'],
|
|
234
|
+
description: 'Selector matching mode for selector_bindings (default: balanced)',
|
|
235
|
+
},
|
|
236
|
+
repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
|
|
237
|
+
},
|
|
238
|
+
required: ['target', 'goal'],
|
|
239
|
+
},
|
|
240
|
+
},
|
|
200
241
|
{
|
|
201
242
|
name: 'impact',
|
|
202
243
|
description: `Analyze the blast radius of changing a code symbol.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veewo/gitnexus",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.9",
|
|
4
4
|
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
|
5
5
|
"author": "Abhigyan Patwari",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"check:neonspark-target": "node -e \"const p=(process.env.GITNEXUS_NEONSPARK_TARGET_PATH||'').trim();if(!p){console.error('Missing env: GITNEXUS_NEONSPARK_TARGET_PATH');process.exit(1);}\"",
|
|
52
52
|
"check:u2-e2e-target": "node -e \"const p=(process.env.GITNEXUS_U2_E2E_TARGET_PATH||'').trim();if(!p){console.error('Missing env: GITNEXUS_U2_E2E_TARGET_PATH');process.exit(1);}\"",
|
|
53
53
|
"test:unity": "npm run build && node --test dist/core/unity/*.test.js",
|
|
54
|
+
"test:unity-ui-trace:smoke": "npm run build && node --test dist/core/unity/ui-trace.acceptance.test.js dist/core/unity/ui-trace.test.js dist/cli/unity-ui-trace.test.js",
|
|
54
55
|
"test:u3:gates": "npm run check:release-paths && npm run build && node --test dist/benchmark/u2-e2e/*.test.js dist/mcp/local/unity-enrichment.test.js dist/core/ingestion/unity-resource-processor.test.js",
|
|
55
56
|
"test:benchmark": "npm run build && node --test dist/benchmark/*.test.js && node --test dist/cli/*.test.js",
|
|
56
57
|
"benchmark:quick": "npm run build && node dist/cli/index.js benchmark-unity ../benchmarks/unity-baseline/v1 --profile quick --target-path ../benchmarks/fixtures/unity-mini",
|
package/skills/gitnexus-cli.md
CHANGED
|
@@ -113,6 +113,35 @@ Rules:
|
|
|
113
113
|
- If response `hydrationMeta.needsParityRetry=true`, rerun with `--unity-hydration parity`.
|
|
114
114
|
- `--unity-hydration parity` is completeness-first mode for advanced verification.
|
|
115
115
|
|
|
116
|
+
### unity-ui-trace — Unity UI evidence tracing workflow
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
$GN unity-ui-trace "Assets/NEON/VeewoUI/Uxml/BarScreen/Patch/PatchItemPreview.uxml" --goal asset_refs --repo neonspark
|
|
120
|
+
$GN unity-ui-trace "Assets/NEON/VeewoUI/Uxml/BarScreen/CoreScreen.uxml" --goal template_refs --repo neonspark
|
|
121
|
+
$GN unity-ui-trace "Assets/NEON/VeewoUI/Uxml/BarScreen/Patch/PatchItemPreview.uxml" --goal selector_bindings --selector-mode balanced --repo neonspark
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Supported goals:
|
|
125
|
+
- `asset_refs`: 哪些 prefab/asset 引用了目标 UXML
|
|
126
|
+
- `template_refs`: 目标 UXML 里引用了哪些模板 UXML
|
|
127
|
+
- `selector_bindings`: C# `AddToClassList/Q(className)` 到 USS 选择器证据链
|
|
128
|
+
|
|
129
|
+
Selector mode(仅 `selector_bindings` 生效):
|
|
130
|
+
- `--selector-mode balanced`(默认): 复合选择器 token 匹配,召回更高
|
|
131
|
+
- `--selector-mode strict`: 仅匹配精确 `.className` 选择器,精度更高
|
|
132
|
+
|
|
133
|
+
输出字段解读:
|
|
134
|
+
- `results[].evidence_chain`: 每跳都有 `path + line + snippet`
|
|
135
|
+
- `results[].score`: 排序分数(越高越优先)
|
|
136
|
+
- `results[].confidence`: `high|medium|low`(基于 score)
|
|
137
|
+
- `diagnostics`: `not_found|ambiguous` 诊断
|
|
138
|
+
|
|
139
|
+
推荐排查顺序(实仓):
|
|
140
|
+
1. 先跑 `asset_refs`,确认资源链是否存在
|
|
141
|
+
2. 再跑 `template_refs`,确认模板链是否存在
|
|
142
|
+
3. 最后跑 `selector_bindings`,默认 `balanced`
|
|
143
|
+
4. 若怀疑误报,复跑 `--selector-mode strict` 对比
|
|
144
|
+
|
|
116
145
|
## After Indexing
|
|
117
146
|
|
|
118
147
|
1. **Read `gitnexus://repo/{name}/context`** to verify the index loaded
|
package/skills/gitnexus-guide.md
CHANGED
|
@@ -37,6 +37,7 @@ For any task involving code understanding, debugging, impact analysis, or refact
|
|
|
37
37
|
| `impact` | Symbol blast radius — what breaks at depth 1/2/3 with confidence |
|
|
38
38
|
| `detect_changes` | Git-diff impact — what do your current changes affect |
|
|
39
39
|
| `rename` | Multi-file coordinated rename with confidence-tagged edits |
|
|
40
|
+
| `unity_ui_trace` | Unity UI query-time evidence chains (`asset_refs/template_refs/selector_bindings`) |
|
|
40
41
|
| `cypher` | Raw graph queries (read `gitnexus://repo/{name}/schema` first) |
|
|
41
42
|
| `list_repos` | Discover indexed repos |
|
|
42
43
|
|
|
@@ -55,6 +56,28 @@ Recommended default workflow:
|
|
|
55
56
|
- `isComplete: true` → keep compact result
|
|
56
57
|
3. Treat parity as the completeness path for advanced verification.
|
|
57
58
|
|
|
59
|
+
### Unity UI Trace Contract (`unity_ui_trace` / `gitnexus unity-ui-trace`)
|
|
60
|
+
|
|
61
|
+
Input:
|
|
62
|
+
- `target`: C# class 名或 UXML 路径
|
|
63
|
+
- `goal`: `asset_refs | template_refs | selector_bindings`
|
|
64
|
+
- `selector_mode`(可选): `balanced`(默认)或 `strict`
|
|
65
|
+
|
|
66
|
+
Modes:
|
|
67
|
+
- `balanced`: 复合选择器 token 匹配,召回优先
|
|
68
|
+
- `strict`: 仅精确 `.className` 选择器,精度优先
|
|
69
|
+
|
|
70
|
+
Output:
|
|
71
|
+
- `results[].evidence_chain`: 严格 `path + line + snippet` 证据跳
|
|
72
|
+
- `results[].score`: 排序分数(高分优先)
|
|
73
|
+
- `results[].confidence`: `high|medium|low`
|
|
74
|
+
- `diagnostics`: `not_found|ambiguous`
|
|
75
|
+
|
|
76
|
+
Recommended workflow:
|
|
77
|
+
1. 先跑 `asset_refs`(确认资源引用链存在)
|
|
78
|
+
2. 再跑 `template_refs`(确认模板引用链存在)
|
|
79
|
+
3. 最后跑 `selector_bindings`(先 `balanced`,必要时切 `strict` 验证)
|
|
80
|
+
|
|
58
81
|
## Resources Reference
|
|
59
82
|
|
|
60
83
|
Lightweight reads (~100-500 tokens) for navigation:
|