@veewo/gitnexus 1.4.10-rc → 1.4.11-rc

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 (84) hide show
  1. package/dist/benchmark/u2-e2e/live-evidence-validator.d.ts +19 -0
  2. package/dist/benchmark/u2-e2e/live-evidence-validator.js +87 -0
  3. package/dist/benchmark/u2-e2e/live-evidence-validator.test.d.ts +1 -0
  4. package/dist/benchmark/u2-e2e/live-evidence-validator.test.js +33 -0
  5. package/dist/benchmark/u2-e2e/neonspark-full-e2e.js +23 -4
  6. package/dist/benchmark/u2-e2e/reload-v1-acceptance-runner.d.ts +38 -0
  7. package/dist/benchmark/u2-e2e/reload-v1-acceptance-runner.js +206 -0
  8. package/dist/benchmark/u2-e2e/reload-v1-acceptance-runner.test.d.ts +1 -0
  9. package/dist/benchmark/u2-e2e/reload-v1-acceptance-runner.test.js +72 -0
  10. package/dist/benchmark/u2-e2e/report.d.ts +1 -0
  11. package/dist/benchmark/u2-e2e/report.js +2 -0
  12. package/dist/benchmark/u2-e2e/retrieval-runner.d.ts +34 -0
  13. package/dist/benchmark/u2-e2e/retrieval-runner.js +95 -5
  14. package/dist/benchmark/u2-e2e/retrieval-runner.test.js +161 -2
  15. package/dist/cli/ai-context.js +31 -1
  16. package/dist/cli/ai-context.test.js +10 -0
  17. package/dist/cli/analyze-summary.d.ts +1 -0
  18. package/dist/cli/analyze-summary.js +21 -0
  19. package/dist/cli/analyze-summary.test.js +7 -1
  20. package/dist/cli/analyze.js +3 -10
  21. package/dist/cli/eval-server.js +1 -1
  22. package/dist/cli/index.js +2 -0
  23. package/dist/cli/setup.js +9 -0
  24. package/dist/cli/setup.test.js +2 -0
  25. package/dist/cli/tool.d.ts +2 -0
  26. package/dist/cli/tool.js +2 -0
  27. package/dist/core/ingestion/pipeline.js +24 -3
  28. package/dist/core/ingestion/process-processor.d.ts +6 -0
  29. package/dist/core/ingestion/process-processor.js +188 -7
  30. package/dist/core/ingestion/unity-lifecycle-config.d.ts +5 -0
  31. package/dist/core/ingestion/unity-lifecycle-config.js +25 -0
  32. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.d.ts +26 -0
  33. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.js +384 -0
  34. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.test.d.ts +1 -0
  35. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.test.js +541 -0
  36. package/dist/core/ingestion/unity-resource-processor.test.js +81 -0
  37. package/dist/core/lbug/csv-generator.js +11 -1
  38. package/dist/core/lbug/fallback-relationship-replay.d.ts +21 -0
  39. package/dist/core/lbug/fallback-relationship-replay.js +39 -0
  40. package/dist/core/lbug/fallback-relationship-replay.test.d.ts +1 -0
  41. package/dist/core/lbug/fallback-relationship-replay.test.js +25 -0
  42. package/dist/core/lbug/lbug-adapter.d.ts +5 -0
  43. package/dist/core/lbug/lbug-adapter.js +22 -23
  44. package/dist/core/lbug/schema.d.ts +2 -2
  45. package/dist/core/lbug/schema.js +9 -0
  46. package/dist/core/lbug/schema.test.js +1 -0
  47. package/dist/mcp/local/local-backend.d.ts +1 -1
  48. package/dist/mcp/local/local-backend.js +339 -50
  49. package/dist/mcp/local/local-backend.unity-merge.test.js +1 -1
  50. package/dist/mcp/local/process-confidence.d.ts +19 -0
  51. package/dist/mcp/local/process-confidence.js +29 -0
  52. package/dist/mcp/local/process-confidence.test.d.ts +1 -0
  53. package/dist/mcp/local/process-confidence.test.js +36 -0
  54. package/dist/mcp/local/process-evidence.d.ts +28 -0
  55. package/dist/mcp/local/process-evidence.js +65 -0
  56. package/dist/mcp/local/process-evidence.test.d.ts +1 -0
  57. package/dist/mcp/local/process-evidence.test.js +56 -0
  58. package/dist/mcp/local/runtime-chain-evidence.d.ts +7 -0
  59. package/dist/mcp/local/runtime-chain-evidence.js +13 -0
  60. package/dist/mcp/local/runtime-chain-evidence.test.d.ts +1 -0
  61. package/dist/mcp/local/runtime-chain-evidence.test.js +24 -0
  62. package/dist/mcp/local/runtime-chain-verify.d.ts +37 -0
  63. package/dist/mcp/local/runtime-chain-verify.js +221 -0
  64. package/dist/mcp/local/runtime-chain-verify.test.d.ts +1 -0
  65. package/dist/mcp/local/runtime-chain-verify.test.js +56 -0
  66. package/dist/mcp/local/unity-process-confidence-config.d.ts +1 -0
  67. package/dist/mcp/local/unity-process-confidence-config.js +4 -0
  68. package/dist/mcp/local/unity-runtime-chain-verify-config.d.ts +1 -0
  69. package/dist/mcp/local/unity-runtime-chain-verify-config.js +10 -0
  70. package/dist/mcp/local/unity-runtime-hydration.d.ts +50 -0
  71. package/dist/mcp/local/unity-runtime-hydration.js +323 -0
  72. package/dist/mcp/local/unity-runtime-hydration.test.d.ts +1 -0
  73. package/dist/mcp/local/unity-runtime-hydration.test.js +108 -0
  74. package/dist/mcp/resources.js +12 -2
  75. package/dist/mcp/tools.js +32 -0
  76. package/package.json +1 -1
  77. package/skills/_shared/unity-runtime-process-contract.md +38 -0
  78. package/skills/gitnexus-cli.md +16 -0
  79. package/skills/gitnexus-debugging.md +6 -0
  80. package/skills/gitnexus-exploring.md +6 -0
  81. package/skills/gitnexus-guide.md +4 -0
  82. package/skills/gitnexus-impact-analysis.md +6 -0
  83. package/skills/gitnexus-pr-review.md +5 -0
  84. package/skills/gitnexus-refactoring.md +4 -0
