@veewo/gitnexus 1.3.8 → 1.3.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.
Files changed (108) hide show
  1. package/README.md +5 -0
  2. package/dist/benchmark/runner.test.js +1 -1
  3. package/dist/benchmark/u2-e2e/analyze-parser.d.ts +22 -0
  4. package/dist/benchmark/u2-e2e/analyze-parser.js +89 -0
  5. package/dist/benchmark/u2-e2e/analyze-parser.test.d.ts +1 -0
  6. package/dist/benchmark/u2-e2e/analyze-parser.test.js +13 -0
  7. package/dist/benchmark/u2-e2e/characterlist-assetref.d.ts +19 -0
  8. package/dist/benchmark/u2-e2e/characterlist-assetref.js +80 -0
  9. package/dist/benchmark/u2-e2e/characterlist-assetref.test.d.ts +1 -0
  10. package/dist/benchmark/u2-e2e/characterlist-assetref.test.js +108 -0
  11. package/dist/benchmark/u2-e2e/config.d.ts +25 -0
  12. package/dist/benchmark/u2-e2e/config.js +86 -0
  13. package/dist/benchmark/u2-e2e/config.test.d.ts +1 -0
  14. package/dist/benchmark/u2-e2e/config.test.js +29 -0
  15. package/dist/benchmark/u2-e2e/metrics.d.ts +20 -0
  16. package/dist/benchmark/u2-e2e/metrics.js +34 -0
  17. package/dist/benchmark/u2-e2e/metrics.test.d.ts +1 -0
  18. package/dist/benchmark/u2-e2e/metrics.test.js +13 -0
  19. package/dist/benchmark/u2-e2e/neonspark-full-e2e.d.ts +33 -0
  20. package/dist/benchmark/u2-e2e/neonspark-full-e2e.js +439 -0
  21. package/dist/benchmark/u2-e2e/neonspark-full-e2e.test.d.ts +1 -0
  22. package/dist/benchmark/u2-e2e/neonspark-full-e2e.test.js +40 -0
  23. package/dist/benchmark/u2-e2e/report.d.ts +58 -0
  24. package/dist/benchmark/u2-e2e/report.js +130 -0
  25. package/dist/benchmark/u2-e2e/report.test.d.ts +1 -0
  26. package/dist/benchmark/u2-e2e/report.test.js +58 -0
  27. package/dist/benchmark/u2-e2e/retrieval-runner.d.ts +21 -0
  28. package/dist/benchmark/u2-e2e/retrieval-runner.js +166 -0
  29. package/dist/benchmark/u2-e2e/retrieval-runner.test.d.ts +1 -0
  30. package/dist/benchmark/u2-e2e/retrieval-runner.test.js +145 -0
  31. package/dist/benchmark/u2-performance-sampler.d.ts +33 -0
  32. package/dist/benchmark/u2-performance-sampler.js +178 -0
  33. package/dist/benchmark/u2-performance-sampler.test.d.ts +1 -0
  34. package/dist/benchmark/u2-performance-sampler.test.js +34 -0
  35. package/dist/cli/analyze-multi-scope-regression.test.js +10 -0
  36. package/dist/cli/analyze-summary.d.ts +7 -0
  37. package/dist/cli/analyze-summary.js +37 -0
  38. package/dist/cli/analyze-summary.test.d.ts +1 -0
  39. package/dist/cli/analyze-summary.test.js +58 -0
  40. package/dist/cli/analyze.js +11 -6
  41. package/dist/cli/benchmark-u2-e2e.d.ts +9 -0
  42. package/dist/cli/benchmark-u2-e2e.js +35 -0
  43. package/dist/cli/benchmark-u2-e2e.test.d.ts +1 -0
  44. package/dist/cli/benchmark-u2-e2e.test.js +7 -0
  45. package/dist/cli/index.js +20 -0
  46. package/dist/cli/tool.d.ts +3 -0
  47. package/dist/cli/tool.js +2 -0
  48. package/dist/cli/unity-bindings.d.ts +8 -0
  49. package/dist/cli/unity-bindings.js +33 -0
  50. package/dist/cli/unity-bindings.test.d.ts +1 -0
  51. package/dist/cli/unity-bindings.test.js +24 -0
  52. package/dist/core/graph/types.d.ts +1 -1
  53. package/dist/core/ingestion/pipeline.d.ts +2 -4
  54. package/dist/core/ingestion/pipeline.js +12 -0
  55. package/dist/core/ingestion/unity-resource-processor.d.ts +26 -0
  56. package/dist/core/ingestion/unity-resource-processor.js +363 -0
  57. package/dist/core/ingestion/unity-resource-processor.test.d.ts +1 -0
  58. package/dist/core/ingestion/unity-resource-processor.test.js +599 -0
  59. package/dist/core/kuzu/kuzu-adapter.d.ts +6 -0
  60. package/dist/core/kuzu/kuzu-adapter.js +18 -7
  61. package/dist/core/kuzu/schema.d.ts +2 -2
  62. package/dist/core/kuzu/schema.js +22 -1
  63. package/dist/core/kuzu/schema.test.d.ts +1 -0
  64. package/dist/core/kuzu/schema.test.js +17 -0
  65. package/dist/core/unity/meta-index.d.ts +5 -0
  66. package/dist/core/unity/meta-index.js +113 -0
  67. package/dist/core/unity/meta-index.test.d.ts +1 -0
  68. package/dist/core/unity/meta-index.test.js +11 -0
  69. package/dist/core/unity/options.d.ts +2 -0
  70. package/dist/core/unity/options.js +9 -0
  71. package/dist/core/unity/options.test.d.ts +1 -0
  72. package/dist/core/unity/options.test.js +10 -0
  73. package/dist/core/unity/override-merger.d.ts +27 -0
  74. package/dist/core/unity/override-merger.js +35 -0
  75. package/dist/core/unity/override-merger.test.d.ts +1 -0
  76. package/dist/core/unity/override-merger.test.js +47 -0
  77. package/dist/core/unity/resolver.d.ts +79 -0
  78. package/dist/core/unity/resolver.js +384 -0
  79. package/dist/core/unity/resolver.test.d.ts +1 -0
  80. package/dist/core/unity/resolver.test.js +244 -0
  81. package/dist/core/unity/resource-hit-scanner.d.ts +10 -0
  82. package/dist/core/unity/resource-hit-scanner.js +60 -0
  83. package/dist/core/unity/resource-hit-scanner.test.d.ts +1 -0
  84. package/dist/core/unity/resource-hit-scanner.test.js +20 -0
  85. package/dist/core/unity/scan-context.d.ts +23 -0
  86. package/dist/core/unity/scan-context.js +318 -0
  87. package/dist/core/unity/scan-context.test.d.ts +1 -0
  88. package/dist/core/unity/scan-context.test.js +118 -0
  89. package/dist/core/unity/serialized-type-index.d.ts +10 -0
  90. package/dist/core/unity/serialized-type-index.js +105 -0
  91. package/dist/core/unity/serialized-type-index.test.d.ts +1 -0
  92. package/dist/core/unity/serialized-type-index.test.js +34 -0
  93. package/dist/core/unity/u2-thresholds.test.d.ts +1 -0
  94. package/dist/core/unity/u2-thresholds.test.js +71 -0
  95. package/dist/core/unity/yaml-object-graph.d.ts +9 -0
  96. package/dist/core/unity/yaml-object-graph.js +92 -0
  97. package/dist/core/unity/yaml-object-graph.test.d.ts +1 -0
  98. package/dist/core/unity/yaml-object-graph.test.js +49 -0
  99. package/dist/mcp/local/local-backend.js +12 -1
  100. package/dist/mcp/local/unity-enrichment.d.ts +6 -0
  101. package/dist/mcp/local/unity-enrichment.js +91 -0
  102. package/dist/mcp/local/unity-enrichment.test.d.ts +1 -0
  103. package/dist/mcp/local/unity-enrichment.test.js +130 -0
  104. package/dist/mcp/tools.js +12 -0
  105. package/dist/types/pipeline.d.ts +7 -0
  106. package/dist/types/pipeline.js +2 -0
  107. package/hooks/check-release-path-hygiene.mjs +108 -0
  108. package/package.json +14 -7
