@veewo/gitnexus 1.3.8 → 1.3.10
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/README.md +5 -0
- package/dist/benchmark/runner.test.js +1 -1
- package/dist/benchmark/u2-e2e/analyze-parser.d.ts +22 -0
- package/dist/benchmark/u2-e2e/analyze-parser.js +89 -0
- package/dist/benchmark/u2-e2e/analyze-parser.test.js +13 -0
- package/dist/benchmark/u2-e2e/characterlist-assetref.d.ts +19 -0
- package/dist/benchmark/u2-e2e/characterlist-assetref.js +80 -0
- package/dist/benchmark/u2-e2e/characterlist-assetref.test.js +108 -0
- package/dist/benchmark/u2-e2e/config.d.ts +25 -0
- package/dist/benchmark/u2-e2e/config.js +86 -0
- package/dist/benchmark/u2-e2e/config.test.js +29 -0
- package/dist/benchmark/u2-e2e/metrics.d.ts +20 -0
- package/dist/benchmark/u2-e2e/metrics.js +34 -0
- package/dist/benchmark/u2-e2e/metrics.test.js +13 -0
- package/dist/benchmark/u2-e2e/neonspark-full-e2e.d.ts +33 -0
- package/dist/benchmark/u2-e2e/neonspark-full-e2e.js +439 -0
- package/dist/benchmark/u2-e2e/neonspark-full-e2e.test.js +40 -0
- package/dist/benchmark/u2-e2e/report.d.ts +58 -0
- package/dist/benchmark/u2-e2e/report.js +130 -0
- package/dist/benchmark/u2-e2e/report.test.js +58 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.d.ts +21 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.js +166 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.test.d.ts +1 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.test.js +145 -0
- package/dist/benchmark/u2-performance-sampler.d.ts +33 -0
- package/dist/benchmark/u2-performance-sampler.js +178 -0
- package/dist/benchmark/u2-performance-sampler.test.d.ts +1 -0
- package/dist/benchmark/u2-performance-sampler.test.js +34 -0
- package/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze-multi-scope-regression.test.js +10 -0
- package/dist/cli/analyze-options.d.ts +19 -0
- package/dist/cli/analyze-options.js +35 -0
- package/dist/cli/analyze-options.test.js +42 -1
- package/dist/cli/analyze-summary.d.ts +7 -0
- package/dist/cli/analyze-summary.js +37 -0
- package/dist/cli/analyze-summary.test.d.ts +1 -0
- package/dist/cli/analyze-summary.test.js +58 -0
- package/dist/cli/analyze.d.ts +1 -0
- package/dist/cli/analyze.js +64 -32
- package/dist/cli/benchmark-u2-e2e.d.ts +9 -0
- package/dist/cli/benchmark-u2-e2e.js +35 -0
- package/dist/cli/benchmark-u2-e2e.test.d.ts +1 -0
- package/dist/cli/benchmark-u2-e2e.test.js +7 -0
- package/dist/cli/index.js +21 -0
- package/dist/cli/repo-manager-alias.test.js +24 -1
- package/dist/cli/tool.d.ts +3 -0
- package/dist/cli/tool.js +2 -0
- package/dist/cli/unity-bindings.d.ts +8 -0
- package/dist/cli/unity-bindings.js +33 -0
- package/dist/cli/unity-bindings.test.d.ts +1 -0
- package/dist/cli/unity-bindings.test.js +24 -0
- package/dist/core/graph/types.d.ts +1 -1
- package/dist/core/ingestion/pipeline.d.ts +2 -4
- package/dist/core/ingestion/pipeline.js +12 -0
- package/dist/core/ingestion/unity-resource-processor.d.ts +26 -0
- package/dist/core/ingestion/unity-resource-processor.js +363 -0
- package/dist/core/ingestion/unity-resource-processor.test.d.ts +1 -0
- package/dist/core/ingestion/unity-resource-processor.test.js +599 -0
- package/dist/core/kuzu/kuzu-adapter.d.ts +6 -0
- package/dist/core/kuzu/kuzu-adapter.js +18 -7
- package/dist/core/kuzu/schema.d.ts +2 -2
- package/dist/core/kuzu/schema.js +22 -1
- package/dist/core/kuzu/schema.test.d.ts +1 -0
- package/dist/core/kuzu/schema.test.js +17 -0
- package/dist/core/unity/meta-index.d.ts +5 -0
- package/dist/core/unity/meta-index.js +113 -0
- package/dist/core/unity/meta-index.test.d.ts +1 -0
- package/dist/core/unity/meta-index.test.js +11 -0
- package/dist/core/unity/options.d.ts +2 -0
- package/dist/core/unity/options.js +9 -0
- package/dist/core/unity/options.test.d.ts +1 -0
- package/dist/core/unity/options.test.js +10 -0
- package/dist/core/unity/override-merger.d.ts +27 -0
- package/dist/core/unity/override-merger.js +35 -0
- package/dist/core/unity/override-merger.test.d.ts +1 -0
- package/dist/core/unity/override-merger.test.js +47 -0
- package/dist/core/unity/resolver.d.ts +79 -0
- package/dist/core/unity/resolver.js +384 -0
- package/dist/core/unity/resolver.test.d.ts +1 -0
- package/dist/core/unity/resolver.test.js +244 -0
- package/dist/core/unity/resource-hit-scanner.d.ts +10 -0
- package/dist/core/unity/resource-hit-scanner.js +60 -0
- package/dist/core/unity/resource-hit-scanner.test.d.ts +1 -0
- package/dist/core/unity/resource-hit-scanner.test.js +20 -0
- package/dist/core/unity/scan-context.d.ts +23 -0
- package/dist/core/unity/scan-context.js +318 -0
- package/dist/core/unity/scan-context.test.d.ts +1 -0
- package/dist/core/unity/scan-context.test.js +118 -0
- package/dist/core/unity/serialized-type-index.d.ts +10 -0
- package/dist/core/unity/serialized-type-index.js +105 -0
- package/dist/core/unity/serialized-type-index.test.d.ts +1 -0
- package/dist/core/unity/serialized-type-index.test.js +34 -0
- package/dist/core/unity/u2-thresholds.test.d.ts +1 -0
- package/dist/core/unity/u2-thresholds.test.js +71 -0
- package/dist/core/unity/yaml-object-graph.d.ts +9 -0
- package/dist/core/unity/yaml-object-graph.js +92 -0
- package/dist/core/unity/yaml-object-graph.test.d.ts +1 -0
- package/dist/core/unity/yaml-object-graph.test.js +49 -0
- package/dist/mcp/local/local-backend.js +12 -1
- package/dist/mcp/local/unity-enrichment.d.ts +6 -0
- package/dist/mcp/local/unity-enrichment.js +91 -0
- package/dist/mcp/local/unity-enrichment.test.d.ts +1 -0
- package/dist/mcp/local/unity-enrichment.test.js +130 -0
- package/dist/mcp/resources.js +1 -1
- package/dist/mcp/staleness.js +1 -1
- package/dist/mcp/tools.js +12 -0
- package/dist/storage/repo-manager.d.ts +6 -0
- package/dist/types/pipeline.d.ts +7 -0
- package/dist/types/pipeline.js +2 -0
- package/hooks/check-release-path-hygiene.mjs +108 -0
- package/package.json +16 -9
- package/dist/cli/analyze-custom-modules-regression.test.js +0 -75
- package/dist/cli/analyze-modules-diagnostics.test.js +0 -36
- package/dist/core/ingestion/modules/assignment-engine.d.ts +0 -33
- package/dist/core/ingestion/modules/assignment-engine.js +0 -179
- package/dist/core/ingestion/modules/assignment-engine.test.js +0 -111
- package/dist/core/ingestion/modules/config-loader.d.ts +0 -2
- package/dist/core/ingestion/modules/config-loader.js +0 -186
- package/dist/core/ingestion/modules/config-loader.test.js +0 -57
- package/dist/core/ingestion/modules/rule-matcher.d.ts +0 -12
- package/dist/core/ingestion/modules/rule-matcher.js +0 -63
- package/dist/core/ingestion/modules/rule-matcher.test.js +0 -58
- package/dist/core/ingestion/modules/types.d.ts +0 -44
- package/dist/core/ingestion/modules/types.js +0 -2
- package/dist/mcp/local/cluster-aggregation.d.ts +0 -20
- package/dist/mcp/local/cluster-aggregation.js +0 -48
- package/dist/mcp/local/cluster-aggregation.test.js +0 -22
- /package/dist/{cli/analyze-custom-modules-regression.test.d.ts → benchmark/u2-e2e/analyze-parser.test.d.ts} +0 -0
- /package/dist/{cli/analyze-modules-diagnostics.test.d.ts → benchmark/u2-e2e/characterlist-assetref.test.d.ts} +0 -0
- /package/dist/{core/ingestion/modules/assignment-engine.test.d.ts → benchmark/u2-e2e/config.test.d.ts} +0 -0
- /package/dist/{core/ingestion/modules/config-loader.test.d.ts → benchmark/u2-e2e/metrics.test.d.ts} +0 -0
- /package/dist/{core/ingestion/modules/rule-matcher.test.d.ts → benchmark/u2-e2e/neonspark-full-e2e.test.d.ts} +0 -0
- /package/dist/{mcp/local/cluster-aggregation.test.d.ts → benchmark/u2-e2e/report.test.d.ts} +0 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import { buildMetaIndex } from './meta-index.js';
|
|
5
|
+
import { mergeOverrideChain } from './override-merger.js';
|
|
6
|
+
import { findGuidHits } from './resource-hit-scanner.js';
|
|
7
|
+
import { parseUnityYamlObjects } from './yaml-object-graph.js';
|
|
8
|
+
export async function resolveUnityBindings(input) {
|
|
9
|
+
const scriptPath = await resolveSymbolScriptPath(input.repoRoot, input.symbol, input.scanContext);
|
|
10
|
+
const scriptGuid = await resolveScriptGuid(input.repoRoot, scriptPath, input.scanContext);
|
|
11
|
+
const hits = input.scanContext
|
|
12
|
+
? (input.scanContext.guidToResourceHits.get(scriptGuid) ?? [])
|
|
13
|
+
: await findGuidHits(input.repoRoot, scriptGuid);
|
|
14
|
+
const resourceBindings = [];
|
|
15
|
+
const unityDiagnostics = [];
|
|
16
|
+
for (const hit of hits) {
|
|
17
|
+
const blocks = await getResourceBlocks(input.repoRoot, hit.resourcePath, input.scanContext);
|
|
18
|
+
const matchedComponents = blocks.filter((block) => block.objectType === 'MonoBehaviour' && block.fields.m_Script?.includes(scriptGuid));
|
|
19
|
+
if (matchedComponents.length === 0) {
|
|
20
|
+
unityDiagnostics.push(`No MonoBehaviour block matched script guid ${scriptGuid} in ${hit.resourcePath}.`);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
for (const block of matchedComponents) {
|
|
24
|
+
const resolved = resolveBindingForComponent(block, blocks, hit, input.scanContext);
|
|
25
|
+
resourceBindings.push({
|
|
26
|
+
resourcePath: hit.resourcePath,
|
|
27
|
+
resourceType: hit.resourceType,
|
|
28
|
+
bindingKind: resolved.bindingKind,
|
|
29
|
+
componentObjectId: block.objectId,
|
|
30
|
+
evidence: {
|
|
31
|
+
line: hit.line,
|
|
32
|
+
lineText: hit.lineText,
|
|
33
|
+
},
|
|
34
|
+
serializedFields: resolved.serializedFields,
|
|
35
|
+
resolvedReferences: resolved.resolvedReferences,
|
|
36
|
+
assetRefPaths: extractAssetRefPathReferences(resolved.serializedFields),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
symbol: input.symbol,
|
|
42
|
+
scriptPath,
|
|
43
|
+
scriptGuid,
|
|
44
|
+
resourceBindings,
|
|
45
|
+
serializedFields: aggregateSerializedFields(resourceBindings),
|
|
46
|
+
unityDiagnostics,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function hasCoverage(resultSet) {
|
|
50
|
+
return {
|
|
51
|
+
hasScalar: resultSet.some((result) => result.serializedFields.scalarFields.length > 0),
|
|
52
|
+
hasReference: resultSet.some((result) => result.serializedFields.referenceFields.length > 0),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function resolveSymbolScriptPath(repoRoot, symbol, scanContext) {
|
|
56
|
+
const contextHit = scanContext?.symbolToScriptPath.get(symbol);
|
|
57
|
+
if (contextHit) {
|
|
58
|
+
return normalizePath(contextHit);
|
|
59
|
+
}
|
|
60
|
+
const scriptFiles = (await glob('**/*.cs', {
|
|
61
|
+
cwd: repoRoot,
|
|
62
|
+
nodir: true,
|
|
63
|
+
dot: false,
|
|
64
|
+
})).sort((left, right) => left.localeCompare(right));
|
|
65
|
+
const basenameMatches = scriptFiles.filter((filePath) => path.basename(filePath, '.cs') === symbol);
|
|
66
|
+
if (basenameMatches.length === 1) {
|
|
67
|
+
return normalizePath(basenameMatches[0]);
|
|
68
|
+
}
|
|
69
|
+
const symbolRegex = new RegExp(`\\b(class|struct|interface)\\s+${escapeRegex(symbol)}\\b`);
|
|
70
|
+
const contentMatches = [];
|
|
71
|
+
for (const filePath of scriptFiles) {
|
|
72
|
+
const content = await fs.readFile(path.join(repoRoot, filePath), 'utf-8');
|
|
73
|
+
if (symbolRegex.test(content)) {
|
|
74
|
+
contentMatches.push(normalizePath(filePath));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (contentMatches.length === 1) {
|
|
78
|
+
return contentMatches[0];
|
|
79
|
+
}
|
|
80
|
+
if (contentMatches.length > 1 || basenameMatches.length > 1) {
|
|
81
|
+
throw new Error(`Unity symbol "${symbol}" is ambiguous.`);
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`Unity symbol "${symbol}" was not found under ${repoRoot}.`);
|
|
84
|
+
}
|
|
85
|
+
async function resolveScriptGuid(repoRoot, scriptPath, scanContext) {
|
|
86
|
+
const contextGuid = scanContext?.scriptPathToGuid.get(normalizePath(scriptPath));
|
|
87
|
+
if (contextGuid) {
|
|
88
|
+
return contextGuid;
|
|
89
|
+
}
|
|
90
|
+
const metaIndex = await buildMetaIndex(repoRoot);
|
|
91
|
+
for (const [guid, indexedScriptPath] of metaIndex.entries()) {
|
|
92
|
+
if (normalizePath(indexedScriptPath) === normalizePath(scriptPath)) {
|
|
93
|
+
return guid;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`No .meta guid found for ${scriptPath}.`);
|
|
97
|
+
}
|
|
98
|
+
async function getResourceBlocks(repoRoot, resourcePath, scanContext) {
|
|
99
|
+
const normalizedResourcePath = normalizePath(resourcePath);
|
|
100
|
+
const cached = scanContext?.resourceDocCache.get(normalizedResourcePath);
|
|
101
|
+
if (cached) {
|
|
102
|
+
return cached;
|
|
103
|
+
}
|
|
104
|
+
const absoluteResourcePath = path.join(repoRoot, normalizedResourcePath);
|
|
105
|
+
const raw = await fs.readFile(absoluteResourcePath, 'utf-8');
|
|
106
|
+
const blocks = parseUnityYamlObjects(raw);
|
|
107
|
+
scanContext?.resourceDocCache.set(normalizedResourcePath, blocks);
|
|
108
|
+
return blocks;
|
|
109
|
+
}
|
|
110
|
+
function resolveBindingForComponent(componentBlock, blocks, hit, scanContext) {
|
|
111
|
+
const directLayer = createLayerFromFields(componentBlock.fields, baseLayerName(hit.resourceType));
|
|
112
|
+
const layers = [directLayer];
|
|
113
|
+
let bindingKind = inferBindingKind(componentBlock, hit.resourceType);
|
|
114
|
+
const prefabInstanceId = extractFileId(componentBlock.fields.m_PrefabInstance);
|
|
115
|
+
if (prefabInstanceId) {
|
|
116
|
+
const prefabInstanceBlock = blocks.find((block) => block.objectType === 'PrefabInstance' && block.objectId === prefabInstanceId);
|
|
117
|
+
if (prefabInstanceBlock?.fields.m_Modification) {
|
|
118
|
+
const modificationLayer = createLayerFromModification(prefabInstanceBlock.fields.m_Modification, componentBlock.objectId, hit.resourceType === 'scene' ? 'scene' : 'prefab-instance');
|
|
119
|
+
if (modificationLayer) {
|
|
120
|
+
layers.push(modificationLayer);
|
|
121
|
+
if (hit.resourceType === 'scene') {
|
|
122
|
+
bindingKind = 'scene-override';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const merged = mergeOverrideChain(layers);
|
|
128
|
+
return {
|
|
129
|
+
bindingKind,
|
|
130
|
+
serializedFields: toSerializedFields(merged),
|
|
131
|
+
resolvedReferences: toResolvedReferences(merged, blocks, hit.resourcePath, scanContext?.assetGuidToPath),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function createLayerFromFields(fields, sourceLayer) {
|
|
135
|
+
const scalarFields = {};
|
|
136
|
+
const referenceFields = {};
|
|
137
|
+
for (const [name, rawValue] of Object.entries(fields)) {
|
|
138
|
+
if (name.startsWith('m_'))
|
|
139
|
+
continue;
|
|
140
|
+
const reference = parseObjectReference(rawValue);
|
|
141
|
+
if (reference) {
|
|
142
|
+
referenceFields[name] = reference;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
scalarFields[name] = {
|
|
146
|
+
value: rawValue.trim(),
|
|
147
|
+
valueType: inferValueType(rawValue),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return { sourceLayer, scalarFields, referenceFields };
|
|
151
|
+
}
|
|
152
|
+
function createLayerFromModification(modificationBody, targetObjectId, sourceLayer) {
|
|
153
|
+
const scalarFields = {};
|
|
154
|
+
const referenceFields = {};
|
|
155
|
+
const entryPattern = /-\s*target:\s*\{fileID:\s*(\d+)[^}]*\}\s*\n\s*propertyPath:\s*([^\n]+)\s*\n\s*value:\s*([^\n]*)\s*\n\s*objectReference:\s*(\{[^\n]*\})/g;
|
|
156
|
+
let match = entryPattern.exec(modificationBody);
|
|
157
|
+
while (match) {
|
|
158
|
+
const [, fileId, propertyPath, rawValue, rawObjectReference] = match;
|
|
159
|
+
if (fileId === targetObjectId && !propertyPath.startsWith('m_')) {
|
|
160
|
+
const normalizedValue = rawValue.trim();
|
|
161
|
+
if (normalizedValue.length > 0) {
|
|
162
|
+
scalarFields[propertyPath] = {
|
|
163
|
+
value: normalizedValue,
|
|
164
|
+
valueType: inferValueType(normalizedValue),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const reference = parseObjectReference(rawObjectReference);
|
|
169
|
+
if (reference) {
|
|
170
|
+
referenceFields[propertyPath] = reference;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
match = entryPattern.exec(modificationBody);
|
|
175
|
+
}
|
|
176
|
+
if (Object.keys(scalarFields).length === 0 && Object.keys(referenceFields).length === 0) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
return { sourceLayer, scalarFields, referenceFields };
|
|
180
|
+
}
|
|
181
|
+
function toSerializedFields(merged) {
|
|
182
|
+
return {
|
|
183
|
+
scalarFields: Object.values(merged.scalarFields).sort((left, right) => left.name.localeCompare(right.name)),
|
|
184
|
+
referenceFields: Object.values(merged.referenceFields).sort((left, right) => left.name.localeCompare(right.name)),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const ASSET_REF_FIELD_RE = /^\s*([A-Za-z0-9_]*Ref):\s*$/;
|
|
188
|
+
const RELATIVE_PATH_RE = /^\s*_relativePath:\s*(.*)$/;
|
|
189
|
+
function unquote(value) {
|
|
190
|
+
return value.replace(/^"|"$/g, '');
|
|
191
|
+
}
|
|
192
|
+
function isSpriteRelativePath(value) {
|
|
193
|
+
const normalized = value.trim().toLowerCase();
|
|
194
|
+
if (!normalized)
|
|
195
|
+
return false;
|
|
196
|
+
if (normalized.includes('/sprites/'))
|
|
197
|
+
return true;
|
|
198
|
+
return /\.(png|jpg|jpeg|tga|psd|webp|spriteatlas|spriteatlasv2)$/.test(normalized);
|
|
199
|
+
}
|
|
200
|
+
export function extractAssetRefPathReferences(serializedFields) {
|
|
201
|
+
const refs = [];
|
|
202
|
+
for (const scalarField of serializedFields.scalarFields) {
|
|
203
|
+
const text = String(scalarField.value || '');
|
|
204
|
+
if (!text)
|
|
205
|
+
continue;
|
|
206
|
+
let currentFieldName = scalarField.name;
|
|
207
|
+
const lines = text.split(/\r?\n/);
|
|
208
|
+
for (const line of lines) {
|
|
209
|
+
const fieldMatch = line.match(ASSET_REF_FIELD_RE);
|
|
210
|
+
if (fieldMatch) {
|
|
211
|
+
currentFieldName = fieldMatch[1];
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const relativeMatch = line.match(RELATIVE_PATH_RE);
|
|
215
|
+
if (!relativeMatch) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const relativePath = unquote((relativeMatch[1] || '').trim());
|
|
219
|
+
refs.push({
|
|
220
|
+
parentFieldName: scalarField.name,
|
|
221
|
+
fieldName: currentFieldName,
|
|
222
|
+
relativePath,
|
|
223
|
+
sourceLayer: scalarField.sourceLayer || 'unknown',
|
|
224
|
+
isEmpty: relativePath.length === 0,
|
|
225
|
+
isSprite: isSpriteRelativePath(relativePath),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return refs;
|
|
230
|
+
}
|
|
231
|
+
function aggregateSerializedFields(resourceBindings) {
|
|
232
|
+
return {
|
|
233
|
+
scalarFields: resourceBindings.flatMap((binding) => binding.serializedFields.scalarFields),
|
|
234
|
+
referenceFields: resourceBindings.flatMap((binding) => binding.serializedFields.referenceFields),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function toResolvedReferences(merged, blocks, resourcePath, assetGuidToPath) {
|
|
238
|
+
const references = [];
|
|
239
|
+
const blocksById = new Map();
|
|
240
|
+
for (const block of blocks) {
|
|
241
|
+
blocksById.set(block.objectId, block);
|
|
242
|
+
}
|
|
243
|
+
for (const reference of Object.values(merged.referenceFields)) {
|
|
244
|
+
references.push(resolveReferenceCandidate({
|
|
245
|
+
fieldName: reference.name,
|
|
246
|
+
sourceLayer: reference.sourceLayer,
|
|
247
|
+
fileId: reference.fileId,
|
|
248
|
+
guid: reference.guid,
|
|
249
|
+
fromList: false,
|
|
250
|
+
}, blocksById, resourcePath, assetGuidToPath));
|
|
251
|
+
}
|
|
252
|
+
for (const scalar of Object.values(merged.scalarFields)) {
|
|
253
|
+
const candidates = parseListReferenceCandidates(scalar.name, scalar.sourceLayer, scalar.value);
|
|
254
|
+
references.push(...candidates.map((candidate) => resolveReferenceCandidate(candidate, blocksById, resourcePath, assetGuidToPath)));
|
|
255
|
+
}
|
|
256
|
+
return references;
|
|
257
|
+
}
|
|
258
|
+
function parseListReferenceCandidates(fieldName, sourceLayer, rawValue) {
|
|
259
|
+
const trimmed = rawValue.trim();
|
|
260
|
+
if (!trimmed.startsWith('-'))
|
|
261
|
+
return [];
|
|
262
|
+
const candidates = [];
|
|
263
|
+
const lines = trimmed.split(/\r?\n/);
|
|
264
|
+
let listIndex = 0;
|
|
265
|
+
for (const line of lines) {
|
|
266
|
+
const lineTrimmed = line.trim();
|
|
267
|
+
if (!lineTrimmed.startsWith('-'))
|
|
268
|
+
continue;
|
|
269
|
+
const entryMatch = lineTrimmed.match(/^-\s*(\{.*\})\s*$/);
|
|
270
|
+
if (!entryMatch)
|
|
271
|
+
continue;
|
|
272
|
+
const parsed = parseObjectReference(entryMatch[1]);
|
|
273
|
+
if (!parsed)
|
|
274
|
+
continue;
|
|
275
|
+
candidates.push({
|
|
276
|
+
fieldName,
|
|
277
|
+
sourceLayer,
|
|
278
|
+
fileId: parsed.fileId,
|
|
279
|
+
guid: parsed.guid,
|
|
280
|
+
fromList: true,
|
|
281
|
+
listIndex,
|
|
282
|
+
});
|
|
283
|
+
listIndex += 1;
|
|
284
|
+
}
|
|
285
|
+
return candidates;
|
|
286
|
+
}
|
|
287
|
+
function resolveReferenceCandidate(candidate, blocksById, resourcePath, assetGuidToPath) {
|
|
288
|
+
const fileId = candidate.fileId;
|
|
289
|
+
const guid = candidate.guid;
|
|
290
|
+
const normalizedGuid = guid ? guid.toLowerCase() : undefined;
|
|
291
|
+
if (fileId === '0') {
|
|
292
|
+
return {
|
|
293
|
+
...candidate,
|
|
294
|
+
resolution: 'null',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (normalizedGuid && !isBuiltInGuid(normalizedGuid)) {
|
|
298
|
+
const assetPath = assetGuidToPath?.get(normalizedGuid) || assetGuidToPath?.get(guid);
|
|
299
|
+
return {
|
|
300
|
+
...candidate,
|
|
301
|
+
resolution: assetPath ? 'external-asset' : 'unresolved',
|
|
302
|
+
target: assetPath ? { assetPath } : undefined,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (fileId) {
|
|
306
|
+
const targetBlock = blocksById.get(fileId);
|
|
307
|
+
if (targetBlock) {
|
|
308
|
+
return {
|
|
309
|
+
...candidate,
|
|
310
|
+
resolution: 'local-object',
|
|
311
|
+
target: {
|
|
312
|
+
resourcePath,
|
|
313
|
+
objectId: targetBlock.objectId,
|
|
314
|
+
objectType: targetBlock.objectType,
|
|
315
|
+
gameObjectName: resolveGameObjectName(targetBlock, blocksById),
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
...candidate,
|
|
322
|
+
resolution: 'unresolved',
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function resolveGameObjectName(block, blocksById) {
|
|
326
|
+
if (block.objectType === 'GameObject') {
|
|
327
|
+
return block.fields.m_Name?.trim() || undefined;
|
|
328
|
+
}
|
|
329
|
+
const gameObjectRef = parseObjectReference(block.fields.m_GameObject || '');
|
|
330
|
+
const gameObjectId = gameObjectRef?.fileId;
|
|
331
|
+
if (!gameObjectId)
|
|
332
|
+
return undefined;
|
|
333
|
+
const gameObjectBlock = blocksById.get(gameObjectId);
|
|
334
|
+
if (!gameObjectBlock || gameObjectBlock.objectType !== 'GameObject')
|
|
335
|
+
return undefined;
|
|
336
|
+
return gameObjectBlock.fields.m_Name?.trim() || undefined;
|
|
337
|
+
}
|
|
338
|
+
function isBuiltInGuid(guid) {
|
|
339
|
+
return /^0+$/.test(guid);
|
|
340
|
+
}
|
|
341
|
+
function inferBindingKind(componentBlock, resourceType) {
|
|
342
|
+
if (componentBlock.stripped && resourceType === 'scene')
|
|
343
|
+
return 'scene-override';
|
|
344
|
+
if (componentBlock.stripped)
|
|
345
|
+
return 'nested';
|
|
346
|
+
if (componentBlock.fields.m_PrefabInstance)
|
|
347
|
+
return 'prefab-instance';
|
|
348
|
+
return 'direct';
|
|
349
|
+
}
|
|
350
|
+
function baseLayerName(resourceType) {
|
|
351
|
+
if (resourceType === 'scene')
|
|
352
|
+
return 'scene';
|
|
353
|
+
if (resourceType === 'asset')
|
|
354
|
+
return 'asset';
|
|
355
|
+
return 'prefab';
|
|
356
|
+
}
|
|
357
|
+
function extractFileId(rawValue) {
|
|
358
|
+
if (!rawValue)
|
|
359
|
+
return undefined;
|
|
360
|
+
return rawValue.match(/fileID:\s*(\d+)/)?.[1];
|
|
361
|
+
}
|
|
362
|
+
function parseObjectReference(rawValue) {
|
|
363
|
+
const trimmed = rawValue.trim();
|
|
364
|
+
if (!trimmed.startsWith('{') || !trimmed.includes('fileID:')) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
const fileId = trimmed.match(/fileID:\s*(-?\d+)/)?.[1];
|
|
368
|
+
const guid = trimmed.match(/guid:\s*([0-9a-f]{32})/i)?.[1];
|
|
369
|
+
return { fileId, guid };
|
|
370
|
+
}
|
|
371
|
+
function inferValueType(rawValue) {
|
|
372
|
+
const trimmed = rawValue.trim();
|
|
373
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed))
|
|
374
|
+
return 'number';
|
|
375
|
+
if (/^(true|false)$/i.test(trimmed))
|
|
376
|
+
return 'boolean';
|
|
377
|
+
return 'string';
|
|
378
|
+
}
|
|
379
|
+
function normalizePath(filePath) {
|
|
380
|
+
return filePath.replace(/\\/g, '/');
|
|
381
|
+
}
|
|
382
|
+
function escapeRegex(value) {
|
|
383
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
384
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { buildUnityScanContext } from './scan-context.js';
|
|
7
|
+
import { extractAssetRefPathReferences, hasCoverage, resolveUnityBindings } from './resolver.js';
|
|
8
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const fixtureRoot = path.resolve(here, '../../../src/core/unity/__fixtures__/mini-unity');
|
|
10
|
+
const requiredSamples = ['Global', 'BattleMode', 'PlayerActor', 'MainUIManager'];
|
|
11
|
+
const acceptanceBaseline = {
|
|
12
|
+
Global: {
|
|
13
|
+
expectedBindingKinds: ['direct'],
|
|
14
|
+
minScalarFields: 1,
|
|
15
|
+
minReferenceFields: 0,
|
|
16
|
+
requiredScalarFields: ['needPause'],
|
|
17
|
+
requiredReferenceFields: [],
|
|
18
|
+
},
|
|
19
|
+
BattleMode: {
|
|
20
|
+
expectedBindingKinds: ['direct'],
|
|
21
|
+
minScalarFields: 1,
|
|
22
|
+
minReferenceFields: 1,
|
|
23
|
+
requiredScalarFields: ['battleState'],
|
|
24
|
+
requiredReferenceFields: ['uiDocument'],
|
|
25
|
+
},
|
|
26
|
+
PlayerActor: {
|
|
27
|
+
expectedBindingKinds: ['direct'],
|
|
28
|
+
minScalarFields: 1,
|
|
29
|
+
minReferenceFields: 1,
|
|
30
|
+
requiredScalarFields: ['walkSpeed'],
|
|
31
|
+
requiredReferenceFields: ['animatorController'],
|
|
32
|
+
},
|
|
33
|
+
MainUIManager: {
|
|
34
|
+
expectedBindingKinds: ['scene-override'],
|
|
35
|
+
minScalarFields: 1,
|
|
36
|
+
minReferenceFields: 1,
|
|
37
|
+
requiredScalarFields: ['needPause'],
|
|
38
|
+
requiredReferenceFields: ['mainUIDocument'],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
test('resolveUnityBindings matches frozen acceptance baseline for required Unity samples', async () => {
|
|
42
|
+
const results = await Promise.all(requiredSamples.map((symbol) => resolveUnityBindings({ repoRoot: fixtureRoot, symbol })));
|
|
43
|
+
for (const result of results) {
|
|
44
|
+
const baseline = acceptanceBaseline[result.symbol];
|
|
45
|
+
assert.ok(baseline, `Missing acceptance baseline for ${result.symbol}`);
|
|
46
|
+
const bindingKinds = Array.from(new Set(result.resourceBindings.map((binding) => binding.bindingKind))).sort();
|
|
47
|
+
assert.ok(result.resourceBindings.length >= 1, `${result.symbol} should have at least one resource binding`);
|
|
48
|
+
assert.deepEqual(bindingKinds, [...baseline.expectedBindingKinds].sort(), `${result.symbol} binding kinds changed`);
|
|
49
|
+
assert.ok(result.serializedFields.scalarFields.length >= baseline.minScalarFields, `${result.symbol} scalar field count below baseline`);
|
|
50
|
+
assert.ok(result.serializedFields.referenceFields.length >= baseline.minReferenceFields, `${result.symbol} reference field count below baseline`);
|
|
51
|
+
const scalarNames = new Set(result.serializedFields.scalarFields.map((field) => field.name));
|
|
52
|
+
const referenceNames = new Set(result.serializedFields.referenceFields.map((field) => field.name));
|
|
53
|
+
for (const fieldName of baseline.requiredScalarFields) {
|
|
54
|
+
assert.ok(scalarNames.has(fieldName), `${result.symbol} missing scalar field ${fieldName}`);
|
|
55
|
+
}
|
|
56
|
+
for (const fieldName of baseline.requiredReferenceFields) {
|
|
57
|
+
assert.ok(referenceNames.has(fieldName), `${result.symbol} missing reference field ${fieldName}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
assert.deepEqual(hasCoverage(results), { hasScalar: true, hasReference: true });
|
|
61
|
+
});
|
|
62
|
+
test('resolveUnityBindings applies PrefabInstance modifications for stripped scene components', async () => {
|
|
63
|
+
const result = await resolveUnityBindings({ repoRoot: fixtureRoot, symbol: 'MainUIManager' });
|
|
64
|
+
const needPause = result.serializedFields.scalarFields.find((field) => field.name === 'needPause');
|
|
65
|
+
const uiDocument = result.serializedFields.referenceFields.find((field) => field.name === 'mainUIDocument');
|
|
66
|
+
assert.equal(result.resourceBindings[0]?.bindingKind, 'scene-override');
|
|
67
|
+
assert.equal(needPause?.value, '1');
|
|
68
|
+
assert.equal(needPause?.sourceLayer, 'scene');
|
|
69
|
+
assert.equal(uiDocument?.guid, '44444444444444444444444444444444');
|
|
70
|
+
assert.equal(uiDocument?.sourceLayer, 'scene');
|
|
71
|
+
});
|
|
72
|
+
test('resolveUnityBindings uses provided scan context without repo re-scan', async () => {
|
|
73
|
+
const context = await buildUnityScanContext({ repoRoot: fixtureRoot });
|
|
74
|
+
const result = await resolveUnityBindings({ repoRoot: fixtureRoot, symbol: 'MainUIManager', scanContext: context });
|
|
75
|
+
assert.ok(result.resourceBindings.length > 0);
|
|
76
|
+
});
|
|
77
|
+
test('resource YAML parse is reused across symbols sharing same resource file', async (t) => {
|
|
78
|
+
const context = await buildUnityScanContext({ repoRoot: fixtureRoot });
|
|
79
|
+
const scriptPath = context.symbolToScriptPath.get('Global');
|
|
80
|
+
assert.ok(scriptPath);
|
|
81
|
+
context.symbolToScriptPath.set('GlobalAlias', scriptPath);
|
|
82
|
+
const scriptGuid = context.scriptPathToGuid.get(scriptPath);
|
|
83
|
+
assert.ok(scriptGuid);
|
|
84
|
+
const targetResourcePath = context.guidToResourceHits.get(scriptGuid)?.[0]?.resourcePath;
|
|
85
|
+
assert.ok(targetResourcePath);
|
|
86
|
+
const originalReadFile = fs.readFile.bind(fs);
|
|
87
|
+
let targetResourceReadCount = 0;
|
|
88
|
+
t.mock.method(fs, 'readFile', async (...args) => {
|
|
89
|
+
const fileArg = args[0];
|
|
90
|
+
const rawPath = typeof fileArg === 'string' ? fileArg : fileArg instanceof URL ? fileArg.pathname : String(fileArg);
|
|
91
|
+
const normalizedPath = rawPath.replace(/\\/g, '/');
|
|
92
|
+
if (normalizedPath.endsWith(targetResourcePath)) {
|
|
93
|
+
targetResourceReadCount += 1;
|
|
94
|
+
}
|
|
95
|
+
return originalReadFile(...args);
|
|
96
|
+
});
|
|
97
|
+
const first = await resolveUnityBindings({ repoRoot: fixtureRoot, symbol: 'Global', scanContext: context });
|
|
98
|
+
const second = await resolveUnityBindings({ repoRoot: fixtureRoot, symbol: 'GlobalAlias', scanContext: context });
|
|
99
|
+
assert.ok(first.resourceBindings.length > 0);
|
|
100
|
+
assert.ok(second.resourceBindings.length > 0);
|
|
101
|
+
assert.equal(targetResourceReadCount, 1);
|
|
102
|
+
});
|
|
103
|
+
test('resolveUnityBindings emits structured local/list reference targets for agent consumption', async () => {
|
|
104
|
+
const result = await resolveUnityBindings({ repoRoot: fixtureRoot, symbol: 'MenuScreenCarrier' });
|
|
105
|
+
const binding = result.resourceBindings[0];
|
|
106
|
+
assert.ok(binding);
|
|
107
|
+
const defaultRef = binding.resolvedReferences.find((ref) => ref.fieldName === 'defaultScreen' && !ref.fromList);
|
|
108
|
+
assert.equal(defaultRef?.resolution, 'local-object');
|
|
109
|
+
assert.equal(defaultRef?.target?.objectType, 'GameObject');
|
|
110
|
+
assert.equal(defaultRef?.target?.gameObjectName, 'ScreenA');
|
|
111
|
+
const listRefs = binding.resolvedReferences
|
|
112
|
+
.filter((ref) => ref.fieldName === 'menuScreenList' && ref.fromList)
|
|
113
|
+
.sort((left, right) => (left.listIndex || 0) - (right.listIndex || 0));
|
|
114
|
+
assert.equal(listRefs.length, 3);
|
|
115
|
+
assert.equal(listRefs[0].resolution, 'local-object');
|
|
116
|
+
assert.equal(listRefs[0].target?.gameObjectName, 'ScreenA');
|
|
117
|
+
assert.equal(listRefs[1].resolution, 'local-object');
|
|
118
|
+
assert.equal(listRefs[1].target?.gameObjectName, 'ScreenB');
|
|
119
|
+
assert.equal(listRefs[2].resolution, 'null');
|
|
120
|
+
});
|
|
121
|
+
test('resolveUnityBindings resolves external guid to asset path when scan context includes asset meta', async () => {
|
|
122
|
+
const context = await buildUnityScanContext({
|
|
123
|
+
repoRoot: fixtureRoot,
|
|
124
|
+
scopedPaths: [
|
|
125
|
+
'Assets/Scripts/MainUIManager.cs',
|
|
126
|
+
'Assets/Scripts/MainUIManager.cs.meta',
|
|
127
|
+
'Assets/Scene/MainUIManager.unity',
|
|
128
|
+
'Assets/Config/MainUIDocument.asset.meta',
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
const result = await resolveUnityBindings({ repoRoot: fixtureRoot, symbol: 'MainUIManager', scanContext: context });
|
|
132
|
+
const mainBinding = result.resourceBindings[0];
|
|
133
|
+
assert.ok(mainBinding);
|
|
134
|
+
const externalRef = mainBinding.resolvedReferences.find((ref) => ref.fieldName === 'mainUIDocument' && ref.guid === '44444444444444444444444444444444');
|
|
135
|
+
assert.equal(externalRef?.resolution, 'external-asset');
|
|
136
|
+
assert.equal(externalRef?.target?.assetPath, 'Assets/Config/MainUIDocument.asset');
|
|
137
|
+
});
|
|
138
|
+
test('resolveUnityBindings supports ScriptableObject .asset resource bindings', async () => {
|
|
139
|
+
const context = await buildUnityScanContext({
|
|
140
|
+
repoRoot: fixtureRoot,
|
|
141
|
+
scopedPaths: [
|
|
142
|
+
'Assets/Scripts/U2ScriptableConfig.cs',
|
|
143
|
+
'Assets/Scripts/U2ScriptableConfig.cs.meta',
|
|
144
|
+
'Assets/Config/U2ScriptableConfig.asset',
|
|
145
|
+
'Assets/Config/U2ScriptableConfig.asset.meta',
|
|
146
|
+
'Assets/Config/MainUIDocument.asset.meta',
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
const result = await resolveUnityBindings({ repoRoot: fixtureRoot, symbol: 'U2ScriptableConfig', scanContext: context });
|
|
150
|
+
const binding = result.resourceBindings[0];
|
|
151
|
+
assert.ok(binding);
|
|
152
|
+
assert.equal(binding.resourceType, 'asset');
|
|
153
|
+
assert.equal(binding.resourcePath, 'Assets/Config/U2ScriptableConfig.asset');
|
|
154
|
+
assert.deepEqual(binding.serializedFields, result.serializedFields);
|
|
155
|
+
assert.deepEqual(binding.serializedFields.scalarFields.map((field) => field.name), ['menuScreenList']);
|
|
156
|
+
assert.deepEqual(binding.serializedFields.referenceFields.map((field) => field.name), ['mainUIDocument']);
|
|
157
|
+
assert.equal(binding.serializedFields.referenceFields[0]?.sourceLayer, 'asset');
|
|
158
|
+
const directExternal = binding.resolvedReferences.find((ref) => ref.fieldName === 'mainUIDocument' && !ref.fromList);
|
|
159
|
+
assert.equal(directExternal?.resolution, 'external-asset');
|
|
160
|
+
assert.equal(directExternal?.target?.assetPath, 'Assets/Config/MainUIDocument.asset');
|
|
161
|
+
const listRefs = binding.resolvedReferences
|
|
162
|
+
.filter((ref) => ref.fieldName === 'menuScreenList' && ref.fromList)
|
|
163
|
+
.sort((left, right) => (left.listIndex || 0) - (right.listIndex || 0));
|
|
164
|
+
assert.equal(listRefs.length, 2);
|
|
165
|
+
assert.equal(listRefs[0]?.resolution, 'null');
|
|
166
|
+
assert.equal(listRefs[1]?.resolution, 'external-asset');
|
|
167
|
+
});
|
|
168
|
+
test('resolveUnityBindings keeps existing scene serializedFields stable when .asset support is enabled', async () => {
|
|
169
|
+
const context = await buildUnityScanContext({
|
|
170
|
+
repoRoot: fixtureRoot,
|
|
171
|
+
scopedPaths: [
|
|
172
|
+
'Assets/Scripts/MainUIManager.cs',
|
|
173
|
+
'Assets/Scripts/MainUIManager.cs.meta',
|
|
174
|
+
'Assets/Scene/MainUIManager.unity',
|
|
175
|
+
'Assets/Scripts/U2ScriptableConfig.cs',
|
|
176
|
+
'Assets/Scripts/U2ScriptableConfig.cs.meta',
|
|
177
|
+
'Assets/Config/U2ScriptableConfig.asset',
|
|
178
|
+
'Assets/Config/U2ScriptableConfig.asset.meta',
|
|
179
|
+
'Assets/Config/MainUIDocument.asset.meta',
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
const result = await resolveUnityBindings({ repoRoot: fixtureRoot, symbol: 'MainUIManager', scanContext: context });
|
|
183
|
+
const needPause = result.serializedFields.scalarFields.find((field) => field.name === 'needPause');
|
|
184
|
+
const mainUIDocument = result.serializedFields.referenceFields.find((field) => field.name === 'mainUIDocument');
|
|
185
|
+
assert.ok(result.resourceBindings.length > 0);
|
|
186
|
+
assert.equal(needPause?.sourceLayer, 'scene');
|
|
187
|
+
assert.equal(needPause?.value, '1');
|
|
188
|
+
assert.equal(mainUIDocument?.sourceLayer, 'scene');
|
|
189
|
+
assert.equal(mainUIDocument?.guid, '44444444444444444444444444444444');
|
|
190
|
+
});
|
|
191
|
+
test('extractAssetRefPathReferences parses nested _relativePath rows and marks sprite assets', () => {
|
|
192
|
+
const refs = extractAssetRefPathReferences({
|
|
193
|
+
scalarFields: [
|
|
194
|
+
{
|
|
195
|
+
name: 'Values',
|
|
196
|
+
sourceLayer: 'asset',
|
|
197
|
+
value: `
|
|
198
|
+
_Head_Ref:
|
|
199
|
+
_relativePath: Assets/NEON/Art/Sprites/UI/0_pixle/ui_character_head/hero_head_Nik.png
|
|
200
|
+
_actorPrefabRef:
|
|
201
|
+
_relativePath: Assets/ActorPrefab/Actor_Nik/V_Actor_Nik.prefab
|
|
202
|
+
`,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
referenceFields: [],
|
|
206
|
+
});
|
|
207
|
+
assert.equal(refs.length, 2);
|
|
208
|
+
assert.equal(refs[0]?.fieldName, '_Head_Ref');
|
|
209
|
+
assert.equal(refs[0]?.isSprite, true);
|
|
210
|
+
assert.equal(refs[1]?.fieldName, '_actorPrefabRef');
|
|
211
|
+
assert.equal(refs[1]?.isSprite, false);
|
|
212
|
+
});
|
|
213
|
+
test('extractAssetRefPathReferences handles Unity Ref naming variants and stable sprite classification', () => {
|
|
214
|
+
const refs = extractAssetRefPathReferences({
|
|
215
|
+
scalarFields: [
|
|
216
|
+
{
|
|
217
|
+
name: 'Values',
|
|
218
|
+
sourceLayer: 'asset',
|
|
219
|
+
value: `
|
|
220
|
+
_icon_Ref:
|
|
221
|
+
_relativePath: "Assets/NEON/Art/Sprites/UI/icon_main.PNG"
|
|
222
|
+
actorPrefabRef:
|
|
223
|
+
_relativePath: Assets/ActorPrefab/Actor_Nik/V_Actor_Nik.prefab
|
|
224
|
+
_atlas_Ref:
|
|
225
|
+
_relativePath: Assets/Atlas/UI.spriteatlasv2
|
|
226
|
+
_empty_Ref:
|
|
227
|
+
_relativePath:
|
|
228
|
+
`,
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
referenceFields: [],
|
|
232
|
+
});
|
|
233
|
+
assert.equal(refs.length, 4);
|
|
234
|
+
assert.equal(refs[0]?.fieldName, '_icon_Ref');
|
|
235
|
+
assert.equal(refs[0]?.relativePath, 'Assets/NEON/Art/Sprites/UI/icon_main.PNG');
|
|
236
|
+
assert.equal(refs[0]?.isSprite, true);
|
|
237
|
+
assert.equal(refs[1]?.fieldName, 'actorPrefabRef');
|
|
238
|
+
assert.equal(refs[1]?.isSprite, false);
|
|
239
|
+
assert.equal(refs[2]?.fieldName, '_atlas_Ref');
|
|
240
|
+
assert.equal(refs[2]?.isSprite, true);
|
|
241
|
+
assert.equal(refs[3]?.fieldName, '_empty_Ref');
|
|
242
|
+
assert.equal(refs[3]?.isEmpty, true);
|
|
243
|
+
assert.equal(refs.every((row) => row.parentFieldName === 'Values'), true);
|
|
244
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface UnityResourceGuidHit {
|
|
2
|
+
resourcePath: string;
|
|
3
|
+
resourceType: 'prefab' | 'scene' | 'asset';
|
|
4
|
+
line: number;
|
|
5
|
+
lineText: string;
|
|
6
|
+
}
|
|
7
|
+
export interface FindGuidHitsOptions {
|
|
8
|
+
resourceFiles?: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function findGuidHits(repoRoot: string, guid: string, options?: FindGuidHitsOptions): Promise<UnityResourceGuidHit[]>;
|