@veewo/gitnexus 1.3.7 → 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 (132) hide show
  1. package/README.md +9 -4
  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-custom-modules-regression.test.d.ts +1 -0
  36. package/dist/cli/analyze-custom-modules-regression.test.js +75 -0
  37. package/dist/cli/analyze-modules-diagnostics.test.d.ts +1 -0
  38. package/dist/cli/analyze-modules-diagnostics.test.js +36 -0
  39. package/dist/cli/analyze-multi-scope-regression.test.js +10 -0
  40. package/dist/cli/analyze-summary.d.ts +7 -0
  41. package/dist/cli/analyze-summary.js +37 -0
  42. package/dist/cli/analyze-summary.test.d.ts +1 -0
  43. package/dist/cli/analyze-summary.test.js +58 -0
  44. package/dist/cli/analyze.js +11 -6
  45. package/dist/cli/benchmark-u2-e2e.d.ts +9 -0
  46. package/dist/cli/benchmark-u2-e2e.js +35 -0
  47. package/dist/cli/benchmark-u2-e2e.test.d.ts +1 -0
  48. package/dist/cli/benchmark-u2-e2e.test.js +7 -0
  49. package/dist/cli/index.js +20 -0
  50. package/dist/cli/setup.js +24 -3
  51. package/dist/cli/setup.test.js +6 -4
  52. package/dist/cli/tool.d.ts +3 -0
  53. package/dist/cli/tool.js +2 -0
  54. package/dist/cli/unity-bindings.d.ts +8 -0
  55. package/dist/cli/unity-bindings.js +33 -0
  56. package/dist/cli/unity-bindings.test.d.ts +1 -0
  57. package/dist/cli/unity-bindings.test.js +24 -0
  58. package/dist/core/graph/types.d.ts +1 -1
  59. package/dist/core/ingestion/modules/assignment-engine.d.ts +33 -0
  60. package/dist/core/ingestion/modules/assignment-engine.js +179 -0
  61. package/dist/core/ingestion/modules/assignment-engine.test.d.ts +1 -0
  62. package/dist/core/ingestion/modules/assignment-engine.test.js +111 -0
  63. package/dist/core/ingestion/modules/config-loader.d.ts +2 -0
  64. package/dist/core/ingestion/modules/config-loader.js +186 -0
  65. package/dist/core/ingestion/modules/config-loader.test.d.ts +1 -0
  66. package/dist/core/ingestion/modules/config-loader.test.js +57 -0
  67. package/dist/core/ingestion/modules/rule-matcher.d.ts +12 -0
  68. package/dist/core/ingestion/modules/rule-matcher.js +63 -0
  69. package/dist/core/ingestion/modules/rule-matcher.test.d.ts +1 -0
  70. package/dist/core/ingestion/modules/rule-matcher.test.js +58 -0
  71. package/dist/core/ingestion/modules/types.d.ts +44 -0
  72. package/dist/core/ingestion/modules/types.js +2 -0
  73. package/dist/core/ingestion/pipeline.d.ts +2 -4
  74. package/dist/core/ingestion/pipeline.js +12 -0
  75. package/dist/core/ingestion/unity-resource-processor.d.ts +26 -0
  76. package/dist/core/ingestion/unity-resource-processor.js +363 -0
  77. package/dist/core/ingestion/unity-resource-processor.test.d.ts +1 -0
  78. package/dist/core/ingestion/unity-resource-processor.test.js +599 -0
  79. package/dist/core/kuzu/kuzu-adapter.d.ts +6 -0
  80. package/dist/core/kuzu/kuzu-adapter.js +18 -7
  81. package/dist/core/kuzu/schema.d.ts +2 -2
  82. package/dist/core/kuzu/schema.js +22 -1
  83. package/dist/core/kuzu/schema.test.d.ts +1 -0
  84. package/dist/core/kuzu/schema.test.js +17 -0
  85. package/dist/core/unity/meta-index.d.ts +5 -0
  86. package/dist/core/unity/meta-index.js +113 -0
  87. package/dist/core/unity/meta-index.test.d.ts +1 -0
  88. package/dist/core/unity/meta-index.test.js +11 -0
  89. package/dist/core/unity/options.d.ts +2 -0
  90. package/dist/core/unity/options.js +9 -0
  91. package/dist/core/unity/options.test.d.ts +1 -0
  92. package/dist/core/unity/options.test.js +10 -0
  93. package/dist/core/unity/override-merger.d.ts +27 -0
  94. package/dist/core/unity/override-merger.js +35 -0
  95. package/dist/core/unity/override-merger.test.d.ts +1 -0
  96. package/dist/core/unity/override-merger.test.js +47 -0
  97. package/dist/core/unity/resolver.d.ts +79 -0
  98. package/dist/core/unity/resolver.js +384 -0
  99. package/dist/core/unity/resolver.test.d.ts +1 -0
  100. package/dist/core/unity/resolver.test.js +244 -0
  101. package/dist/core/unity/resource-hit-scanner.d.ts +10 -0
  102. package/dist/core/unity/resource-hit-scanner.js +60 -0
  103. package/dist/core/unity/resource-hit-scanner.test.d.ts +1 -0
  104. package/dist/core/unity/resource-hit-scanner.test.js +20 -0
  105. package/dist/core/unity/scan-context.d.ts +23 -0
  106. package/dist/core/unity/scan-context.js +318 -0
  107. package/dist/core/unity/scan-context.test.d.ts +1 -0
  108. package/dist/core/unity/scan-context.test.js +118 -0
  109. package/dist/core/unity/serialized-type-index.d.ts +10 -0
  110. package/dist/core/unity/serialized-type-index.js +105 -0
  111. package/dist/core/unity/serialized-type-index.test.d.ts +1 -0
  112. package/dist/core/unity/serialized-type-index.test.js +34 -0
  113. package/dist/core/unity/u2-thresholds.test.d.ts +1 -0
  114. package/dist/core/unity/u2-thresholds.test.js +71 -0
  115. package/dist/core/unity/yaml-object-graph.d.ts +9 -0
  116. package/dist/core/unity/yaml-object-graph.js +92 -0
  117. package/dist/core/unity/yaml-object-graph.test.d.ts +1 -0
  118. package/dist/core/unity/yaml-object-graph.test.js +49 -0
  119. package/dist/mcp/local/cluster-aggregation.d.ts +20 -0
  120. package/dist/mcp/local/cluster-aggregation.js +48 -0
  121. package/dist/mcp/local/cluster-aggregation.test.d.ts +1 -0
  122. package/dist/mcp/local/cluster-aggregation.test.js +22 -0
  123. package/dist/mcp/local/local-backend.js +12 -1
  124. package/dist/mcp/local/unity-enrichment.d.ts +6 -0
  125. package/dist/mcp/local/unity-enrichment.js +91 -0
  126. package/dist/mcp/local/unity-enrichment.test.d.ts +1 -0
  127. package/dist/mcp/local/unity-enrichment.test.js +130 -0
  128. package/dist/mcp/tools.js +12 -0
  129. package/dist/types/pipeline.d.ts +7 -0
  130. package/dist/types/pipeline.js +2 -0
  131. package/hooks/check-release-path-hygiene.mjs +108 -0
  132. package/package.json +14 -7