@@ -0,0 +1,363 @@
1
+ import path from 'node:path';
2
+ import { performance } from 'node:perf_hooks';
3
+ import { generateId } from '../../lib/utils.js';
4
+ import { buildUnityScanContext } from '../unity/scan-context.js';
5
+ import { resolveUnityBindings } from '../unity/resolver.js';
6
+ const UNITY_DIAGNOSTIC_SAMPLE_LIMIT = 3;
7
+ export async function processUnityResources(graph, options, deps) {
8
+ const tStart = performance.now();
9
+ const buildScanContextFn = deps?.buildScanContext || buildUnityScanContext;
10
+ const resolveBindingsFn = deps?.resolveBindings || resolveUnityBindings;
11
+ const payloadMode = resolveUnityPayloadMode(options.payloadMode);
12
+ const classNodes = [...graph.iterNodes()].filter((node) => node.label === 'Class' && String(node.properties.filePath || '').endsWith('.cs'));
13
+ const symbolDeclarations = classNodes
14
+ .map((node) => ({
15
+ symbol: String(node.properties.name || '').trim(),
16
+ scriptPath: String(node.properties.filePath || '').trim(),
17
+ }))
18
+ .filter((entry) => entry.symbol.length > 0 && entry.scriptPath.length > 0);
19
+ let processedSymbols = 0;
20
+ let bindingCount = 0;
21
+ let componentCount = 0;
22
+ const diagnostics = [];
23
+ const issueDiagnostics = [];
24
+ let scanContext;
25
+ let symbolsWithResourceHits = new Set();
26
+ let skippedNoGuidHit = 0;
27
+ let skippedMissingCanonical = 0;
28
+ let skippedNonCanonical = 0;
29
+ let canonicalSelected = 0;
30
+ let serializedTypeEdgeCount = 0;
31
+ let serializedTypeMissCount = 0;
32
+ const serializedTypeSymbols = new Set();
33
+ const resolvedBySymbol = new Map();
34
+ const resolveErrorBySymbol = new Map();
35
+ let scanContextMs = 0;
36
+ let resolveMs = 0;
37
+ let graphWriteMs = 0;
38
+ try {
39
+ const tScanContextStart = performance.now();
40
+ scanContext = await buildScanContextFn({
41
+ repoRoot: options.repoPath,
42
+ scopedPaths: options.scopedPaths,
43
+ symbolDeclarations,
44
+ });
45
+ scanContextMs += performance.now() - tScanContextStart;
46
+ const uniqueResourcePaths = new Set();
47
+ for (const hits of scanContext.guidToResourceHits.values()) {
48
+ for (const hit of hits) {
49
+ uniqueResourcePaths.add(hit.resourcePath);
50
+ }
51
+ }
52
+ diagnostics.push(`scanContext: scripts=${scanContext.symbolToScriptPath.size}, guids=${scanContext.scriptPathToGuid.size}, resources=${uniqueResourcePaths.size}`);
53
+ symbolsWithResourceHits = collectSymbolsWithResourceHits(scanContext);
54
+ }
55
+ catch (error) {
56
+ if (scanContextMs === 0) {
57
+ scanContextMs = performance.now() - tStart;
58
+ }
59
+ diagnostics.push(error instanceof Error ? error.message : String(error));
60
+ }
61
+ const canonicalClassNodeBySymbol = buildCanonicalClassNodeIndex(classNodes, scanContext);
62
+ for (const classNode of classNodes) {
63
+ const symbol = String(classNode.properties.name || '').trim();
64
+ if (!symbol)
65
+ continue;
66
+ if (scanContext) {
67
+ const canonicalScriptPath = getCanonicalScriptPath(scanContext, symbol);
68
+ if (!canonicalScriptPath) {
69
+ skippedMissingCanonical += 1;
70
+ continue;
71
+ }
72
+ const classNodePath = normalizePath(String(classNode.properties.filePath || '').trim());
73
+ if (classNodePath !== canonicalScriptPath) {
74
+ skippedNonCanonical += 1;
75
+ continue;
76
+ }
77
+ canonicalSelected += 1;
78
+ if (!symbolsWithResourceHits.has(symbol)) {
79
+ skippedNoGuidHit += 1;
80
+ continue;
81
+ }
82
+ }
83
+ try {
84
+ const resolveError = resolveErrorBySymbol.get(symbol);
85
+ if (resolveError) {
86
+ issueDiagnostics.push(resolveError);
87
+ continue;
88
+ }
89
+ let resolved = resolvedBySymbol.get(symbol);
90
+ if (!resolved) {
91
+ const tResolveStart = performance.now();
92
+ resolved = await resolveBindingsFn({ repoRoot: options.repoPath, symbol, scanContext });
93
+ resolveMs += performance.now() - tResolveStart;
94
+ resolvedBySymbol.set(symbol, resolved);
95
+ }
96
+ issueDiagnostics.push(...resolved.unityDiagnostics);
97
+ if (resolved.resourceBindings.length === 0) {
98
+ continue;
99
+ }
100
+ processedSymbols += 1;
101
+ for (const binding of resolved.resourceBindings) {
102
+ const tWriteStart = performance.now();
103
+ bindingCount += 1;
104
+ componentCount += 1;
105
+ const resourceFileNode = ensureResourceFileNode(graph, binding.resourcePath);
106
+ const componentNode = createComponentNode(symbol, binding, payloadMode);
107
+ graph.addNode(componentNode);
108
+ graph.addRelationship({
109
+ id: generateId('UNITY_COMPONENT_IN', `${classNode.id}:${binding.componentObjectId}->${resourceFileNode.id}`),
110
+ type: 'UNITY_COMPONENT_IN',
111
+ sourceId: classNode.id,
112
+ targetId: resourceFileNode.id,
113
+ confidence: 1.0,
114
+ reason: binding.bindingKind,
115
+ });
116
+ graph.addRelationship({
117
+ id: generateId('UNITY_COMPONENT_INSTANCE', `${classNode.id}->${componentNode.id}`),
118
+ type: 'UNITY_COMPONENT_INSTANCE',
119
+ sourceId: classNode.id,
120
+ targetId: componentNode.id,
121
+ confidence: 1.0,
122
+ reason: binding.bindingKind,
123
+ });
124
+ const serializableTypeLinking = linkSerializableTypeEdges(graph, componentNode, symbol, binding, scanContext, canonicalClassNodeBySymbol);
125
+ serializedTypeEdgeCount += serializableTypeLinking.edgeCount;
126
+ serializedTypeMissCount += serializableTypeLinking.missCount;
127
+ for (const hitSymbol of serializableTypeLinking.symbols) {
128
+ serializedTypeSymbols.add(hitSymbol);
129
+ }
130
+ graphWriteMs += performance.now() - tWriteStart;
131
+ }
132
+ }
133
+ catch (error) {
134
+ const message = error instanceof Error ? error.message : String(error);
135
+ resolveErrorBySymbol.set(symbol, message);
136
+ issueDiagnostics.push(message);
137
+ }
138
+ }
139
+ if (skippedNoGuidHit > 0) {
140
+ diagnostics.push(`prefilter: skipped ${skippedNoGuidHit} symbol(s) without guid resource hits`);
141
+ }
142
+ diagnostics.push(`canonical: selected=${canonicalSelected}, skip-non-canonical=${skippedNonCanonical}, missing-canonical=${skippedMissingCanonical}`);
143
+ diagnostics.push(`serialized-type: edges=${serializedTypeEdgeCount}, symbols=${serializedTypeSymbols.size}, misses=${serializedTypeMissCount}`);
144
+ if (skippedMissingCanonical > 0) {
145
+ diagnostics.push(`prefilter: skipped ${skippedMissingCanonical} symbol(s) missing canonical script mapping`);
146
+ }
147
+ diagnostics.push(...aggregateUnityDiagnostics(issueDiagnostics));
148
+ return {
149
+ processedSymbols,
150
+ bindingCount,
151
+ componentCount,
152
+ diagnostics,
153
+ timingsMs: {
154
+ scanContext: roundMs(scanContextMs),
155
+ resolve: roundMs(resolveMs),
156
+ graphWrite: roundMs(graphWriteMs),
157
+ total: roundMs(performance.now() - tStart),
158
+ },
159
+ };
160
+ }
161
+ function collectSymbolsWithResourceHits(scanContext) {
162
+ const symbols = new Set();
163
+ const canonicalEntries = scanContext.symbolToCanonicalScriptPath?.entries() || scanContext.symbolToScriptPath.entries();
164
+ for (const [symbol, scriptPath] of canonicalEntries) {
165
+ const guid = scanContext.scriptPathToGuid.get(scriptPath);
166
+ if (!guid)
167
+ continue;
168
+ if ((scanContext.guidToResourceHits.get(guid) || []).length === 0)
169
+ continue;
170
+ symbols.add(symbol);
171
+ }
172
+ return symbols;
173
+ }
174
+ function getCanonicalScriptPath(scanContext, symbol) {
175
+ const canonicalPath = scanContext.symbolToCanonicalScriptPath?.get(symbol);
176
+ if (canonicalPath) {
177
+ return normalizePath(canonicalPath);
178
+ }
179
+ const fallbackPath = scanContext.symbolToScriptPath.get(symbol);
180
+ if (fallbackPath) {
181
+ return normalizePath(fallbackPath);
182
+ }
183
+ return undefined;
184
+ }
185
+ function normalizePath(filePath) {
186
+ return filePath.replace(/\\/g, '/');
187
+ }
188
+ function resolveUnityPayloadMode(explicit) {
189
+ if (explicit)
190
+ return explicit;
191
+ const envMode = String(process.env.GITNEXUS_UNITY_PAYLOAD_MODE || '').trim().toLowerCase();
192
+ if (envMode === 'full')
193
+ return 'full';
194
+ return 'compact';
195
+ }
196
+ function ensureResourceFileNode(graph, resourcePath) {
197
+ const normalizedResourcePath = resourcePath.replace(/\\/g, '/');
198
+ const fileId = generateId('File', normalizedResourcePath);
199
+ const existing = graph.getNode(fileId);
200
+ if (existing) {
201
+ return existing;
202
+ }
203
+ const node = {
204
+ id: fileId,
205
+ label: 'File',
206
+ properties: {
207
+ name: path.basename(normalizedResourcePath),
208
+ filePath: normalizedResourcePath,
209
+ },
210
+ };
211
+ graph.addNode(node);
212
+ return node;
213
+ }
214
+ function createComponentNode(symbol, binding, payloadMode) {
215
+ const payload = buildUnityPayload(binding, payloadMode);
216
+ return {
217
+ id: generateId('CodeElement', `${binding.resourcePath}:${binding.componentObjectId}`),
218
+ label: 'CodeElement',
219
+ properties: {
220
+ name: `${symbol}@${binding.componentObjectId}`,
221
+ filePath: binding.resourcePath,
222
+ startLine: binding.evidence.line,
223
+ endLine: binding.evidence.line,
224
+ description: JSON.stringify(payload),
225
+ },
226
+ };
227
+ }
228
+ function buildUnityPayload(binding, mode) {
229
+ const payload = {
230
+ bindingKind: binding.bindingKind,
231
+ componentObjectId: binding.componentObjectId,
232
+ };
233
+ if (binding.serializedFields.scalarFields.length > 0 || binding.serializedFields.referenceFields.length > 0) {
234
+ payload.serializedFields = binding.serializedFields;
235
+ }
236
+ if (binding.resolvedReferences && binding.resolvedReferences.length > 0) {
237
+ payload.resolvedReferences = binding.resolvedReferences;
238
+ }
239
+ if (binding.assetRefPaths && binding.assetRefPaths.length > 0) {
240
+ payload.assetRefPaths = binding.assetRefPaths;
241
+ }
242
+ if (mode === 'full') {
243
+ payload.resourcePath = binding.resourcePath;
244
+ payload.resourceType = binding.resourceType;
245
+ payload.evidence = binding.evidence;
246
+ }
247
+ return payload;
248
+ }
249
+ function buildCanonicalClassNodeIndex(classNodes, scanContext) {
250
+ const index = new Map();
251
+ for (const classNode of classNodes) {
252
+ const symbol = String(classNode.properties.name || '').trim();
253
+ if (!symbol || index.has(symbol))
254
+ continue;
255
+ const classPath = normalizePath(String(classNode.properties.filePath || '').trim());
256
+ const canonicalPath = scanContext ? getCanonicalScriptPath(scanContext, symbol) : undefined;
257
+ if (canonicalPath && classPath !== canonicalPath) {
258
+ continue;
259
+ }
260
+ index.set(symbol, classNode);
261
+ }
262
+ return index;
263
+ }
264
+ function linkSerializableTypeEdges(graph, componentNode, hostSymbol, binding, scanContext, canonicalClassNodeBySymbol) {
265
+ const stats = {
266
+ edgeCount: 0,
267
+ missCount: 0,
268
+ symbols: new Set(),
269
+ };
270
+ if (!scanContext)
271
+ return stats;
272
+ const serializableSymbols = scanContext.serializableSymbols;
273
+ const hostFieldTypeHints = scanContext.hostFieldTypeHints;
274
+ if (!serializableSymbols || !hostFieldTypeHints)
275
+ return stats;
276
+ const hostHints = hostFieldTypeHints.get(hostSymbol);
277
+ if (!hostHints || hostHints.size === 0)
278
+ return stats;
279
+ const fieldSourceLayer = collectBindingFieldSources(binding);
280
+ if (fieldSourceLayer.size === 0)
281
+ return stats;
282
+ for (const [fieldName, declaredType] of hostHints.entries()) {
283
+ if (!serializableSymbols.has(declaredType))
284
+ continue;
285
+ const sourceLayer = fieldSourceLayer.get(fieldName);
286
+ if (!sourceLayer)
287
+ continue;
288
+ const serializableNode = canonicalClassNodeBySymbol.get(declaredType);
289
+ if (!serializableNode) {
290
+ stats.missCount += 1;
291
+ continue;
292
+ }
293
+ graph.addRelationship({
294
+ id: generateId('UNITY_SERIALIZED_TYPE_IN', `${serializableNode.id}->${componentNode.id}:${fieldName}`),
295
+ type: 'UNITY_SERIALIZED_TYPE_IN',
296
+ sourceId: serializableNode.id,
297
+ targetId: componentNode.id,
298
+ confidence: 1.0,
299
+ reason: JSON.stringify({ hostSymbol, fieldName, declaredType, sourceLayer }),
300
+ });
301
+ stats.edgeCount += 1;
302
+ stats.symbols.add(declaredType);
303
+ }
304
+ return stats;
305
+ }
306
+ function collectBindingFieldSources(binding) {
307
+ const fieldSources = new Map();
308
+ for (const field of binding.serializedFields.scalarFields) {
309
+ if (!fieldSources.has(field.name)) {
310
+ fieldSources.set(field.name, field.sourceLayer || 'unknown');
311
+ }
312
+ }
313
+ for (const field of binding.serializedFields.referenceFields) {
314
+ if (!fieldSources.has(field.name)) {
315
+ fieldSources.set(field.name, field.sourceLayer || 'unknown');
316
+ }
317
+ }
318
+ return fieldSources;
319
+ }
320
+ function roundMs(value) {
321
+ return Number(value.toFixed(1));
322
+ }
323
+ function aggregateUnityDiagnostics(messages) {
324
+ if (messages.length === 0) {
325
+ return [];
326
+ }
327
+ const buckets = new Map();
328
+ for (const message of messages) {
329
+ const category = classifyUnityDiagnostic(message);
330
+ const bucket = buckets.get(category) || { count: 0, samples: [] };
331
+ bucket.count += 1;
332
+ if (bucket.samples.length < UNITY_DIAGNOSTIC_SAMPLE_LIMIT && !bucket.samples.includes(message)) {
333
+ bucket.samples.push(message);
334
+ }
335
+ buckets.set(category, bucket);
336
+ }
337
+ const ordered = [...buckets.entries()].sort((left, right) => right[1].count - left[1].count);
338
+ const lines = [
339
+ `diagnostics: aggregated ${messages.length} issue(s) across ${ordered.length} category(ies); sampleLimit=${UNITY_DIAGNOSTIC_SAMPLE_LIMIT}`,
340
+ ];
341
+ for (const [category, bucket] of ordered) {
342
+ lines.push(`diagnostics: category=${category} count=${bucket.count} sampleCount=${bucket.samples.length}`);
343
+ for (const sample of bucket.samples) {
344
+ lines.push(`diagnostics: sample[${category}] ${sample}`);
345
+ }
346
+ }
347
+ return lines;
348
+ }
349
+ function classifyUnityDiagnostic(message) {
350
+ if (message.startsWith('No MonoBehaviour block matched script guid ')) {
351
+ return 'no-monobehaviour-match';
352
+ }
353
+ if (message.startsWith('Unity symbol "') && message.endsWith('" is ambiguous.')) {
354
+ return 'ambiguous-symbol';
355
+ }
356
+ if (message.startsWith('Unity symbol "') && message.includes('" was not found under ')) {
357
+ return 'symbol-not-found';
358
+ }
359
+ if (message.startsWith('No .meta guid found for ')) {
360
+ return 'missing-meta-guid';
361
+ }
362
+ return 'other';
363
+ }