@@ -0,0 +1,541 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { generateId } from '../../lib/utils.js';
4
+ import { createKnowledgeGraph } from '../graph/graph.js';
5
+ import { applyUnityLifecycleSyntheticCalls, detectUnityLifecycleHosts, } from './unity-lifecycle-synthetic-calls.js';
6
+ const PLACEHOLDER_RE = /(TODO|TBD|\/placeholder\/)/i;
7
+ const addClass = (graph, input) => {
8
+ const classId = generateId('Class', `${input.filePath}:${input.className}`);
9
+ graph.addNode({
10
+ id: classId,
11
+ label: 'Class',
12
+ properties: {
13
+ name: input.className,
14
+ filePath: input.filePath,
15
+ },
16
+ });
17
+ if (input.baseType) {
18
+ const baseTypeId = generateId('Type', input.baseType);
19
+ graph.addNode({
20
+ id: baseTypeId,
21
+ label: 'Type',
22
+ properties: {
23
+ name: input.baseType,
24
+ filePath: '',
25
+ },
26
+ });
27
+ graph.addRelationship({
28
+ id: generateId('EXTENDS', `${classId}->${baseTypeId}`),
29
+ type: 'EXTENDS',
30
+ sourceId: classId,
31
+ targetId: baseTypeId,
32
+ confidence: 1,
33
+ reason: 'test-fixture',
34
+ });
35
+ }
36
+ const methodNames = [
37
+ ...(input.callbackNames ?? []),
38
+ ...(input.loaderNames ?? []),
39
+ ...(input.extraMethods ?? []),
40
+ ];
41
+ for (const methodName of methodNames) {
42
+ const methodId = generateId('Method', `${input.filePath}:${input.className}.${methodName}`);
43
+ graph.addNode({
44
+ id: methodId,
45
+ label: 'Method',
46
+ properties: {
47
+ name: methodName,
48
+ filePath: input.filePath,
49
+ },
50
+ });
51
+ graph.addRelationship({
52
+ id: generateId('HAS_METHOD', `${classId}->${methodId}`),
53
+ type: 'HAS_METHOD',
54
+ sourceId: classId,
55
+ targetId: methodId,
56
+ confidence: 1,
57
+ reason: 'test-fixture',
58
+ });
59
+ }
60
+ return classId;
61
+ };
62
+ test('detects Unity lifecycle hosts and callback anchors', () => {
63
+ const graph = createKnowledgeGraph();
64
+ const monoClassId = addClass(graph, {
65
+ className: 'GunGraphMB',
66
+ filePath: 'Assets/Scripts/GunGraphMB.cs',
67
+ baseType: 'MonoBehaviour',
68
+ callbackNames: ['Awake', 'OnEnable', 'Start', 'Update'],
69
+ loaderNames: ['RegisterEvents', 'StartRoutineWithEvents'],
70
+ });
71
+ addClass(graph, {
72
+ className: 'ReloadConfig',
73
+ filePath: 'Assets/Scripts/ReloadConfig.cs',
74
+ baseType: 'ScriptableObject',
75
+ callbackNames: ['OnEnable'],
76
+ loaderNames: ['GetValue', 'CheckReload'],
77
+ });
78
+ addClass(graph, {
79
+ className: 'PlainService',
80
+ filePath: 'Assets/Scripts/PlainService.cs',
81
+ callbackNames: ['Start'],
82
+ extraMethods: ['DoWork'],
83
+ });
84
+ const hosts = detectUnityLifecycleHosts(graph);
85
+ const hostIds = new Set(hosts.map((host) => host.classNode.id));
86
+ assert.equal(hostIds.has(monoClassId), true);
87
+ assert.equal(hosts.length >= 2, true);
88
+ assert.equal(hosts.some((host) => host.baseType === 'MonoBehaviour'), true);
89
+ assert.equal(hosts.some((host) => host.baseType === 'ScriptableObject'), true);
90
+ assert.equal(hosts.some((host) => host.classNode.properties.name === 'PlainService'), false);
91
+ const monoHost = hosts.find((host) => host.classNode.id === monoClassId);
92
+ assert.ok(monoHost);
93
+ assert.deepEqual(monoHost.lifecycleCallbacks.map((method) => method.properties.name).sort(), ['Awake', 'OnEnable', 'Start', 'Update']);
94
+ });
95
+ test('emits bounded synthetic CALLS edges with reason tags', () => {
96
+ const graph = createKnowledgeGraph();
97
+ const plainClassId = addClass(graph, {
98
+ className: 'PlainService',
99
+ filePath: 'Assets/Scripts/PlainService.cs',
100
+ callbackNames: ['Start'],
101
+ extraMethods: ['DoWork'],
102
+ });
103
+ addClass(graph, {
104
+ className: 'GunGraphMB',
105
+ filePath: 'Assets/Scripts/GunGraphMB.cs',
106
+ baseType: 'MonoBehaviour',
107
+ callbackNames: ['Awake', 'Start'],
108
+ loaderNames: ['RegisterEvents', 'StartRoutineWithEvents'],
109
+ });
110
+ addClass(graph, {
111
+ className: 'ReloadConfig',
112
+ filePath: 'Assets/Scripts/ReloadConfig.cs',
113
+ baseType: 'ScriptableObject',
114
+ callbackNames: ['OnEnable'],
115
+ loaderNames: ['GetValue', 'CheckReload'],
116
+ });
117
+ const result = applyUnityLifecycleSyntheticCalls(graph, {
118
+ enabled: true,
119
+ maxSyntheticEdgesPerClass: 4,
120
+ maxSyntheticEdgesTotal: 10,
121
+ });
122
+ const edges = [...graph.iterRelationships()].filter((edge) => edge.reason.includes('unity-'));
123
+ const plainMethods = new Set([...graph.iterRelationships()]
124
+ .filter((edge) => edge.type === 'HAS_METHOD' && edge.sourceId === plainClassId)
125
+ .map((edge) => edge.targetId));
126
+ assert.equal(result.syntheticEdgeCount, edges.length);
127
+ assert.equal(result.syntheticEdgeCount > 0, true);
128
+ assert.equal(result.syntheticEdgeCount <= 10, true);
129
+ assert.equal(edges.every((edge) => edge.type === 'CALLS'), true);
130
+ assert.equal(edges.every((edge) => edge.confidence < 1), true);
131
+ assert.equal(edges.every((edge) => /unity-(lifecycle|runtime-loader)-synthetic/.test(edge.reason)), true);
132
+ assert.equal(edges.some((edge) => edge.sourceId.includes('unity-runtime-root')), true);
133
+ assert.equal(edges.some((edge) => plainMethods.has(edge.sourceId) || plainMethods.has(edge.targetId)), false);
134
+ });
135
+ test('rejects placeholder paths and fake compliance', () => {
136
+ const graph = createKnowledgeGraph();
137
+ addClass(graph, {
138
+ className: 'FakeHost',
139
+ filePath: '/placeholder/FakeHost.cs',
140
+ baseType: 'MonoBehaviour',
141
+ callbackNames: ['Awake'],
142
+ loaderNames: ['RegisterEvents'],
143
+ });
144
+ addClass(graph, {
145
+ className: 'RealHost',
146
+ filePath: 'Assets/Scripts/RealHost.cs',
147
+ baseType: 'MonoBehaviour',
148
+ callbackNames: ['Awake'],
149
+ loaderNames: ['RegisterEvents'],
150
+ });
151
+ const result = applyUnityLifecycleSyntheticCalls(graph, {
152
+ enabled: true,
153
+ maxSyntheticEdgesPerClass: 4,
154
+ maxSyntheticEdgesTotal: 8,
155
+ });
156
+ const syntheticRoot = [...graph.iterNodes()].find((node) => node.label === 'Method' && node.properties.name === 'unity-runtime-root');
157
+ const syntheticEdges = [...graph.iterRelationships()].filter((edge) => edge.reason.includes('unity-'));
158
+ assert.equal(result.rejectedHostCount >= 1, true);
159
+ assert.equal(result.syntheticEdgeCount > 0, true);
160
+ assert.ok(syntheticRoot);
161
+ assert.equal(syntheticRoot.properties.filePath === '' || !PLACEHOLDER_RE.test(syntheticRoot.properties.filePath), true);
162
+ assert.equal(syntheticEdges.every((edge) => !PLACEHOLDER_RE.test(`${edge.sourceId} ${edge.targetId} ${edge.reason}`)), true);
163
+ });
164
+ test('emits no synthetic edges when there is no Unity host signal', () => {
165
+ const graph = createKnowledgeGraph();
166
+ addClass(graph, {
167
+ className: 'PlainService',
168
+ filePath: 'Assets/Scripts/PlainService.cs',
169
+ callbackNames: ['Start', 'Update'],
170
+ loaderNames: ['RegisterEvents', 'CheckReload'],
171
+ });
172
+ const result = applyUnityLifecycleSyntheticCalls(graph, {
173
+ enabled: true,
174
+ maxSyntheticEdgesPerClass: 4,
175
+ maxSyntheticEdgesTotal: 8,
176
+ });
177
+ const syntheticEdges = [...graph.iterRelationships()].filter((edge) => edge.reason.includes('unity-'));
178
+ const runtimeRoot = [...graph.iterNodes()].find((node) => node.id.includes('unity-runtime-root'));
179
+ assert.equal(result.syntheticEdgeCount, 0);
180
+ assert.equal(syntheticEdges.length, 0);
181
+ assert.equal(runtimeRoot, undefined);
182
+ });
183
+ test('detects Unity hosts through transitive inheritance chains', () => {
184
+ const graph = createKnowledgeGraph();
185
+ const scriptableObjectId = generateId('Type', 'ScriptableObject');
186
+ graph.addNode({
187
+ id: scriptableObjectId,
188
+ label: 'Type',
189
+ properties: {
190
+ name: 'ScriptableObject',
191
+ filePath: '',
192
+ },
193
+ });
194
+ const nodeGraphId = generateId('Class', 'Packages/com.example/NodeGraph.cs:NodeGraph');
195
+ graph.addNode({
196
+ id: nodeGraphId,
197
+ label: 'Class',
198
+ properties: {
199
+ name: 'NodeGraph',
200
+ filePath: 'Packages/com.example/NodeGraph.cs',
201
+ },
202
+ });
203
+ graph.addRelationship({
204
+ id: generateId('EXTENDS', `${nodeGraphId}->${scriptableObjectId}`),
205
+ type: 'EXTENDS',
206
+ sourceId: nodeGraphId,
207
+ targetId: scriptableObjectId,
208
+ confidence: 1,
209
+ reason: 'test-fixture',
210
+ });
211
+ const gameNodeGraphId = generateId('Class', 'Assets/NEON/Code/Game/Graph/Graphs/GameNodeGraph.cs:GameNodeGraph');
212
+ graph.addNode({
213
+ id: gameNodeGraphId,
214
+ label: 'Class',
215
+ properties: {
216
+ name: 'GameNodeGraph',
217
+ filePath: 'Assets/NEON/Code/Game/Graph/Graphs/GameNodeGraph.cs',
218
+ },
219
+ });
220
+ graph.addRelationship({
221
+ id: generateId('EXTENDS', `${gameNodeGraphId}->${nodeGraphId}`),
222
+ type: 'EXTENDS',
223
+ sourceId: gameNodeGraphId,
224
+ targetId: nodeGraphId,
225
+ confidence: 1,
226
+ reason: 'test-fixture',
227
+ });
228
+ const gunGraphId = addClass(graph, {
229
+ className: 'GunGraph',
230
+ filePath: 'Assets/NEON/Code/Game/Graph/Graphs/GunGraph.cs',
231
+ callbackNames: ['OnEnable'],
232
+ loaderNames: ['RegisterEvents', 'StartRoutineWithEvents'],
233
+ });
234
+ graph.addRelationship({
235
+ id: generateId('EXTENDS', `${gunGraphId}->${gameNodeGraphId}`),
236
+ type: 'EXTENDS',
237
+ sourceId: gunGraphId,
238
+ targetId: gameNodeGraphId,
239
+ confidence: 1,
240
+ reason: 'test-fixture',
241
+ });
242
+ const hosts = detectUnityLifecycleHosts(graph);
243
+ const gunGraphHost = hosts.find((host) => host.classNode.id === gunGraphId);
244
+ assert.ok(gunGraphHost);
245
+ assert.equal(gunGraphHost.baseType, 'ScriptableObject');
246
+ assert.deepEqual(gunGraphHost.loaderAnchors.map((method) => method.properties.name), ['RegisterEvents', 'StartRoutineWithEvents']);
247
+ });
248
+ test('prioritizes gameplay lifecycle hosts when synthetic edge budget is tight', () => {
249
+ const graph = createKnowledgeGraph();
250
+ for (let i = 0; i < 6; i += 1) {
251
+ addClass(graph, {
252
+ className: `GenericHost${i}`,
253
+ filePath: `Assets/Scripts/GenericHost${i}.cs`,
254
+ baseType: 'MonoBehaviour',
255
+ callbackNames: ['Awake', 'Start'],
256
+ loaderNames: i % 2 === 0 ? ['RegisterEvents'] : [],
257
+ });
258
+ }
259
+ addClass(graph, {
260
+ className: 'GunGraphMB',
261
+ filePath: 'Assets/NEON/Code/Game/Core/GunGraphMB.cs',
262
+ baseType: 'MonoBehaviour',
263
+ callbackNames: ['Awake', 'OnEnable'],
264
+ loaderNames: ['RegisterGraphEvents', 'RegisterEvents', 'StartRoutineWithEvents'],
265
+ });
266
+ addClass(graph, {
267
+ className: 'ReloadConfig',
268
+ filePath: 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadConfig.cs',
269
+ baseType: 'ScriptableObject',
270
+ callbackNames: ['OnEnable'],
271
+ loaderNames: ['GetValue', 'CheckReload', 'ReloadRoutine'],
272
+ });
273
+ const result = applyUnityLifecycleSyntheticCalls(graph, {
274
+ enabled: true,
275
+ maxSyntheticEdgesPerClass: 4,
276
+ maxSyntheticEdgesTotal: 8,
277
+ });
278
+ const runtimeEdges = [...graph.iterRelationships()].filter((edge) => edge.type === 'CALLS' &&
279
+ (edge.reason === 'unity-lifecycle-synthetic' || edge.reason === 'unity-runtime-loader-synthetic'));
280
+ const targets = new Set(runtimeEdges.map((edge) => edge.targetId));
281
+ assert.equal(result.syntheticEdgeCount, 8);
282
+ assert.equal([...targets].some((id) => id.includes('Assets/NEON/Code/Game/Core/GunGraphMB.cs')), true);
283
+ assert.equal([...targets].some((id) => id.includes('Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadConfig.cs')), true);
284
+ });
285
+ test('resolves named class inheritance targets when direct target node lookup is ambiguous', () => {
286
+ const graph = createKnowledgeGraph();
287
+ const nodeGraphId = generateId('Class', 'Assets/Plugins/xNode/Scripts/NodeGraph.cs:NodeGraph');
288
+ graph.addNode({
289
+ id: nodeGraphId,
290
+ label: 'Class',
291
+ properties: {
292
+ name: 'NodeGraph',
293
+ filePath: 'Assets/Plugins/xNode/Scripts/NodeGraph.cs',
294
+ },
295
+ });
296
+ graph.addRelationship({
297
+ id: generateId('EXTENDS', `${nodeGraphId}->Class:ScriptableObject`),
298
+ type: 'EXTENDS',
299
+ sourceId: nodeGraphId,
300
+ targetId: 'Class:ScriptableObject',
301
+ confidence: 1,
302
+ reason: 'test-fixture',
303
+ });
304
+ const gameNodeGraphId = generateId('Class', 'Assets/NEON/Code/Game/Graph/Graphs/GameNodeGraph.cs:GameNodeGraph');
305
+ graph.addNode({
306
+ id: gameNodeGraphId,
307
+ label: 'Class',
308
+ properties: {
309
+ name: 'GameNodeGraph',
310
+ filePath: 'Assets/NEON/Code/Game/Graph/Graphs/GameNodeGraph.cs',
311
+ },
312
+ });
313
+ graph.addRelationship({
314
+ id: generateId('EXTENDS', `${gameNodeGraphId}->Class:NodeGraph`),
315
+ type: 'EXTENDS',
316
+ sourceId: gameNodeGraphId,
317
+ targetId: 'Class:NodeGraph',
318
+ confidence: 1,
319
+ reason: 'test-fixture',
320
+ });
321
+ const gunGraphId = addClass(graph, {
322
+ className: 'GunGraph',
323
+ filePath: 'Assets/NEON/Code/Game/Graph/Graphs/GunGraph.cs',
324
+ loaderNames: ['RegisterEvents', 'StartRoutineWithEvents'],
325
+ });
326
+ graph.addRelationship({
327
+ id: generateId('EXTENDS', `${gunGraphId}->${gameNodeGraphId}`),
328
+ type: 'EXTENDS',
329
+ sourceId: gunGraphId,
330
+ targetId: gameNodeGraphId,
331
+ confidence: 1,
332
+ reason: 'test-fixture',
333
+ });
334
+ const hosts = detectUnityLifecycleHosts(graph);
335
+ const gunGraphHost = hosts.find((host) => host.classNode.id === gunGraphId);
336
+ assert.ok(gunGraphHost);
337
+ assert.equal(gunGraphHost.baseType, 'ScriptableObject');
338
+ });
339
+ test('emits deterministic runtime loader bridge chain after pre-bridge budget is exhausted', () => {
340
+ const graph = createKnowledgeGraph();
341
+ addClass(graph, {
342
+ className: 'GunGraphMB',
343
+ filePath: 'Assets/NEON/Code/Game/Core/GunGraphMB.cs',
344
+ baseType: 'MonoBehaviour',
345
+ callbackNames: ['Awake', 'OnEnable'],
346
+ loaderNames: ['RegisterGraphEvents'],
347
+ });
348
+ addClass(graph, {
349
+ className: 'BudgetExhauster',
350
+ filePath: 'Assets/NEON/Code/Game/Core/BudgetExhauster.cs',
351
+ baseType: 'MonoBehaviour',
352
+ callbackNames: ['Start'],
353
+ });
354
+ const nodeGraphId = generateId('Class', 'Assets/Plugins/xNode/Scripts/NodeGraph.cs:NodeGraph');
355
+ graph.addNode({
356
+ id: nodeGraphId,
357
+ label: 'Class',
358
+ properties: {
359
+ name: 'NodeGraph',
360
+ filePath: 'Assets/Plugins/xNode/Scripts/NodeGraph.cs',
361
+ },
362
+ });
363
+ graph.addRelationship({
364
+ id: generateId('EXTENDS', `${nodeGraphId}->Class:ScriptableObject`),
365
+ type: 'EXTENDS',
366
+ sourceId: nodeGraphId,
367
+ targetId: 'Class:ScriptableObject',
368
+ confidence: 1,
369
+ reason: 'test-fixture',
370
+ });
371
+ const gameNodeGraphId = generateId('Class', 'Assets/NEON/Code/Game/Graph/Graphs/GameNodeGraph.cs:GameNodeGraph');
372
+ graph.addNode({
373
+ id: gameNodeGraphId,
374
+ label: 'Class',
375
+ properties: {
376
+ name: 'GameNodeGraph',
377
+ filePath: 'Assets/NEON/Code/Game/Graph/Graphs/GameNodeGraph.cs',
378
+ },
379
+ });
380
+ graph.addRelationship({
381
+ id: generateId('EXTENDS', `${gameNodeGraphId}->Class:NodeGraph`),
382
+ type: 'EXTENDS',
383
+ sourceId: gameNodeGraphId,
384
+ targetId: 'Class:NodeGraph',
385
+ confidence: 1,
386
+ reason: 'test-fixture',
387
+ });
388
+ const gunGraphId = addClass(graph, {
389
+ className: 'GunGraph',
390
+ filePath: 'Assets/NEON/Code/Game/Graph/Graphs/GunGraph.cs',
391
+ loaderNames: ['RegisterEvents', 'StartRoutineWithEvents'],
392
+ });
393
+ graph.addRelationship({
394
+ id: generateId('EXTENDS', `${gunGraphId}->${gameNodeGraphId}`),
395
+ type: 'EXTENDS',
396
+ sourceId: gunGraphId,
397
+ targetId: gameNodeGraphId,
398
+ confidence: 1,
399
+ reason: 'test-fixture',
400
+ });
401
+ const reloadBaseId = addClass(graph, {
402
+ className: 'ReloadBase',
403
+ filePath: 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadBase.cs',
404
+ loaderNames: ['GetValue', 'CheckReload'],
405
+ });
406
+ graph.addRelationship({
407
+ id: generateId('EXTENDS', `${reloadBaseId}->Class:NodeGraph`),
408
+ type: 'EXTENDS',
409
+ sourceId: reloadBaseId,
410
+ targetId: 'Class:NodeGraph',
411
+ confidence: 1,
412
+ reason: 'test-fixture',
413
+ });
414
+ const reloadId = addClass(graph, {
415
+ className: 'Reload',
416
+ filePath: 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/Reload.cs',
417
+ loaderNames: ['ReloadRoutine'],
418
+ });
419
+ graph.addRelationship({
420
+ id: generateId('EXTENDS', `${reloadId}->${reloadBaseId}`),
421
+ type: 'EXTENDS',
422
+ sourceId: reloadId,
423
+ targetId: reloadBaseId,
424
+ confidence: 1,
425
+ reason: 'test-fixture',
426
+ });
427
+ const result = applyUnityLifecycleSyntheticCalls(graph, {
428
+ enabled: true,
429
+ maxSyntheticEdgesPerClass: 12,
430
+ maxSyntheticEdgesTotal: 10,
431
+ });
432
+ const syntheticEdges = [...graph.iterRelationships()].filter((edge) => edge.type === 'CALLS' && edge.reason === 'unity-runtime-loader-synthetic');
433
+ const syntheticPairs = new Set(syntheticEdges.map((edge) => `${edge.sourceId}->${edge.targetId}`));
434
+ assert.equal(result.syntheticEdgeCount, 10);
435
+ assert.equal(syntheticPairs.has(`${generateId('Method', 'Assets/NEON/Code/Game/Core/GunGraphMB.cs:GunGraphMB.RegisterGraphEvents')}->${generateId('Method', 'Assets/NEON/Code/Game/Graph/Graphs/GunGraph.cs:GunGraph.RegisterEvents')}`), true);
436
+ assert.equal(syntheticPairs.has(`${generateId('Method', 'Assets/NEON/Code/Game/Graph/Graphs/GunGraph.cs:GunGraph.RegisterEvents')}->${generateId('Method', 'Assets/NEON/Code/Game/Graph/Graphs/GunGraph.cs:GunGraph.StartRoutineWithEvents')}`), true);
437
+ assert.equal(syntheticPairs.has(`${generateId('Method', 'Assets/NEON/Code/Game/Graph/Graphs/GunGraph.cs:GunGraph.StartRoutineWithEvents')}->${generateId('Method', 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadBase.cs:ReloadBase.GetValue')}`), true);
438
+ assert.equal(syntheticPairs.has(`${generateId('Method', 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadBase.cs:ReloadBase.GetValue')}->${generateId('Method', 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadBase.cs:ReloadBase.CheckReload')}`), true);
439
+ assert.equal(syntheticPairs.has(`${generateId('Method', 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadBase.cs:ReloadBase.CheckReload')}->${generateId('Method', 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/Reload.cs:Reload.ReloadRoutine')}`), true);
440
+ });
441
+ test('preserves bridge budget when accepted host count exceeds synthetic edge cap', () => {
442
+ const graph = createKnowledgeGraph();
443
+ for (let i = 0; i < 20; i += 1) {
444
+ addClass(graph, {
445
+ className: `NoiseHost${i}`,
446
+ filePath: `Assets/NEON/Code/Game/Core/NoiseHost${i}.cs`,
447
+ baseType: 'MonoBehaviour',
448
+ callbackNames: ['Awake'],
449
+ loaderNames: ['RegisterEvents'],
450
+ });
451
+ }
452
+ addClass(graph, {
453
+ className: 'GunGraphMB',
454
+ filePath: 'Assets/NEON/Code/Game/Core/GunGraphMB.cs',
455
+ baseType: 'MonoBehaviour',
456
+ callbackNames: ['Awake', 'OnEnable'],
457
+ loaderNames: ['RegisterGraphEvents'],
458
+ });
459
+ const nodeGraphId = generateId('Class', 'Assets/Plugins/xNode/Scripts/NodeGraph.cs:NodeGraph');
460
+ graph.addNode({
461
+ id: nodeGraphId,
462
+ label: 'Class',
463
+ properties: {
464
+ name: 'NodeGraph',
465
+ filePath: 'Assets/Plugins/xNode/Scripts/NodeGraph.cs',
466
+ },
467
+ });
468
+ graph.addRelationship({
469
+ id: generateId('EXTENDS', `${nodeGraphId}->Class:ScriptableObject`),
470
+ type: 'EXTENDS',
471
+ sourceId: nodeGraphId,
472
+ targetId: 'Class:ScriptableObject',
473
+ confidence: 1,
474
+ reason: 'test-fixture',
475
+ });
476
+ const gameNodeGraphId = generateId('Class', 'Assets/NEON/Code/Game/Graph/Graphs/GameNodeGraph.cs:GameNodeGraph');
477
+ graph.addNode({
478
+ id: gameNodeGraphId,
479
+ label: 'Class',
480
+ properties: {
481
+ name: 'GameNodeGraph',
482
+ filePath: 'Assets/NEON/Code/Game/Graph/Graphs/GameNodeGraph.cs',
483
+ },
484
+ });
485
+ graph.addRelationship({
486
+ id: generateId('EXTENDS', `${gameNodeGraphId}->Class:NodeGraph`),
487
+ type: 'EXTENDS',
488
+ sourceId: gameNodeGraphId,
489
+ targetId: 'Class:NodeGraph',
490
+ confidence: 1,
491
+ reason: 'test-fixture',
492
+ });
493
+ const gunGraphId = addClass(graph, {
494
+ className: 'GunGraph',
495
+ filePath: 'Assets/NEON/Code/Game/Graph/Graphs/GunGraph.cs',
496
+ loaderNames: ['RegisterEvents', 'StartRoutineWithEvents'],
497
+ });
498
+ graph.addRelationship({
499
+ id: generateId('EXTENDS', `${gunGraphId}->${gameNodeGraphId}`),
500
+ type: 'EXTENDS',
501
+ sourceId: gunGraphId,
502
+ targetId: gameNodeGraphId,
503
+ confidence: 1,
504
+ reason: 'test-fixture',
505
+ });
506
+ const reloadBaseId = addClass(graph, {
507
+ className: 'ReloadBase',
508
+ filePath: 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadBase.cs',
509
+ loaderNames: ['GetValue', 'CheckReload'],
510
+ });
511
+ graph.addRelationship({
512
+ id: generateId('EXTENDS', `${reloadBaseId}->Class:NodeGraph`),
513
+ type: 'EXTENDS',
514
+ sourceId: reloadBaseId,
515
+ targetId: 'Class:NodeGraph',
516
+ confidence: 1,
517
+ reason: 'test-fixture',
518
+ });
519
+ const reloadId = addClass(graph, {
520
+ className: 'Reload',
521
+ filePath: 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/Reload.cs',
522
+ loaderNames: ['ReloadRoutine'],
523
+ });
524
+ graph.addRelationship({
525
+ id: generateId('EXTENDS', `${reloadId}->${reloadBaseId}`),
526
+ type: 'EXTENDS',
527
+ sourceId: reloadId,
528
+ targetId: reloadBaseId,
529
+ confidence: 1,
530
+ reason: 'test-fixture',
531
+ });
532
+ applyUnityLifecycleSyntheticCalls(graph, {
533
+ enabled: true,
534
+ maxSyntheticEdgesPerClass: 12,
535
+ maxSyntheticEdgesTotal: 12,
536
+ });
537
+ const syntheticPairs = new Set([...graph.iterRelationships()]
538
+ .filter((edge) => edge.type === 'CALLS' && edge.reason === 'unity-runtime-loader-synthetic')
539
+ .map((edge) => `${edge.sourceId}->${edge.targetId}`));
540
+ assert.equal(syntheticPairs.has(`${generateId('Method', 'Assets/NEON/Code/Game/Core/GunGraphMB.cs:GunGraphMB.RegisterGraphEvents')}->${generateId('Method', 'Assets/NEON/Code/Game/Graph/Graphs/GunGraph.cs:GunGraph.RegisterEvents')}`), true);
541
+ });
@@ -1,13 +1,37 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
3
5
  import path from 'node:path';
