@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
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { glob } from 'glob';
|
|
5
|
+
const YAML_INLINE_GUID_PATTERN = /\bguid\s*:\s*([0-9a-f]{32})\b/i;
|
|
6
|
+
const YAML_FIELD_OPEN_PATTERN = /^\s*([A-Za-z0-9_]+)\s*:\s*\{/;
|
|
7
|
+
const YAML_INLINE_FIELD_PATTERN = /^\s*([A-Za-z0-9_]+)\s*:\s*\{[^}]*\bguid\s*:\s*([0-9a-f]{32})\b[^}]*\}/i;
|
|
8
|
+
const SCAN_CONCURRENCY = 8;
|
|
9
|
+
export async function scanUiAssetRefs(input) {
|
|
10
|
+
const resourceFiles = await resolveFiles(input.repoRoot, input.scopedPaths);
|
|
11
|
+
const guidFilter = toGuidFilter(input.targetGuids);
|
|
12
|
+
const candidateFiles = guidFilter && guidFilter.size > 0
|
|
13
|
+
? await findCandidateFilesByGuid(input.repoRoot, resourceFiles, guidFilter)
|
|
14
|
+
: resourceFiles;
|
|
15
|
+
const perFile = await mapWithConcurrency(candidateFiles, SCAN_CONCURRENCY, async (resourcePath) => {
|
|
16
|
+
const sourceType = resourcePath.endsWith('.prefab') ? 'prefab' : 'asset';
|
|
17
|
+
return scanResourceFileForGuidRefs(input.repoRoot, resourcePath, sourceType, guidFilter);
|
|
18
|
+
});
|
|
19
|
+
const all = [];
|
|
20
|
+
for (const entries of perFile) {
|
|
21
|
+
all.push(...entries);
|
|
22
|
+
}
|
|
23
|
+
return all;
|
|
24
|
+
}
|
|
25
|
+
async function findCandidateFilesByGuid(repoRoot, resourceFiles, guidFilter) {
|
|
26
|
+
const candidates = await mapWithConcurrency(resourceFiles, SCAN_CONCURRENCY, async (resourcePath) => {
|
|
27
|
+
const absolutePath = path.join(repoRoot, resourcePath);
|
|
28
|
+
const stream = createReadStream(absolutePath, { encoding: 'utf-8' });
|
|
29
|
+
const reader = createInterface({ input: stream, crlfDelay: Infinity });
|
|
30
|
+
try {
|
|
31
|
+
for await (const line of reader) {
|
|
32
|
+
const lower = line.toLowerCase();
|
|
33
|
+
for (const guid of guidFilter) {
|
|
34
|
+
if (lower.includes(guid))
|
|
35
|
+
return resourcePath;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const code = error.code;
|
|
42
|
+
if (code === 'ENOENT' || code === 'EISDIR')
|
|
43
|
+
return null;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
reader.close();
|
|
48
|
+
stream.destroy();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return candidates
|
|
52
|
+
.filter((value) => Boolean(value))
|
|
53
|
+
.sort((left, right) => left.localeCompare(right));
|
|
54
|
+
}
|
|
55
|
+
async function scanResourceFileForGuidRefs(repoRoot, resourcePath, sourceType, guidFilter) {
|
|
56
|
+
const absolutePath = path.join(repoRoot, resourcePath);
|
|
57
|
+
const stream = createReadStream(absolutePath, { encoding: 'utf-8' });
|
|
58
|
+
const reader = createInterface({ input: stream, crlfDelay: Infinity });
|
|
59
|
+
const out = [];
|
|
60
|
+
let lineNumber = 0;
|
|
61
|
+
let currentField = '';
|
|
62
|
+
let currentStartLine = 0;
|
|
63
|
+
let currentLines = [];
|
|
64
|
+
let currentGuidLine = 0;
|
|
65
|
+
let braceDepth = 0;
|
|
66
|
+
const flushCurrentBlock = () => {
|
|
67
|
+
if (!currentField || currentLines.length === 0) {
|
|
68
|
+
resetCurrentBlock();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const combined = currentLines.join('\n');
|
|
72
|
+
const guidMatch = combined.match(YAML_INLINE_GUID_PATTERN);
|
|
73
|
+
if (!guidMatch) {
|
|
74
|
+
resetCurrentBlock();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const guid = guidMatch[1].toLowerCase();
|
|
78
|
+
if (guidFilter && !guidFilter.has(guid)) {
|
|
79
|
+
resetCurrentBlock();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
out.push({
|
|
83
|
+
sourceType,
|
|
84
|
+
sourcePath: resourcePath,
|
|
85
|
+
line: currentGuidLine || currentStartLine || 1,
|
|
86
|
+
fieldName: currentField,
|
|
87
|
+
guid,
|
|
88
|
+
snippet: currentLines[0].trim(),
|
|
89
|
+
});
|
|
90
|
+
resetCurrentBlock();
|
|
91
|
+
};
|
|
92
|
+
const resetCurrentBlock = () => {
|
|
93
|
+
currentField = '';
|
|
94
|
+
currentStartLine = 0;
|
|
95
|
+
currentLines = [];
|
|
96
|
+
currentGuidLine = 0;
|
|
97
|
+
braceDepth = 0;
|
|
98
|
+
};
|
|
99
|
+
try {
|
|
100
|
+
for await (const line of reader) {
|
|
101
|
+
lineNumber += 1;
|
|
102
|
+
const inlineMatch = line.match(YAML_INLINE_FIELD_PATTERN);
|
|
103
|
+
if (inlineMatch) {
|
|
104
|
+
const guid = inlineMatch[2].toLowerCase();
|
|
105
|
+
if (!guidFilter || guidFilter.has(guid)) {
|
|
106
|
+
out.push({
|
|
107
|
+
sourceType,
|
|
108
|
+
sourcePath: resourcePath,
|
|
109
|
+
line: lineNumber,
|
|
110
|
+
fieldName: inlineMatch[1],
|
|
111
|
+
guid,
|
|
112
|
+
snippet: line.trim(),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (currentField) {
|
|
118
|
+
currentLines.push(line);
|
|
119
|
+
if (!currentGuidLine && YAML_INLINE_GUID_PATTERN.test(line)) {
|
|
120
|
+
currentGuidLine = lineNumber;
|
|
121
|
+
}
|
|
122
|
+
braceDepth += countChar(line, '{');
|
|
123
|
+
braceDepth -= countChar(line, '}');
|
|
124
|
+
if (braceDepth <= 0 || line.includes('}')) {
|
|
125
|
+
flushCurrentBlock();
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const openMatch = line.match(YAML_FIELD_OPEN_PATTERN);
|
|
130
|
+
if (!openMatch)
|
|
131
|
+
continue;
|
|
132
|
+
currentField = openMatch[1];
|
|
133
|
+
currentStartLine = lineNumber;
|
|
134
|
+
currentLines = [line];
|
|
135
|
+
currentGuidLine = YAML_INLINE_GUID_PATTERN.test(line) ? lineNumber : 0;
|
|
136
|
+
braceDepth = Math.max(1, countChar(line, '{') - countChar(line, '}'));
|
|
137
|
+
if (line.includes('}') || braceDepth <= 0) {
|
|
138
|
+
flushCurrentBlock();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
const code = error.code;
|
|
144
|
+
if (code === 'ENOENT' || code === 'EISDIR') {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
reader.close();
|
|
151
|
+
stream.destroy();
|
|
152
|
+
}
|
|
153
|
+
if (currentField) {
|
|
154
|
+
flushCurrentBlock();
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
async function resolveFiles(repoRoot, scopedPaths) {
|
|
159
|
+
if (!scopedPaths || scopedPaths.length === 0) {
|
|
160
|
+
return (await glob(['**/*.prefab', '**/*.asset'], {
|
|
161
|
+
cwd: repoRoot,
|
|
162
|
+
nodir: true,
|
|
163
|
+
dot: false,
|
|
164
|
+
})).sort((left, right) => left.localeCompare(right));
|
|
165
|
+
}
|
|
166
|
+
const normalized = scopedPaths
|
|
167
|
+
.filter((value) => value.endsWith('.prefab') || value.endsWith('.asset'))
|
|
168
|
+
.map((value) => normalizeRelativePath(repoRoot, value))
|
|
169
|
+
.filter((value) => value !== null)
|
|
170
|
+
.sort((left, right) => left.localeCompare(right));
|
|
171
|
+
return [...new Set(normalized)];
|
|
172
|
+
}
|
|
173
|
+
function normalizeRelativePath(repoRoot, filePath) {
|
|
174
|
+
const relativePath = path.isAbsolute(filePath) ? path.relative(repoRoot, filePath) : filePath;
|
|
175
|
+
const normalized = relativePath.replace(/\\/g, '/');
|
|
176
|
+
if (normalized.startsWith('../'))
|
|
177
|
+
return null;
|
|
178
|
+
return normalized;
|
|
179
|
+
}
|
|
180
|
+
function toGuidFilter(guidInputs) {
|
|
181
|
+
if (!guidInputs || guidInputs.length === 0)
|
|
182
|
+
return null;
|
|
183
|
+
const normalized = guidInputs
|
|
184
|
+
.map((value) => String(value || '').trim().toLowerCase())
|
|
185
|
+
.filter((value) => /^[0-9a-f]{32}$/.test(value));
|
|
186
|
+
if (normalized.length === 0)
|
|
187
|
+
return null;
|
|
188
|
+
return new Set(normalized);
|
|
189
|
+
}
|
|
190
|
+
function countChar(input, char) {
|
|
191
|
+
let count = 0;
|
|
192
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
193
|
+
if (input[i] === char)
|
|
194
|
+
count += 1;
|
|
195
|
+
}
|
|
196
|
+
return count;
|
|
197
|
+
}
|
|
198
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
199
|
+
const safeConcurrency = Math.max(1, Math.min(concurrency, items.length || 1));
|
|
200
|
+
const results = new Array(items.length);
|
|
201
|
+
let cursor = 0;
|
|
202
|
+
const workers = Array.from({ length: safeConcurrency }, async () => {
|
|
203
|
+
while (true) {
|
|
204
|
+
const index = cursor;
|
|
205
|
+
cursor += 1;
|
|
206
|
+
if (index >= items.length)
|
|
207
|
+
break;
|
|
208
|
+
results[index] = await mapper(items[index], index);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
await Promise.all(workers);
|
|
212
|
+
return results;
|
|
213
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { scanUiAssetRefs } from './ui-asset-ref-scanner.js';
|
|
8
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const fixtureRoot = path.resolve(here, '../../../src/core/unity/__fixtures__/mini-unity-ui');
|
|
10
|
+
test('scans prefab and asset VisualTreeAsset refs with evidence lines', async () => {
|
|
11
|
+
const refs = await scanUiAssetRefs({ repoRoot: fixtureRoot });
|
|
12
|
+
assert.ok(refs.some((entry) => entry.sourceType === 'prefab'
|
|
13
|
+
&& entry.sourcePath === 'Assets/Prefabs/EliteBossScreen.prefab'
|
|
14
|
+
&& entry.guid === 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
|
15
|
+
&& entry.line > 0));
|
|
16
|
+
assert.ok(refs.some((entry) => entry.sourceType === 'asset'
|
|
17
|
+
&& entry.sourcePath === 'Assets/Config/DressUpScreenConfig.asset'
|
|
18
|
+
&& entry.guid === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
|
|
19
|
+
&& entry.line > 0));
|
|
20
|
+
});
|
|
21
|
+
test('parses multiline YAML object refs and supports target guid prefilter', async () => {
|
|
22
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-ui-asset-scan-'));
|
|
23
|
+
await fs.mkdir(path.join(tempRoot, 'Assets/Prefabs'), { recursive: true });
|
|
24
|
+
await fs.writeFile(path.join(tempRoot, 'Assets/Prefabs/MultiLine.prefab'), [
|
|
25
|
+
'%YAML 1.1',
|
|
26
|
+
'--- !u!114 &11400000',
|
|
27
|
+
'MonoBehaviour:',
|
|
28
|
+
' m_ObjectHideFlags: 0',
|
|
29
|
+
' m_VisualTreeAsset: {',
|
|
30
|
+
' fileID: 9197481965408888,',
|
|
31
|
+
' guid: abcdefabcdefabcdefabcdefabcdefab,',
|
|
32
|
+
' type: 3',
|
|
33
|
+
' }',
|
|
34
|
+
].join('\n'), 'utf-8');
|
|
35
|
+
const refs = await scanUiAssetRefs({
|
|
36
|
+
repoRoot: tempRoot,
|
|
37
|
+
targetGuids: ['abcdefabcdefabcdefabcdefabcdefab'],
|
|
38
|
+
});
|
|
39
|
+
assert.equal(refs.length, 1);
|
|
40
|
+
assert.equal(refs[0].sourcePath, 'Assets/Prefabs/MultiLine.prefab');
|
|
41
|
+
assert.equal(refs[0].fieldName, 'm_VisualTreeAsset');
|
|
42
|
+
assert.equal(refs[0].guid, 'abcdefabcdefabcdefabcdefabcdefab');
|
|
43
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
44
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface UnityUiMetaIndex {
|
|
2
|
+
uxmlGuidToPath: Map<string, string>;
|
|
3
|
+
ussGuidToPath: Map<string, string>;
|
|
4
|
+
}
|
|
5
|
+
export interface BuildUnityUiMetaIndexOptions {
|
|
6
|
+
scopedPaths?: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function buildUnityUiMetaIndex(repoRoot: string, options?: BuildUnityUiMetaIndexOptions): Promise<UnityUiMetaIndex>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { glob } from 'glob';
|
|
3
|
+
import { buildAssetMetaIndex } from './meta-index.js';
|
|
4
|
+
export async function buildUnityUiMetaIndex(repoRoot, options = {}) {
|
|
5
|
+
const metaFiles = await resolveUiMetaFiles(repoRoot, options.scopedPaths);
|
|
6
|
+
if (metaFiles.length === 0) {
|
|
7
|
+
return {
|
|
8
|
+
uxmlGuidToPath: new Map(),
|
|
9
|
+
ussGuidToPath: new Map(),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
const guidToAssetPath = await buildAssetMetaIndex(repoRoot, { metaFiles });
|
|
13
|
+
const uxmlGuidToPath = new Map();
|
|
14
|
+
const ussGuidToPath = new Map();
|
|
15
|
+
for (const [guid, assetPath] of guidToAssetPath.entries()) {
|
|
16
|
+
const normalizedPath = assetPath.replace(/\\/g, '/');
|
|
17
|
+
if (normalizedPath.endsWith('.uxml')) {
|
|
18
|
+
uxmlGuidToPath.set(guid, normalizedPath);
|
|
19
|
+
uxmlGuidToPath.set(guid.toLowerCase(), normalizedPath);
|
|
20
|
+
}
|
|
21
|
+
if (normalizedPath.endsWith('.uss')) {
|
|
22
|
+
ussGuidToPath.set(guid, normalizedPath);
|
|
23
|
+
ussGuidToPath.set(guid.toLowerCase(), normalizedPath);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
uxmlGuidToPath,
|
|
28
|
+
ussGuidToPath,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async function resolveUiMetaFiles(repoRoot, scopedPaths) {
|
|
32
|
+
if (!scopedPaths || scopedPaths.length === 0) {
|
|
33
|
+
return (await glob(['**/*.uxml.meta', '**/*.uss.meta'], {
|
|
34
|
+
cwd: repoRoot,
|
|
35
|
+
nodir: true,
|
|
36
|
+
dot: false,
|
|
37
|
+
})).sort((left, right) => left.localeCompare(right));
|
|
38
|
+
}
|
|
39
|
+
const out = new Set();
|
|
40
|
+
for (const scopedPath of scopedPaths) {
|
|
41
|
+
const normalized = normalizeRelativePath(repoRoot, scopedPath);
|
|
42
|
+
if (!normalized)
|
|
43
|
+
continue;
|
|
44
|
+
if (normalized.endsWith('.uxml.meta') || normalized.endsWith('.uss.meta')) {
|
|
45
|
+
out.add(normalized);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (normalized.endsWith('.uxml') || normalized.endsWith('.uss')) {
|
|
49
|
+
out.add(`${normalized}.meta`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return [...out].sort((left, right) => left.localeCompare(right));
|
|
53
|
+
}
|
|
54
|
+
function normalizeRelativePath(repoRoot, filePath) {
|
|
55
|
+
const relativePath = path.isAbsolute(filePath) ? path.relative(repoRoot, filePath) : filePath;
|
|
56
|
+
const normalized = relativePath.replace(/\\/g, '/');
|
|
57
|
+
if (normalized.startsWith('../'))
|
|
58
|
+
return null;
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { buildUnityUiMetaIndex } from './ui-meta-index.js';
|
|
6
|
+
import { buildUnityScanContext } from './scan-context.js';
|
|
7
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const fixtureRoot = path.resolve(here, '../../../src/core/unity/__fixtures__/mini-unity-ui');
|
|
9
|
+
test('builds *.uxml.meta/*.uss.meta guid indexes', async () => {
|
|
10
|
+
const index = await buildUnityUiMetaIndex(fixtureRoot);
|
|
11
|
+
assert.equal(index.uxmlGuidToPath.get('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'), 'Assets/UI/Screens/EliteBossScreenNew.uxml');
|
|
12
|
+
assert.equal(index.ussGuidToPath.get('dddddddddddddddddddddddddddddddd'), 'Assets/UI/Styles/EliteBossScreenNew.uss');
|
|
13
|
+
});
|
|
14
|
+
test('buildUnityScanContext exposes uxml/uss guid indexes', async () => {
|
|
15
|
+
const context = await buildUnityScanContext({ repoRoot: fixtureRoot });
|
|
16
|
+
assert.equal(context.uxmlGuidToPath?.get('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'), 'Assets/UI/Screens/DressUpScreenNew.uxml');
|
|
17
|
+
assert.equal(context.ussGuidToPath?.get('eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'), 'Assets/UI/Styles/DressUpScreenNew.uss');
|
|
18
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { NODE_TABLES, REL_TYPES } from '../lbug/schema.js';
|
|
4
|
+
test('v1 ui trace does not require schema migration', () => {
|
|
5
|
+
assert.equal(NODE_TABLES.includes('Uxml'), false);
|
|
6
|
+
assert.equal(NODE_TABLES.includes('Uss'), false);
|
|
7
|
+
assert.equal(REL_TYPES.includes('UNITY_UI_TEMPLATE_REF'), false);
|
|
8
|
+
assert.equal(REL_TYPES.includes('UNITY_UI_STYLE_REF'), false);
|
|
9
|
+
assert.equal(REL_TYPES.includes('UNITY_UI_SELECTOR_BINDS'), false);
|
|
10
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { runUnityUiTrace } from './ui-trace.js';
|
|
6
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const fixtureRoot = path.resolve(here, '../../../src/core/unity/__fixtures__/mini-unity-ui');
|
|
8
|
+
test('Q1: EliteBossScreenNew TooltipBox layout file trace', async () => {
|
|
9
|
+
const out = await runUnityUiTrace({
|
|
10
|
+
repoRoot: fixtureRoot,
|
|
11
|
+
target: 'Assets/UI/Screens/EliteBossScreenNew.uxml',
|
|
12
|
+
goal: 'template_refs',
|
|
13
|
+
});
|
|
14
|
+
assert.equal(out.results.length, 1);
|
|
15
|
+
assert.equal(out.results[0].evidence_chain[1].path, 'Assets/UI/Components/TooltipBox.uxml');
|
|
16
|
+
});
|
|
17
|
+
test('Q2: DressUpScreenNew template refs trace', async () => {
|
|
18
|
+
const out = await runUnityUiTrace({
|
|
19
|
+
repoRoot: fixtureRoot,
|
|
20
|
+
target: 'Assets/UI/Screens/DressUpScreenNew.uxml',
|
|
21
|
+
goal: 'template_refs',
|
|
22
|
+
});
|
|
23
|
+
assert.equal(out.results.length, 1);
|
|
24
|
+
assert.equal(out.results[0].evidence_chain[1].path, 'Assets/UI/Components/TooltipBox.uxml');
|
|
25
|
+
});
|
|
26
|
+
test('csharp target and uxml target resolve identical answers for asset_refs', async () => {
|
|
27
|
+
const outByClass = await runUnityUiTrace({
|
|
28
|
+
repoRoot: fixtureRoot,
|
|
29
|
+
target: 'EliteBossScreenController',
|
|
30
|
+
goal: 'asset_refs',
|
|
31
|
+
});
|
|
32
|
+
const outByUxml = await runUnityUiTrace({
|
|
33
|
+
repoRoot: fixtureRoot,
|
|
34
|
+
target: 'Assets/UI/Screens/EliteBossScreenNew.uxml',
|
|
35
|
+
goal: 'asset_refs',
|
|
36
|
+
});
|
|
37
|
+
assert.deepEqual(outByClass.results, outByUxml.results);
|
|
38
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type UnityUiTraceGoal = 'asset_refs' | 'template_refs' | 'selector_bindings';
|
|
2
|
+
export type UnityUiSelectorMode = 'strict' | 'balanced';
|
|
3
|
+
export interface UnityUiTraceEvidenceHop {
|
|
4
|
+
path: string;
|
|
5
|
+
line: number;
|
|
6
|
+
snippet: string;
|
|
7
|
+
}
|
|
8
|
+
export interface UnityUiTraceResult {
|
|
9
|
+
key: string;
|
|
10
|
+
evidence_chain: UnityUiTraceEvidenceHop[];
|
|
11
|
+
score?: number;
|
|
12
|
+
confidence?: 'high' | 'medium' | 'low';
|
|
13
|
+
}
|
|
14
|
+
export interface UnityUiTraceDiagnostic {
|
|
15
|
+
code: 'ambiguous' | 'not_found';
|
|
16
|
+
message: string;
|
|
17
|
+
candidates: UnityUiTraceEvidenceHop[];
|
|
18
|
+
}
|
|
19
|
+
export interface UnityUiTraceOutput {
|
|
20
|
+
goal: UnityUiTraceGoal;
|
|
21
|
+
target: string;
|
|
22
|
+
results: UnityUiTraceResult[];
|
|
23
|
+
diagnostics: UnityUiTraceDiagnostic[];
|
|
24
|
+
}
|
|
25
|
+
export interface UnityUiTraceInput {
|
|
26
|
+
repoRoot: string;
|
|
27
|
+
target: string;
|
|
28
|
+
goal: UnityUiTraceGoal;
|
|
29
|
+
selectorMode?: UnityUiSelectorMode;
|
|
30
|
+
}
|
|
31
|
+
export declare function runUnityUiTrace(input: UnityUiTraceInput): Promise<UnityUiTraceOutput>;
|