@@ -0,0 +1,599 @@
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 { generateId } from '../../lib/utils.js';
6
+ import { createKnowledgeGraph } from '../graph/graph.js';
7
+ import { processUnityResources } from './unity-resource-processor.js';
8
+ const here = path.dirname(fileURLToPath(import.meta.url));
9
+ const fixtureRoot = path.resolve(here, '../../../src/core/unity/__fixtures__/mini-unity');
10
+ const symbols = ['Global', 'BattleMode', 'PlayerActor', 'MainUIManager'];
11
+ test('processUnityResources adds Unity resource relationships and component payload nodes', async () => {
12
+ const graph = createKnowledgeGraph();
13
+ for (const symbol of symbols) {
14
+ const filePath = `Assets/Scripts/${symbol}.cs`;
15
+ const fileId = generateId('File', filePath);
16
+ const classId = generateId('Class', `${filePath}:${symbol}`);
17
+ graph.addNode({
18
+ id: fileId,
19
+ label: 'File',
20
+ properties: {
21
+ name: `${symbol}.cs`,
22
+ filePath,
23
+ },
24
+ });
25
+ graph.addNode({
26
+ id: classId,
27
+ label: 'Class',
28
+ properties: {
29
+ name: symbol,
30
+ filePath,
31
+ },
32
+ });
33
+ graph.addRelationship({
34
+ id: generateId('DEFINES', `${fileId}->${classId}`),
35
+ type: 'DEFINES',
36
+ sourceId: fileId,
37
+ targetId: classId,
38
+ confidence: 1.0,
39
+ reason: '',
40
+ });
41
+ }
42
+ const result = await processUnityResources(graph, { repoPath: fixtureRoot });
43
+ const unityFileRelations = [...graph.iterRelationships()].filter((rel) => rel.type === 'UNITY_COMPONENT_IN');
44
+ const unityInstanceRelations = [...graph.iterRelationships()].filter((rel) => rel.type === 'UNITY_COMPONENT_INSTANCE');
45
+ const componentNodes = [...graph.iterNodes()].filter((node) => node.label === 'CodeElement' && /\.(unity|prefab)$/.test(String(node.properties.filePath)));
46
+ assert.ok(unityFileRelations.length > 0);
47
+ assert.ok(unityInstanceRelations.length > 0);
48
+ assert.ok(componentNodes.length > 0);
49
+ assert.ok(componentNodes.some((node) => String(node.properties.description).includes('mainUIDocument')));
50
+ assert.ok(result.bindingCount >= symbols.length);
51
+ assert.ok(result.timingsMs.scanContext >= 0);
52
+ assert.ok(result.timingsMs.resolve >= 0);
53
+ assert.ok(result.timingsMs.graphWrite > 0);
54
+ });
55
+ test('processUnityResources builds scan context once and enriches all class nodes', async () => {
56
+ const graph = createKnowledgeGraph();
57
+ for (const symbol of symbols) {
58
+ const filePath = `Assets/Scripts/${symbol}.cs`;
59
+ const fileId = generateId('File', filePath);
60
+ const classId = generateId('Class', `${filePath}:${symbol}`);
61
+ graph.addNode({
62
+ id: fileId,
63
+ label: 'File',
64
+ properties: {
65
+ name: `${symbol}.cs`,
66
+ filePath,
67
+ },
68
+ });
69
+ graph.addNode({
70
+ id: classId,
71
+ label: 'Class',
72
+ properties: {
73
+ name: symbol,
74
+ filePath,
75
+ },
76
+ });
77
+ }
78
+ const result = await processUnityResources(graph, {
79
+ repoPath: fixtureRoot,
80
+ scopedPaths: ['Assets/Scripts/MainUIManager.cs', 'Assets/Scene/MainUIManager.unity'],
81
+ });
82
+ assert.ok(result.processedSymbols > 0);
83
+ assert.ok(result.bindingCount > 0);
84
+ });
85
+ test('processUnityResources skips resolve for symbols without guid resource hits in scan context', async () => {
86
+ const graph = createKnowledgeGraph();
87
+ for (const symbol of ['HitSymbol', 'MissSymbol']) {
88
+ const filePath = `Assets/Scripts/${symbol}.cs`;
89
+ const classId = generateId('Class', `${filePath}:${symbol}`);
90
+ graph.addNode({
91
+ id: classId,
92
+ label: 'Class',
93
+ properties: {
94
+ name: symbol,
95
+ filePath,
96
+ },
97
+ });
98
+ }
99
+ const calledSymbols = [];
100
+ const fakeScanContext = {
101
+ symbolToScriptPath: new Map([
102
+ ['HitSymbol', 'Assets/Scripts/HitSymbol.cs'],
103
+ ['MissSymbol', 'Assets/Scripts/MissSymbol.cs'],
104
+ ]),
105
+ scriptPathToGuid: new Map([
106
+ ['Assets/Scripts/HitSymbol.cs', '11111111111111111111111111111111'],
107
+ ['Assets/Scripts/MissSymbol.cs', '22222222222222222222222222222222'],
108
+ ]),
109
+ guidToResourceHits: new Map([
110
+ ['11111111111111111111111111111111', [{ resourcePath: 'Assets/Scene/Test.unity', resourceType: 'scene', line: 1, lineText: 'guid: 1111' }]],
111
+ ['22222222222222222222222222222222', []],
112
+ ]),
113
+ resourceDocCache: new Map(),
114
+ };
115
+ const result = await processUnityResources(graph, { repoPath: fixtureRoot }, {
116
+ buildScanContext: async () => fakeScanContext,
117
+ resolveBindings: async ({ symbol }) => {
118
+ calledSymbols.push(String(symbol));
119
+ if (symbol === 'HitSymbol') {
120
+ return {
121
+ symbol: 'HitSymbol',
122
+ scriptPath: 'Assets/Scripts/HitSymbol.cs',
123
+ scriptGuid: '11111111111111111111111111111111',
124
+ resourceBindings: [
125
+ {
126
+ resourcePath: 'Assets/Scene/Test.unity',
127
+ resourceType: 'scene',
128
+ bindingKind: 'direct',
129
+ componentObjectId: '11400000',
130
+ evidence: { line: 1, lineText: 'guid: 1111' },
131
+ serializedFields: { scalarFields: [], referenceFields: [] },
132
+ },
133
+ ],
134
+ serializedFields: { scalarFields: [], referenceFields: [] },
135
+ unityDiagnostics: [],
136
+ };
137
+ }
138
+ return {
139
+ symbol: String(symbol),
140
+ scriptPath: '',
141
+ scriptGuid: '',
142
+ resourceBindings: [],
143
+ serializedFields: { scalarFields: [], referenceFields: [] },
144
+ unityDiagnostics: [],
145
+ };
146
+ },
147
+ });
148
+ assert.deepEqual(calledSymbols, ['HitSymbol']);
149
+ assert.equal(result.processedSymbols, 1);
150
+ assert.equal(result.bindingCount, 1);
151
+ });
152
+ test('processUnityResources skips resolve for symbols missing canonical script mapping', async () => {
153
+ const graph = createKnowledgeGraph();
154
+ for (const symbol of ['HitSymbol', 'UnknownSymbol']) {
155
+ const filePath = `Assets/Scripts/${symbol}.cs`;
156
+ const classId = generateId('Class', `${filePath}:${symbol}`);
157
+ graph.addNode({
158
+ id: classId,
159
+ label: 'Class',
160
+ properties: {
161
+ name: symbol,
162
+ filePath,
163
+ },
164
+ });
165
+ }
166
+ const calledSymbols = [];
167
+ const fakeScanContext = {
168
+ symbolToScriptPath: new Map([['HitSymbol', 'Assets/Scripts/HitSymbol.cs']]),
169
+ scriptPathToGuid: new Map([['Assets/Scripts/HitSymbol.cs', '11111111111111111111111111111111']]),
170
+ guidToResourceHits: new Map([
171
+ ['11111111111111111111111111111111', [{ resourcePath: 'Assets/Scene/Test.unity', resourceType: 'scene', line: 1, lineText: 'guid: 1111' }]],
172
+ ]),
173
+ resourceDocCache: new Map(),
174
+ };
175
+ const result = await processUnityResources(graph, { repoPath: fixtureRoot }, {
176
+ buildScanContext: async () => fakeScanContext,
177
+ resolveBindings: async ({ symbol }) => {
178
+ calledSymbols.push(String(symbol));
179
+ return {
180
+ symbol: String(symbol),
181
+ scriptPath: 'Assets/Scripts/HitSymbol.cs',
182
+ scriptGuid: '11111111111111111111111111111111',
183
+ resourceBindings: [
184
+ {
185
+ resourcePath: 'Assets/Scene/Test.unity',
186
+ resourceType: 'scene',
187
+ bindingKind: 'direct',
188
+ componentObjectId: '11400000',
189
+ evidence: { line: 1, lineText: 'guid: 1111' },
190
+ serializedFields: { scalarFields: [], referenceFields: [] },
191
+ },
192
+ ],
193
+ serializedFields: { scalarFields: [], referenceFields: [] },
194
+ unityDiagnostics: [],
195
+ };
196
+ },
197
+ });
198
+ assert.deepEqual(calledSymbols, ['HitSymbol']);
199
+ assert.ok(result.diagnostics.some((line) => line.includes('missing canonical script mapping')));
200
+ });
201
+ test('processUnityResources memoizes resolve results by symbol within one run', async () => {
202
+ const graph = createKnowledgeGraph();
203
+ for (const filePath of ['Assets/Scripts/DupA.cs', 'Assets/Scripts/DupB.cs']) {
204
+ const classId = generateId('Class', `${filePath}:DupSymbol`);
205
+ graph.addNode({
206
+ id: classId,
207
+ label: 'Class',
208
+ properties: {
209
+ name: 'DupSymbol',
210
+ filePath,
211
+ },
212
+ });
213
+ }
214
+ const calledSymbols = [];
215
+ const fakeScanContext = {
216
+ symbolToScriptPath: new Map([['DupSymbol', 'Assets/Scripts/DupA.cs']]),
217
+ scriptPathToGuid: new Map([['Assets/Scripts/DupA.cs', '11111111111111111111111111111111']]),
218
+ guidToResourceHits: new Map([
219
+ ['11111111111111111111111111111111', [{ resourcePath: 'Assets/Scene/Test.unity', resourceType: 'scene', line: 1, lineText: 'guid: 1111' }]],
220
+ ]),
221
+ resourceDocCache: new Map(),
222
+ };
223
+ const result = await processUnityResources(graph, { repoPath: fixtureRoot }, {
224
+ buildScanContext: async () => fakeScanContext,
225
+ resolveBindings: async ({ symbol }) => {
226
+ calledSymbols.push(String(symbol));
227
+ return {
228
+ symbol: String(symbol),
229
+ scriptPath: 'Assets/Scripts/DupA.cs',
230
+ scriptGuid: '11111111111111111111111111111111',
231
+ resourceBindings: [
232
+ {
233
+ resourcePath: 'Assets/Scene/Test.unity',
234
+ resourceType: 'scene',
235
+ bindingKind: 'direct',
236
+ componentObjectId: '11400000',
237
+ evidence: { line: 1, lineText: 'guid: 1111' },
238
+ serializedFields: { scalarFields: [], referenceFields: [] },
239
+ },
240
+ ],
241
+ serializedFields: { scalarFields: [], referenceFields: [] },
242
+ unityDiagnostics: [],
243
+ };
244
+ },
245
+ });
246
+ assert.deepEqual(calledSymbols, ['DupSymbol']);
247
+ assert.equal(result.processedSymbols, 1);
248
+ assert.equal(result.bindingCount, 1);
249
+ assert.ok(result.diagnostics.some((line) => line.includes('skip-non-canonical=1')));
250
+ });
251
+ test('processUnityResources writes UNITY_COMPONENT_INSTANCE only for canonical class node', async () => {
252
+ const graph = createKnowledgeGraph();
253
+ const canonicalPath = 'Assets/Scripts/PlayerActor.cs';
254
+ const partialPath = 'Assets/Scripts/PlayerActor.Visual.cs';
255
+ const canonicalClassId = generateId('Class', `${canonicalPath}:PlayerActor`);
256
+ const partialClassId = generateId('Class', `${partialPath}:PlayerActor`);
257
+ graph.addNode({
258
+ id: canonicalClassId,
259
+ label: 'Class',
260
+ properties: { name: 'PlayerActor', filePath: canonicalPath },
261
+ });
262
+ graph.addNode({
263
+ id: partialClassId,
264
+ label: 'Class',
265
+ properties: { name: 'PlayerActor', filePath: partialPath },
266
+ });
267
+ const fakeScanContext = {
268
+ symbolToScriptPath: new Map([['PlayerActor', canonicalPath]]),
269
+ symbolToScriptPaths: new Map([['PlayerActor', [canonicalPath, partialPath]]]),
270
+ symbolToCanonicalScriptPath: new Map([['PlayerActor', canonicalPath]]),
271
+ scriptPathToGuid: new Map([[canonicalPath, '11111111111111111111111111111111']]),
272
+ guidToResourceHits: new Map([
273
+ ['11111111111111111111111111111111', [{ resourcePath: 'Assets/Scene/Test.unity', resourceType: 'scene', line: 1, lineText: 'guid: 1111' }]],
274
+ ]),
275
+ resourceDocCache: new Map(),
276
+ };
277
+ const result = await processUnityResources(graph, { repoPath: fixtureRoot }, {
278
+ buildScanContext: async () => fakeScanContext,
279
+ resolveBindings: async () => ({
280
+ symbol: 'PlayerActor',
281
+ scriptPath: canonicalPath,
282
+ scriptGuid: '11111111111111111111111111111111',
283
+ resourceBindings: [
284
+ {
285
+ resourcePath: 'Assets/Scene/Test.unity',
286
+ resourceType: 'scene',
287
+ bindingKind: 'direct',
288
+ componentObjectId: '11400000',
289
+ evidence: { line: 1, lineText: 'guid: 1111' },
290
+ serializedFields: { scalarFields: [], referenceFields: [] },
291
+ },
292
+ ],
293
+ serializedFields: { scalarFields: [], referenceFields: [] },
294
+ unityDiagnostics: [],
295
+ }),
296
+ });
297
+ const instanceRelations = [...graph.iterRelationships()].filter((rel) => rel.type === 'UNITY_COMPONENT_INSTANCE');
298
+ assert.equal(instanceRelations.length, 1);
299
+ assert.equal(instanceRelations[0]?.sourceId, canonicalClassId);
300
+ assert.equal(result.processedSymbols, 1);
301
+ assert.equal(result.bindingCount, 1);
302
+ assert.ok(result.diagnostics.some((line) => line.includes('selected=1')));
303
+ assert.ok(result.diagnostics.some((line) => line.includes('skip-non-canonical=1')));
304
+ });
305
+ test('processUnityResources writes UNITY_SERIALIZED_TYPE_IN for serializable class field matches', async () => {
306
+ const graph = createKnowledgeGraph();
307
+ const hostPath = 'Assets/Scripts/HostClass.cs';
308
+ const serializablePath = 'Assets/Scripts/AssetRef.cs';
309
+ const hostClassId = generateId('Class', `${hostPath}:HostClass`);
310
+ const serializableClassId = generateId('Class', `${serializablePath}:AssetRef`);
311
+ graph.addNode({
312
+ id: hostClassId,
313
+ label: 'Class',
314
+ properties: { name: 'HostClass', filePath: hostPath },
315
+ });
316
+ graph.addNode({
317
+ id: serializableClassId,
318
+ label: 'Class',
319
+ properties: { name: 'AssetRef', filePath: serializablePath },
320
+ });
321
+ const fakeScanContext = {
322
+ symbolToScriptPath: new Map([
323
+ ['HostClass', hostPath],
324
+ ['AssetRef', serializablePath],
325
+ ]),
326
+ symbolToScriptPaths: new Map([
327
+ ['HostClass', [hostPath]],
328
+ ['AssetRef', [serializablePath]],
329
+ ]),
330
+ symbolToCanonicalScriptPath: new Map([
331
+ ['HostClass', hostPath],
332
+ ['AssetRef', serializablePath],
333
+ ]),
334
+ scriptPathToGuid: new Map([[hostPath, '11111111111111111111111111111111']]),
335
+ guidToResourceHits: new Map([
336
+ ['11111111111111111111111111111111', [{ resourcePath: 'Assets/Scene/Test.unity', resourceType: 'scene', line: 3, lineText: 'guid: 1111' }]],
337
+ ]),
338
+ serializableSymbols: new Set(['AssetRef']),
339
+ hostFieldTypeHints: new Map([['HostClass', new Map([['assetRef', 'AssetRef']])]]),
340
+ resourceDocCache: new Map(),
341
+ };
342
+ await processUnityResources(graph, { repoPath: fixtureRoot }, {
343
+ buildScanContext: async () => fakeScanContext,
344
+ resolveBindings: async () => ({
345
+ symbol: 'HostClass',
346
+ scriptPath: hostPath,
347
+ scriptGuid: '11111111111111111111111111111111',
348
+ resourceBindings: [
349
+ {
350
+ resourcePath: 'Assets/Scene/Test.unity',
351
+ resourceType: 'scene',
352
+ bindingKind: 'direct',
353
+ componentObjectId: '11400000',
354
+ evidence: { line: 3, lineText: 'guid: 1111' },
355
+ serializedFields: {
356
+ scalarFields: [],
357
+ referenceFields: [
358
+ { name: 'assetRef', guid: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', fileId: '0', sourceLayer: 'scene' },
359
+ ],
360
+ },
361
+ },
362
+ ],
363
+ serializedFields: { scalarFields: [], referenceFields: [] },
364
+ unityDiagnostics: [],
365
+ }),
366
+ });
367
+ const serializedTypeEdges = [...graph.iterRelationships()].filter((rel) => rel.type === 'UNITY_SERIALIZED_TYPE_IN');
368
+ assert.equal(serializedTypeEdges.length, 1);
369
+ assert.equal(serializedTypeEdges[0]?.sourceId, serializableClassId);
370
+ });
371
+ test('processUnityResources writes compact unity payload by default', async () => {
372
+ const graph = createKnowledgeGraph();
373
+ const classId = generateId('Class', 'Assets/Scripts/Compact.cs:CompactSymbol');
374
+ graph.addNode({
375
+ id: classId,
376
+ label: 'Class',
377
+ properties: { name: 'CompactSymbol', filePath: 'Assets/Scripts/Compact.cs' },
378
+ });
379
+ const fakeScanContext = {
380
+ symbolToScriptPath: new Map([['CompactSymbol', 'Assets/Scripts/Compact.cs']]),
381
+ scriptPathToGuid: new Map([['Assets/Scripts/Compact.cs', '11111111111111111111111111111111']]),
382
+ guidToResourceHits: new Map([
383
+ ['11111111111111111111111111111111', [{ resourcePath: 'Assets/Scene/Test.unity', resourceType: 'scene', line: 9, lineText: 'guid: 1111' }]],
384
+ ]),
385
+ resourceDocCache: new Map(),
386
+ };
387
+ await processUnityResources(graph, { repoPath: fixtureRoot }, {
388
+ buildScanContext: async () => fakeScanContext,
389
+ resolveBindings: async () => ({
390
+ symbol: 'CompactSymbol',
391
+ scriptPath: 'Assets/Scripts/Compact.cs',
392
+ scriptGuid: '11111111111111111111111111111111',
393
+ resourceBindings: [
394
+ {
395
+ resourcePath: 'Assets/Scene/Test.unity',
396
+ resourceType: 'scene',
397
+ bindingKind: 'scene-override',
398
+ componentObjectId: '11400000',
399
+ evidence: { line: 9, lineText: 'guid: 1111' },
400
+ serializedFields: {
401
+ scalarFields: [{ name: 'needPause', value: '1', valueType: 'number', sourceLayer: 'scene' }],
402
+ referenceFields: [],
403
+ },
404
+ },
405
+ ],
406
+ serializedFields: { scalarFields: [], referenceFields: [] },
407
+ unityDiagnostics: [],
408
+ }),
409
+ });
410
+ const component = [...graph.iterNodes()].find((node) => node.label === 'CodeElement');
411
+ assert.ok(component);
412
+ const payload = JSON.parse(String(component.properties.description));
413
+ assert.equal(payload.bindingKind, 'scene-override');
414
+ assert.equal(payload.componentObjectId, '11400000');
415
+ assert.ok(Array.isArray(payload.serializedFields.scalarFields));
416
+ assert.equal(payload.resourcePath, undefined);
417
+ assert.equal(payload.resourceType, undefined);
418
+ assert.equal(payload.evidence, undefined);
419
+ });
420
+ test('processUnityResources includes structured assetRefPaths in component payload', async () => {
421
+ const graph = createKnowledgeGraph();
422
+ const classId = generateId('Class', 'Assets/Scripts/CharacterList.cs:CharacterList');
423
+ graph.addNode({
424
+ id: classId,
425
+ label: 'Class',
426
+ properties: { name: 'CharacterList', filePath: 'Assets/Scripts/CharacterList.cs' },
427
+ });
428
+ const fakeScanContext = {
429
+ symbolToScriptPath: new Map([['CharacterList', 'Assets/Scripts/CharacterList.cs']]),
430
+ scriptPathToGuid: new Map([['Assets/Scripts/CharacterList.cs', '11111111111111111111111111111111']]),
431
+ guidToResourceHits: new Map([
432
+ ['11111111111111111111111111111111', [{ resourcePath: 'Assets/NEON/DataAssets/CharacterList.asset', resourceType: 'asset', line: 1, lineText: 'guid: 1111' }]],
433
+ ]),
434
+ resourceDocCache: new Map(),
435
+ };
436
+ await processUnityResources(graph, { repoPath: fixtureRoot }, {
437
+ buildScanContext: async () => fakeScanContext,
438
+ resolveBindings: async () => ({
439
+ symbol: 'CharacterList',
440
+ scriptPath: 'Assets/Scripts/CharacterList.cs',
441
+ scriptGuid: '11111111111111111111111111111111',
442
+ resourceBindings: [
443
+ {
444
+ resourcePath: 'Assets/NEON/DataAssets/CharacterList.asset',
445
+ resourceType: 'asset',
446
+ bindingKind: 'prefab-instance',
447
+ componentObjectId: '11400000',
448
+ evidence: { line: 1, lineText: 'guid: 1111' },
449
+ serializedFields: { scalarFields: [], referenceFields: [] },
450
+ resolvedReferences: [],
451
+ assetRefPaths: [
452
+ {
453
+ parentFieldName: 'Values',
454
+ fieldName: '_Head_Ref',
455
+ relativePath: 'Assets/NEON/Art/Sprites/UI/0_pixle/ui_character_head/hero_head_Nik.png',
456
+ sourceLayer: 'asset',
457
+ isEmpty: false,
458
+ isSprite: true,
459
+ },
460
+ ],
461
+ },
462
+ ],
463
+ serializedFields: { scalarFields: [], referenceFields: [] },
464
+ unityDiagnostics: [],
465
+ }),
466
+ });
467
+ const component = [...graph.iterNodes()].find((node) => node.label === 'CodeElement');
468
+ assert.ok(component);
469
+ const payload = JSON.parse(String(component.properties.description));
470
+ assert.equal(payload.assetRefPaths?.length, 1);
471
+ assert.equal(payload.assetRefPaths?.[0]?.fieldName, '_Head_Ref');
472
+ assert.equal(payload.assetRefPaths?.[0]?.isSprite, true);
473
+ });
474
+ test('processUnityResources writes full unity payload when payloadMode=full', async () => {
475
+ const graph = createKnowledgeGraph();
476
+ const classId = generateId('Class', 'Assets/Scripts/Full.cs:FullSymbol');
477
+ graph.addNode({
478
+ id: classId,
479
+ label: 'Class',
480
+ properties: { name: 'FullSymbol', filePath: 'Assets/Scripts/Full.cs' },
481
+ });
482
+ const fakeScanContext = {
483
+ symbolToScriptPath: new Map([['FullSymbol', 'Assets/Scripts/Full.cs']]),
484
+ scriptPathToGuid: new Map([['Assets/Scripts/Full.cs', '11111111111111111111111111111111']]),
485
+ guidToResourceHits: new Map([
486
+ ['11111111111111111111111111111111', [{ resourcePath: 'Assets/Scene/Test.unity', resourceType: 'scene', line: 9, lineText: 'guid: 1111' }]],
487
+ ]),
488
+ resourceDocCache: new Map(),
489
+ };
490
+ await processUnityResources(graph, { repoPath: fixtureRoot, payloadMode: 'full' }, {
491
+ buildScanContext: async () => fakeScanContext,
492
+ resolveBindings: async () => ({
493
+ symbol: 'FullSymbol',
494
+ scriptPath: 'Assets/Scripts/Full.cs',
495
+ scriptGuid: '11111111111111111111111111111111',
496
+ resourceBindings: [
497
+ {
498
+ resourcePath: 'Assets/Scene/Test.unity',
499
+ resourceType: 'scene',
500
+ bindingKind: 'scene-override',
501
+ componentObjectId: '11400000',
502
+ evidence: { line: 9, lineText: 'guid: 1111' },
503
+ serializedFields: { scalarFields: [], referenceFields: [] },
504
+ },
505
+ ],
506
+ serializedFields: { scalarFields: [], referenceFields: [] },
507
+ unityDiagnostics: [],
508
+ }),
509
+ });
510
+ const component = [...graph.iterNodes()].find((node) => node.label === 'CodeElement');
511
+ assert.ok(component);
512
+ const payload = JSON.parse(String(component.properties.description));
513
+ assert.equal(payload.resourcePath, 'Assets/Scene/Test.unity');
514
+ assert.equal(payload.resourceType, 'scene');
515
+ assert.equal(payload.evidence?.line, 9);
516
+ });
517
+ test('processUnityResources aggregates repetitive diagnostics with capped samples', async () => {
518
+ const graph = createKnowledgeGraph();
519
+ const symbols = ['DiagA', 'DiagB', 'DiagC'];
520
+ const fakeScanContext = {
521
+ symbolToScriptPath: new Map(symbols.map((symbol) => [symbol, `Assets/Scripts/${symbol}.cs`])),
522
+ scriptPathToGuid: new Map(symbols.map((symbol, index) => [`Assets/Scripts/${symbol}.cs`, `${index + 1}`.repeat(32)])),
523
+ guidToResourceHits: new Map(symbols.map((symbol, index) => [
524
+ `${index + 1}`.repeat(32),
525
+ [{ resourcePath: `Assets/Scene/${symbol}.unity`, resourceType: 'scene', line: 1, lineText: `guid: ${index + 1}` }],
526
+ ])),
527
+ resourceDocCache: new Map(),
528
+ };
529
+ for (const symbol of symbols) {
530
+ const classId = generateId('Class', `Assets/Scripts/${symbol}.cs:${symbol}`);
531
+ graph.addNode({
532
+ id: classId,
533
+ label: 'Class',
534
+ properties: { name: symbol, filePath: `Assets/Scripts/${symbol}.cs` },
535
+ });
536
+ }
537
+ const result = await processUnityResources(graph, { repoPath: fixtureRoot }, {
538
+ buildScanContext: async () => fakeScanContext,
539
+ resolveBindings: async ({ symbol }) => {
540
+ if (symbol === 'DiagC') {
541
+ throw new Error('Unity symbol "SharedName" is ambiguous.');
542
+ }
543
+ return {
544
+ symbol: String(symbol),
545
+ scriptPath: `Assets/Scripts/${symbol}.cs`,
546
+ scriptGuid: '1'.repeat(32),
547
+ resourceBindings: [],
548
+ serializedFields: { scalarFields: [], referenceFields: [] },
549
+ unityDiagnostics: [
550
+ 'No MonoBehaviour block matched script guid 123 in Assets/Scene/A.unity.',
551
+ 'No MonoBehaviour block matched script guid 123 in Assets/Scene/B.unity.',
552
+ ],
553
+ };
554
+ },
555
+ });
556
+ assert.ok(result.diagnostics.some((line) => line.includes('diagnostics: aggregated')));
557
+ assert.ok(result.diagnostics.some((line) => line.includes('category=no-monobehaviour-match') && line.includes('count=4')));
558
+ assert.ok(result.diagnostics.some((line) => line.includes('category=ambiguous-symbol') && line.includes('count=1')));
559
+ const monoSamples = result.diagnostics.filter((line) => line.includes('sample[no-monobehaviour-match]'));
560
+ assert.ok(monoSamples.length <= 3);
561
+ assert.equal(result.diagnostics.filter((line) => line.startsWith('No MonoBehaviour block matched')).length, 0);
562
+ });
563
+ test('processUnityResources passes class symbol declarations to scan context builder', async () => {
564
+ const graph = createKnowledgeGraph();
565
+ for (const filePath of ['Assets/Scripts/Alpha.cs', 'Assets/Scripts/Beta.cs']) {
566
+ const symbol = path.basename(filePath, '.cs');
567
+ graph.addNode({
568
+ id: generateId('Class', `${filePath}:${symbol}`),
569
+ label: 'Class',
570
+ properties: {
571
+ name: symbol,
572
+ filePath,
573
+ },
574
+ });
575
+ }
576
+ let capturedInput;
577
+ await processUnityResources(graph, { repoPath: fixtureRoot }, {
578
+ buildScanContext: async (input) => {
579
+ capturedInput = input;
580
+ return {
581
+ symbolToScriptPath: new Map(),
582
+ scriptPathToGuid: new Map(),
583
+ guidToResourceHits: new Map(),
584
+ resourceDocCache: new Map(),
585
+ };
586
+ },
587
+ resolveBindings: async () => ({
588
+ symbol: '',
589
+ scriptPath: '',
590
+ scriptGuid: '',
591
+ resourceBindings: [],
592
+ serializedFields: { scalarFields: [], referenceFields: [] },
593
+ unityDiagnostics: [],
594
+ }),
595
+ });
596
+ assert.ok(Array.isArray(capturedInput.symbolDeclarations));
597
+ assert.equal(capturedInput.symbolDeclarations.length, 2);
598
+ assert.deepEqual(capturedInput.symbolDeclarations.map((entry) => entry.symbol).sort(), ['Alpha', 'Beta']);
599
+ });
@@ -10,11 +10,17 @@ export declare const initKuzu: (dbPath: string) => Promise<{
10
10
  */
11
11
  export declare const withKuzuDb: <T>(dbPath: string, operation: () => Promise<T>) => Promise<T>;
12
12
  export type KuzuProgressCallback = (message: string) => void;
13
+ export interface FallbackInsertStats {
14
+ attempted: number;
15
+ succeeded: number;
16
+ failed: number;
17
+ }
13
18
  export declare const loadGraphToKuzu: (graph: KnowledgeGraph, repoPath: string, storagePath: string, onProgress?: KuzuProgressCallback) => Promise<{
14
19
  success: boolean;
15
20
  insertedRels: number;
16
21
  skippedRels: number;
17
22
  warnings: string[];
23
+ fallbackStats: FallbackInsertStats;
18
24
  }>;
19
25
  /**
20
26
  * Insert a single node to KuzuDB