4
6
  import { fileURLToPath } from 'node:url';
5
7
  import { generateId } from '../../lib/utils.js';
6
8
  import { createKnowledgeGraph } from '../graph/graph.js';
9
+ import { closeLbug, executeQuery, initLbug, loadGraphToLbug } from '../lbug/lbug-adapter.js';
7
10
  import { processUnityResources } from './unity-resource-processor.js';
8
11
  const here = path.dirname(fileURLToPath(import.meta.url));
9
12
  const fixtureRoot = path.resolve(here, '../../../src/core/unity/__fixtures__/mini-unity');
10
13
  const symbols = ['Global', 'BattleMode', 'PlayerActor', 'MainUIManager'];
14
+ const listRelativeFixtureFiles = async (root) => {
15
+ const out = [];
16
+ const stack = ['.'];
17
+ while (stack.length > 0) {
18
+ const relDir = stack.pop();
19
+ const absDir = path.join(root, relDir);
20
+ const entries = await fs.readdir(absDir, { withFileTypes: true });
21
+ for (const entry of entries) {
22
+ const relPath = path.join(relDir, entry.name);
23
+ if (entry.isDirectory()) {
24
+ stack.push(relPath);
25
+ continue;
26
+ }
27
+ if (!entry.isFile()) {
28
+ continue;
29
+ }
30
+ out.push(relPath.startsWith('./') ? relPath.slice(2) : relPath);
31
+ }
32
+ }
33
+ return out;
34
+ };
11
35
  test('processUnityResources does not emit UNITY_COMPONENT_IN or synthetic resource File nodes', async () => {
12
36
  const graph = createKnowledgeGraph();
13
37
  for (const symbol of symbols) {
@@ -56,6 +80,63 @@ test('processUnityResources does not emit UNITY_COMPONENT_IN or synthetic resour
56
80
  assert.ok(result.timingsMs.resolve >= 0);
57
81
  assert.ok(result.timingsMs.graphWrite > 0);
58
82
  });
83
+ test('processUnityResources persists UNITY_RESOURCE_SUMMARY in LadybugDB', async () => {
84
+ const graph = createKnowledgeGraph();
85
+ for (const symbol of symbols) {
86
+ const filePath = `Assets/Scripts/${symbol}.cs`;
87
+ const fileId = generateId('File', filePath);
88
+ const classId = generateId('Class', `${filePath}:${symbol}`);
89
+ graph.addNode({
90
+ id: fileId,
91
+ label: 'File',
92
+ properties: {
93
+ name: `${symbol}.cs`,
94
+ filePath,
95
+ },
96
+ });
97
+ graph.addNode({
98
+ id: classId,
99
+ label: 'Class',
100
+ properties: {
101
+ name: symbol,
102
+ filePath,
103
+ },
104
+ });
105
+ graph.addRelationship({
106
+ id: generateId('DEFINES', `${fileId}->${classId}`),
107
+ type: 'DEFINES',
108
+ sourceId: fileId,
109
+ targetId: classId,
110
+ confidence: 1.0,
111
+ reason: '',
112
+ });
113
+ }
114
+ for (const relPath of await listRelativeFixtureFiles(fixtureRoot)) {
115
+ const normalized = relPath.replace(/\\/g, '/');
116
+ graph.addNode({
117
+ id: generateId('File', normalized),
118
+ label: 'File',
119
+ properties: {
120
+ name: path.basename(normalized),
121
+ filePath: normalized,
122
+ },
123
+ });
124
+ }
125
+ await processUnityResources(graph, { repoPath: fixtureRoot });
126
+ const storageDir = await fs.mkdtemp(path.join(os.tmpdir(), 'unity-resource-summary-'));
127
+ const dbPath = path.join(storageDir, 'graph.lbug');
128
+ try {
129
+ await initLbug(dbPath);
130
+ await loadGraphToLbug(graph, fixtureRoot, storageDir);
131
+ const rows = await executeQuery(`MATCH (c:Class)-[r:CodeRelation {type:'UNITY_RESOURCE_SUMMARY'}]->(f:File)
132
+ RETURN count(r) AS cnt`);
133
+ assert.ok((rows?.[0]?.cnt ?? 0) > 0, 'UNITY_RESOURCE_SUMMARY must persist in LadybugDB');
134
+ }
135
+ finally {
136
+ await closeLbug();
137
+ await fs.rm(storageDir, { recursive: true, force: true });
138
+ }
139
+ });
59
140
  test('processUnityResources builds scan context once and enriches all class nodes', async () => {
60
141
  const graph = createKnowledgeGraph();
61
142
  for (const symbol of symbols) {
@@ -225,7 +225,7 @@ export const streamAllCSVsToDisk = async (graph, repoPath, csvDir) => {
225
225
  const methodWriter = new BufferedCSVWriter(path.join(csvDir, 'method.csv'), methodHeader);
226
226
  const codeElemWriter = new BufferedCSVWriter(path.join(csvDir, 'codeelement.csv'), codeElementHeader);
227
227
  const communityWriter = new BufferedCSVWriter(path.join(csvDir, 'community.csv'), 'id,label,heuristicLabel,keywords,description,enrichedBy,cohesion,symbolCount');
228
- const processWriter = new BufferedCSVWriter(path.join(csvDir, 'process.csv'), 'id,label,heuristicLabel,processType,stepCount,communities,entryPointId,terminalId');
228
+ const processWriter = new BufferedCSVWriter(path.join(csvDir, 'process.csv'), 'id,label,heuristicLabel,processType,processSubtype,runtimeChainConfidence,sourceReasons,sourceConfidences,stepCount,communities,entryPointId,terminalId');
229
229
  // Multi-language node types share the same CSV shape (no isExported column)
230
230
  const multiLangHeader = 'id,name,filePath,startLine,endLine,content,description';
231
231
  const MULTI_LANG_TYPES = ['Struct', 'Enum', 'Macro', 'Typedef', 'Union', 'Namespace', 'Trait', 'Impl',
@@ -282,11 +282,21 @@ export const streamAllCSVsToDisk = async (graph, repoPath, csvDir) => {
282
282
  case 'Process': {
283
283
  const communities = node.properties.communities || [];
284
284
  const communitiesStr = `[${communities.map((c) => `'${c.replace(/'/g, "''")}'`).join(',')}]`;
285
+ const sourceReasons = node.properties.sourceReasons || [];
286
+ const sourceReasonsStr = `[${sourceReasons.map((reason) => `'${String(reason).replace(/'/g, "''")}'`).join(',')}]`;
287
+ const sourceConfidences = node.properties.sourceConfidences || [];
288
+ const sourceConfidencesStr = `[${sourceConfidences
289
+ .map((confidence) => Number.isFinite(confidence) ? String(confidence) : '0')
290
+ .join(',')}]`;
285
291
  await processWriter.addRow([
286
292
  escapeCSVField(node.id),
287
293
  escapeCSVField(node.properties.name || ''),
288
294
  escapeCSVField(node.properties.heuristicLabel || ''),
289
295
  escapeCSVField(node.properties.processType || ''),
296
+ escapeCSVField(node.properties.processSubtype || ''),
297
+ escapeCSVField(node.properties.runtimeChainConfidence || ''),
298
+ escapeCSVField(sourceReasonsStr),
299
+ escapeCSVField(sourceConfidencesStr),
290
300
  escapeCSVNumber(node.properties.stepCount, 0),
291
301
  escapeCSVField(communitiesStr),
292
302
  escapeCSVField(node.properties.entryPointId || ''),