@veewo/gitnexus 1.3.9 → 1.3.11-rc.1

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 (134) hide show
  1. package/dist/benchmark/analyze-memory-sampler.d.ts +10 -0
  2. package/dist/benchmark/analyze-memory-sampler.js +12 -0
  3. package/dist/benchmark/analyze-memory-sampler.test.js +12 -0
  4. package/dist/benchmark/io.test.js +48 -5
  5. package/dist/benchmark/u2-e2e/config.d.ts +1 -0
  6. package/dist/benchmark/u2-e2e/retrieval-runner.js +25 -3
  7. package/dist/benchmark/u2-e2e/retrieval-runner.test.js +44 -1
  8. package/dist/benchmark/unity-lazy-context-sampler.d.ts +58 -0
  9. package/dist/benchmark/unity-lazy-context-sampler.js +217 -0
  10. package/dist/benchmark/unity-lazy-context-sampler.test.js +32 -0
  11. package/dist/cli/ai-context.js +1 -1
  12. package/dist/cli/analyze-close-policy.d.ts +5 -0
  13. package/dist/cli/analyze-close-policy.js +9 -0
  14. package/dist/cli/analyze-close-policy.test.js +12 -0
  15. package/dist/cli/analyze-multi-scope-regression.test.js +1 -1
  16. package/dist/cli/analyze-options.d.ts +19 -0
  17. package/dist/cli/analyze-options.js +35 -0
  18. package/dist/cli/analyze-options.test.js +42 -1
  19. package/dist/cli/analyze-runtime-summary.d.ts +2 -0
  20. package/dist/cli/analyze-runtime-summary.js +9 -0
  21. package/dist/cli/analyze-runtime-summary.test.js +14 -0
  22. package/dist/cli/analyze.d.ts +1 -0
  23. package/dist/cli/analyze.js +95 -41
  24. package/dist/cli/eval-server.js +3 -0
  25. package/dist/cli/exit-code.d.ts +13 -0
  26. package/dist/cli/exit-code.js +25 -0
  27. package/dist/cli/exit-code.test.js +28 -0
  28. package/dist/cli/index.js +9 -2
  29. package/dist/cli/mcp.js +3 -0
  30. package/dist/cli/repo-manager-alias.test.js +24 -1
  31. package/dist/cli/setup.js +3 -2
  32. package/dist/cli/setup.test.js +67 -0
  33. package/dist/cli/tool.d.ts +3 -1
  34. package/dist/cli/tool.js +2 -0
  35. package/dist/core/graph/types.d.ts +1 -1
  36. package/dist/core/ingestion/filesystem-walker.d.ts +6 -0
  37. package/dist/core/ingestion/filesystem-walker.js +17 -0
  38. package/dist/core/ingestion/filesystem-walker.test.js +51 -0
  39. package/dist/core/ingestion/pipeline.js +4 -3
  40. package/dist/core/ingestion/unity-parity-seed.d.ts +9 -0
  41. package/dist/core/ingestion/unity-parity-seed.js +69 -0
  42. package/dist/core/ingestion/unity-parity-seed.test.d.ts +1 -0
  43. package/dist/core/ingestion/unity-parity-seed.test.js +35 -0
  44. package/dist/core/ingestion/unity-resource-processor.d.ts +2 -0
  45. package/dist/core/ingestion/unity-resource-processor.js +87 -53
  46. package/dist/core/ingestion/unity-resource-processor.test.js +37 -39
  47. package/dist/core/kuzu/csv-generator.d.ts +20 -1
  48. package/dist/core/kuzu/csv-generator.js +92 -25
  49. package/dist/core/kuzu/csv-generator.test.d.ts +1 -0
  50. package/dist/core/kuzu/csv-generator.test.js +28 -0
  51. package/dist/core/kuzu/kuzu-adapter.js +35 -54
  52. package/dist/core/kuzu/relationship-pair-buckets.d.ts +17 -0
  53. package/dist/core/kuzu/relationship-pair-buckets.js +79 -0
  54. package/dist/core/kuzu/relationship-pair-buckets.test.d.ts +1 -0
  55. package/dist/core/kuzu/relationship-pair-buckets.test.js +10 -0
  56. package/dist/core/kuzu/schema.d.ts +1 -1
  57. package/dist/core/kuzu/schema.js +1 -0
  58. package/dist/core/unity/options.d.ts +2 -0
  59. package/dist/core/unity/options.js +9 -0
  60. package/dist/core/unity/options.test.js +8 -1
  61. package/dist/core/unity/resolver.d.ts +3 -0
  62. package/dist/core/unity/resolver.js +56 -2
  63. package/dist/core/unity/resolver.test.js +46 -0
  64. package/dist/core/unity/scan-context.d.ts +5 -0
  65. package/dist/core/unity/scan-context.js +133 -44
  66. package/dist/core/unity/scan-context.test.js +41 -2
  67. package/dist/core/unity/serialized-type-index.d.ts +5 -0
  68. package/dist/core/unity/serialized-type-index.js +44 -13
  69. package/dist/core/unity/serialized-type-index.test.js +9 -1
  70. package/dist/mcp/local/local-backend.d.ts +16 -0
  71. package/dist/mcp/local/local-backend.js +320 -4
  72. package/dist/mcp/local/local-backend.unity-merge.test.d.ts +1 -0
  73. package/dist/mcp/local/local-backend.unity-merge.test.js +261 -0
  74. package/dist/mcp/local/unity-enrichment.d.ts +15 -0
  75. package/dist/mcp/local/unity-enrichment.js +69 -5
  76. package/dist/mcp/local/unity-enrichment.test.js +69 -1
  77. package/dist/mcp/local/unity-lazy-config.d.ts +6 -0
  78. package/dist/mcp/local/unity-lazy-config.js +7 -0
  79. package/dist/mcp/local/unity-lazy-config.test.d.ts +1 -0
  80. package/dist/mcp/local/unity-lazy-config.test.js +9 -0
  81. package/dist/mcp/local/unity-lazy-hydrator.d.ts +15 -0
  82. package/dist/mcp/local/unity-lazy-hydrator.js +43 -0
  83. package/dist/mcp/local/unity-lazy-hydrator.test.d.ts +1 -0
  84. package/dist/mcp/local/unity-lazy-hydrator.test.js +66 -0
  85. package/dist/mcp/local/unity-lazy-overlay.d.ts +3 -0
  86. package/dist/mcp/local/unity-lazy-overlay.js +89 -0
  87. package/dist/mcp/local/unity-lazy-overlay.test.d.ts +1 -0
  88. package/dist/mcp/local/unity-lazy-overlay.test.js +83 -0
  89. package/dist/mcp/local/unity-parity-cache.d.ts +7 -0
  90. package/dist/mcp/local/unity-parity-cache.js +88 -0
  91. package/dist/mcp/local/unity-parity-cache.test.d.ts +1 -0
  92. package/dist/mcp/local/unity-parity-cache.test.js +143 -0
  93. package/dist/mcp/local/unity-parity-seed-loader.d.ts +2 -0
  94. package/dist/mcp/local/unity-parity-seed-loader.js +30 -0
  95. package/dist/mcp/local/unity-parity-seed-loader.test.d.ts +1 -0
  96. package/dist/mcp/local/unity-parity-seed-loader.test.js +25 -0
  97. package/dist/mcp/local/unity-parity-warmup-queue.d.ts +6 -0
  98. package/dist/mcp/local/unity-parity-warmup-queue.js +28 -0
  99. package/dist/mcp/local/unity-parity-warmup-queue.test.d.ts +1 -0
  100. package/dist/mcp/local/unity-parity-warmup-queue.test.js +15 -0
  101. package/dist/mcp/resources.js +1 -1
  102. package/dist/mcp/staleness.js +1 -1
  103. package/dist/mcp/tools.js +24 -2
  104. package/dist/storage/repo-manager.d.ts +6 -0
  105. package/dist/types/pipeline.d.ts +7 -0
  106. package/package.json +6 -3
  107. package/skills/gitnexus-cli.md +18 -0
  108. package/skills/gitnexus-debugging.md +16 -2
  109. package/skills/gitnexus-exploring.md +15 -1
  110. package/skills/gitnexus-guide.md +15 -0
  111. package/skills/gitnexus-impact-analysis.md +2 -0
  112. package/skills/gitnexus-refactoring.md +5 -1
  113. package/dist/cli/analyze-custom-modules-regression.test.js +0 -75
  114. package/dist/cli/analyze-modules-diagnostics.test.js +0 -36
  115. package/dist/core/ingestion/modules/assignment-engine.d.ts +0 -33
  116. package/dist/core/ingestion/modules/assignment-engine.js +0 -179
  117. package/dist/core/ingestion/modules/assignment-engine.test.js +0 -111
  118. package/dist/core/ingestion/modules/config-loader.d.ts +0 -2
  119. package/dist/core/ingestion/modules/config-loader.js +0 -186
  120. package/dist/core/ingestion/modules/config-loader.test.js +0 -57
  121. package/dist/core/ingestion/modules/rule-matcher.d.ts +0 -12
  122. package/dist/core/ingestion/modules/rule-matcher.js +0 -63
  123. package/dist/core/ingestion/modules/rule-matcher.test.js +0 -58
  124. package/dist/core/ingestion/modules/types.d.ts +0 -44
  125. package/dist/core/ingestion/modules/types.js +0 -2
  126. package/dist/mcp/local/cluster-aggregation.d.ts +0 -20
  127. package/dist/mcp/local/cluster-aggregation.js +0 -48
  128. package/dist/mcp/local/cluster-aggregation.test.js +0 -22
  129. /package/dist/{cli/analyze-custom-modules-regression.test.d.ts → benchmark/analyze-memory-sampler.test.d.ts} +0 -0
  130. /package/dist/{cli/analyze-modules-diagnostics.test.d.ts → benchmark/unity-lazy-context-sampler.test.d.ts} +0 -0
  131. /package/dist/{core/ingestion/modules/assignment-engine.test.d.ts → cli/analyze-close-policy.test.d.ts} +0 -0
  132. /package/dist/{core/ingestion/modules/config-loader.test.d.ts → cli/analyze-runtime-summary.test.d.ts} +0 -0
  133. /package/dist/{core/ingestion/modules/rule-matcher.test.d.ts → cli/exit-code.test.d.ts} +0 -0
  134. /package/dist/{mcp/local/cluster-aggregation.test.d.ts → core/ingestion/filesystem-walker.test.d.ts} +0 -0
@@ -8,7 +8,7 @@ import { processUnityResources } from './unity-resource-processor.js';
8
8
  const here = path.dirname(fileURLToPath(import.meta.url));
9
9
  const fixtureRoot = path.resolve(here, '../../../src/core/unity/__fixtures__/mini-unity');
10
10
  const symbols = ['Global', 'BattleMode', 'PlayerActor', 'MainUIManager'];
11
- test('processUnityResources adds Unity resource relationships and component payload nodes', async () => {
11
+ test('processUnityResources does not emit UNITY_COMPONENT_IN or synthetic resource File nodes', async () => {
12
12
  const graph = createKnowledgeGraph();
13
13
  for (const symbol of symbols) {
14
14
  const filePath = `Assets/Scripts/${symbol}.cs`;
@@ -41,13 +41,17 @@ test('processUnityResources adds Unity resource relationships and component payl
41
41
  }
42
42
  const result = await processUnityResources(graph, { repoPath: fixtureRoot });
43
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')));
44
+ const unitySummaryRelations = [...graph.iterRelationships()].filter((rel) => rel.type === 'UNITY_RESOURCE_SUMMARY');
45
+ const syntheticResourceFiles = [...graph.iterNodes()].filter((node) => node.label === 'File' && /\.(prefab|unity|asset)$/.test(String(node.properties.filePath)));
46
+ const componentNodes = [...graph.iterNodes()].filter((node) => node.label === 'CodeElement');
47
+ assert.equal(result.bindingCount > 0, true);
48
+ assert.equal(unityFileRelations.length, 0);
49
+ assert.equal(syntheticResourceFiles.length, 0);
50
+ assert.ok(unitySummaryRelations.length > 0);
51
+ assert.equal(componentNodes.length, 0);
50
52
  assert.ok(result.bindingCount >= symbols.length);
53
+ assert.equal(result.paritySeed?.version, 1);
54
+ assert.ok((result.paritySeed?.scriptPathToGuid || {})['Assets/Scripts/MainUIManager.cs']);
51
55
  assert.ok(result.timingsMs.scanContext >= 0);
52
56
  assert.ok(result.timingsMs.resolve >= 0);
53
57
  assert.ok(result.timingsMs.graphWrite > 0);
@@ -248,7 +252,7 @@ test('processUnityResources memoizes resolve results by symbol within one run',
248
252
  assert.equal(result.bindingCount, 1);
249
253
  assert.ok(result.diagnostics.some((line) => line.includes('skip-non-canonical=1')));
250
254
  });
251
- test('processUnityResources writes UNITY_COMPONENT_INSTANCE only for canonical class node', async () => {
255
+ test('processUnityResources writes UNITY_RESOURCE_SUMMARY only for canonical class node', async () => {
252
256
  const graph = createKnowledgeGraph();
253
257
  const canonicalPath = 'Assets/Scripts/PlayerActor.cs';
254
258
  const partialPath = 'Assets/Scripts/PlayerActor.Visual.cs';
@@ -294,15 +298,15 @@ test('processUnityResources writes UNITY_COMPONENT_INSTANCE only for canonical c
294
298
  unityDiagnostics: [],
295
299
  }),
296
300
  });
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);
301
+ const summaryRelations = [...graph.iterRelationships()].filter((rel) => rel.type === 'UNITY_RESOURCE_SUMMARY');
302
+ assert.equal(summaryRelations.length, 1);
303
+ assert.equal(summaryRelations[0]?.sourceId, canonicalClassId);
300
304
  assert.equal(result.processedSymbols, 1);
301
305
  assert.equal(result.bindingCount, 1);
302
306
  assert.ok(result.diagnostics.some((line) => line.includes('selected=1')));
303
307
  assert.ok(result.diagnostics.some((line) => line.includes('skip-non-canonical=1')));
304
308
  });
305
- test('processUnityResources writes UNITY_SERIALIZED_TYPE_IN for serializable class field matches', async () => {
309
+ test('processUnityResources writes UNITY_RESOURCE_SUMMARY for serializable class field matches', async () => {
306
310
  const graph = createKnowledgeGraph();
307
311
  const hostPath = 'Assets/Scripts/HostClass.cs';
308
312
  const serializablePath = 'Assets/Scripts/AssetRef.cs';
@@ -364,11 +368,11 @@ test('processUnityResources writes UNITY_SERIALIZED_TYPE_IN for serializable cla
364
368
  unityDiagnostics: [],
365
369
  }),
366
370
  });
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);
371
+ const summaryRelations = [...graph.iterRelationships()].filter((rel) => rel.type === 'UNITY_RESOURCE_SUMMARY');
372
+ const serializableSummary = summaryRelations.filter((rel) => rel.sourceId === serializableClassId);
373
+ assert.equal(serializableSummary.length, 1);
370
374
  });
371
- test('processUnityResources writes compact unity payload by default', async () => {
375
+ test('processUnityResources writes compact UNITY_RESOURCE_SUMMARY reason by default', async () => {
372
376
  const graph = createKnowledgeGraph();
373
377
  const classId = generateId('Class', 'Assets/Scripts/Compact.cs:CompactSymbol');
374
378
  graph.addNode({
@@ -407,17 +411,14 @@ test('processUnityResources writes compact unity payload by default', async () =
407
411
  unityDiagnostics: [],
408
412
  }),
409
413
  });
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);
414
+ const summary = [...graph.iterRelationships()].find((rel) => rel.type === 'UNITY_RESOURCE_SUMMARY');
415
+ assert.ok(summary);
416
+ const reason = JSON.parse(String(summary.reason || '{}'));
417
+ assert.deepEqual(reason.bindingKinds, ['scene-override']);
418
+ assert.equal(reason.lightweight, true);
419
+ assert.equal(reason.resourceType, 'scene');
419
420
  });
420
- test('processUnityResources includes structured assetRefPaths in component payload', async () => {
421
+ test('processUnityResources keeps UNITY_RESOURCE_SUMMARY reason compact when bindings include assetRefPaths', async () => {
421
422
  const graph = createKnowledgeGraph();
422
423
  const classId = generateId('Class', 'Assets/Scripts/CharacterList.cs:CharacterList');
423
424
  graph.addNode({
@@ -464,14 +465,11 @@ test('processUnityResources includes structured assetRefPaths in component paylo
464
465
  unityDiagnostics: [],
465
466
  }),
466
467
  });
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);
468
+ const summary = [...graph.iterRelationships()].find((rel) => rel.type === 'UNITY_RESOURCE_SUMMARY');
469
+ assert.ok(summary);
470
+ assert.equal(String(summary.reason).includes('assetRefPaths'), false);
473
471
  });
474
- test('processUnityResources writes full unity payload when payloadMode=full', async () => {
472
+ test('processUnityResources summary-only persistence ignores full payload mode and keeps summary reason', async () => {
475
473
  const graph = createKnowledgeGraph();
476
474
  const classId = generateId('Class', 'Assets/Scripts/Full.cs:FullSymbol');
477
475
  graph.addNode({
@@ -507,12 +505,12 @@ test('processUnityResources writes full unity payload when payloadMode=full', as
507
505
  unityDiagnostics: [],
508
506
  }),
509
507
  });
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);
508
+ const summary = [...graph.iterRelationships()].find((rel) => rel.type === 'UNITY_RESOURCE_SUMMARY');
509
+ assert.ok(summary);
510
+ const reason = JSON.parse(String(summary.reason || '{}'));
511
+ assert.equal(reason.resourceType, 'scene');
512
+ assert.deepEqual(reason.bindingKinds, ['scene-override']);
513
+ assert.equal(String(summary.reason).includes('evidence'), false);
516
514
  });
517
515
  test('processUnityResources aggregates repetitive diagnostics with capped samples', async () => {
518
516
  const graph = createKnowledgeGraph();
@@ -11,8 +11,27 @@
11
11
  * - Double quotes within fields are escaped by doubling them ("")
12
12
  * - All fields are consistently quoted for safety with code content
13
13
  */
14
- import { KnowledgeGraph } from '../graph/types.js';
14
+ import { KnowledgeGraph, GraphNode } from '../graph/types.js';
15
15
  import { NodeTableName } from './schema.js';
16
+ /**
17
+ * LRU content cache — avoids re-reading the same source file for every
18
+ * symbol defined in it. Sized generously so most files stay cached during
19
+ * the single-pass node iteration.
20
+ */
21
+ export declare class FileContentCache {
22
+ private repoPath;
23
+ private maxBytes;
24
+ private cache;
25
+ private currentBytes;
26
+ constructor(repoPath: string, maxBytes?: number);
27
+ get(relativePath: string): Promise<string>;
28
+ setForTest(key: string, value: string): void;
29
+ hasForTest(key: string): boolean;
30
+ private touch;
31
+ private set;
32
+ private evictIfNeeded;
33
+ }
34
+ export declare function toCodeElementCsvRow(node: GraphNode, contentCache?: FileContentCache): Promise<string>;
16
35
  export interface StreamedCSVResult {
17
36
  nodeFiles: Map<NodeTableName, {
18
37
  csvPath: string;
@@ -39,6 +39,7 @@ const escapeCSVNumber = (value, defaultValue = -1) => {
39
39
  return String(defaultValue);
40
40
  return String(value);
41
41
  };
42
+ const UNITY_RESOURCE_PATH_PATTERN = /\.(prefab|unity|asset)$/i;
42
43
  // ============================================================================
43
44
  // CONTENT EXTRACTION (lazy — reads from disk on demand)
44
45
  // ============================================================================
@@ -59,21 +60,23 @@ const isBinaryContent = (content) => {
59
60
  * symbol defined in it. Sized generously so most files stay cached during
60
61
  * the single-pass node iteration.
61
62
  */
62
- class FileContentCache {
63
- cache = new Map();
64
- accessOrder = [];
65
- maxSize;
63
+ export class FileContentCache {
66
64
  repoPath;
67
- constructor(repoPath, maxSize = 3000) {
65
+ maxBytes;
66
+ cache = new Map();
67
+ currentBytes = 0;
68
+ constructor(repoPath, maxBytes = 128 * 1024 * 1024) {
68
69
  this.repoPath = repoPath;
69
- this.maxSize = maxSize;
70
+ this.maxBytes = maxBytes;
70
71
  }
71
72
  async get(relativePath) {
72
73
  if (!relativePath)
73
74
  return '';
74
75
  const cached = this.cache.get(relativePath);
75
- if (cached !== undefined)
76
- return cached;
76
+ if (cached !== undefined) {
77
+ this.touch(relativePath, cached);
78
+ return cached.content;
79
+ }
77
80
  try {
78
81
  const fullPath = path.join(this.repoPath, relativePath);
79
82
  const content = await fs.readFile(fullPath, 'utf-8');
@@ -85,14 +88,38 @@ class FileContentCache {
85
88
  return '';
86
89
  }
87
90
  }
91
+ setForTest(key, value) {
92
+ this.set(key, value);
93
+ }
94
+ hasForTest(key) {
95
+ return this.cache.has(key);
96
+ }
97
+ touch(key, value) {
98
+ this.cache.delete(key);
99
+ this.cache.set(key, value);
100
+ }
88
101
  set(key, value) {
89
- if (this.cache.size >= this.maxSize) {
90
- const oldest = this.accessOrder.shift();
91
- if (oldest)
92
- this.cache.delete(oldest);
102
+ const prev = this.cache.get(key);
103
+ if (prev) {
104
+ this.currentBytes -= prev.sizeBytes;
105
+ this.cache.delete(key);
106
+ }
107
+ const sizeBytes = Buffer.byteLength(value, 'utf-8');
108
+ this.cache.set(key, { content: value, sizeBytes });
109
+ this.currentBytes += sizeBytes;
110
+ this.evictIfNeeded();
111
+ }
112
+ evictIfNeeded() {
113
+ while (this.currentBytes > this.maxBytes && this.cache.size > 0) {
114
+ const oldestKey = this.cache.keys().next().value;
115
+ if (!oldestKey)
116
+ break;
117
+ const oldest = this.cache.get(oldestKey);
118
+ this.cache.delete(oldestKey);
119
+ if (oldest) {
120
+ this.currentBytes -= oldest.sizeBytes;
121
+ }
93
122
  }
94
- this.cache.set(key, value);
95
- this.accessOrder.push(key);
96
123
  }
97
124
  }
98
125
  const extractContent = async (node, contentCache) => {
@@ -123,6 +150,56 @@ const extractContent = async (node, contentCache) => {
123
150
  ? snippet.slice(0, MAX_SNIPPET) + '\n... [truncated]'
124
151
  : snippet;
125
152
  };
153
+ function compactUnityComponentDescription(description) {
154
+ const raw = typeof description === 'string' ? description : '';
155
+ if (!raw)
156
+ return '';
157
+ try {
158
+ const parsed = JSON.parse(raw);
159
+ const compact = {};
160
+ if (parsed.bindingKind)
161
+ compact.bindingKind = parsed.bindingKind;
162
+ if (parsed.componentObjectId)
163
+ compact.componentObjectId = parsed.componentObjectId;
164
+ if (parsed.lightweight)
165
+ compact.lightweight = true;
166
+ const scalarFields = parsed.serializedFields?.scalarFields || [];
167
+ const referenceFields = parsed.serializedFields?.referenceFields || [];
168
+ if (scalarFields.length > 0 || referenceFields.length > 0) {
169
+ compact.serializedFields = { scalarFields, referenceFields };
170
+ }
171
+ if (Array.isArray(parsed.resolvedReferences) && parsed.resolvedReferences.length > 0) {
172
+ compact.resolvedReferences = parsed.resolvedReferences;
173
+ }
174
+ if (Array.isArray(parsed.assetRefPaths) && parsed.assetRefPaths.length > 0) {
175
+ compact.assetRefPaths = parsed.assetRefPaths;
176
+ }
177
+ return JSON.stringify(Object.keys(compact).length > 0 ? compact : parsed);
178
+ }
179
+ catch {
180
+ return raw;
181
+ }
182
+ }
183
+ export async function toCodeElementCsvRow(node, contentCache) {
184
+ const filePath = String(node.properties.filePath || '');
185
+ const isUnityComponentRow = node.label === 'CodeElement' && UNITY_RESOURCE_PATH_PATTERN.test(filePath);
186
+ const content = isUnityComponentRow
187
+ ? ''
188
+ : (contentCache ? await extractContent(node, contentCache) : '');
189
+ const description = isUnityComponentRow
190
+ ? compactUnityComponentDescription(node.properties.description || '')
191
+ : String(node.properties.description || '');
192
+ return [
193
+ escapeCSVField(node.id),
194
+ escapeCSVField(node.properties.name || ''),
195
+ escapeCSVField(filePath),
196
+ escapeCSVNumber(node.properties.startLine, -1),
197
+ escapeCSVNumber(node.properties.endLine, -1),
198
+ node.properties.isExported ? 'true' : 'false',
199
+ escapeCSVField(content),
200
+ escapeCSVField(description),
201
+ ].join(',');
202
+ }
126
203
  // ============================================================================
127
204
  // BUFFERED CSV WRITER
128
205
  // ============================================================================
@@ -266,17 +343,7 @@ export const streamAllCSVsToDisk = async (graph, repoPath, csvDir) => {
266
343
  // Code element nodes (Function, Class, Interface, Method, CodeElement)
267
344
  const writer = codeWriterMap[node.label];
268
345
  if (writer) {
269
- const content = await extractContent(node, contentCache);
270
- await writer.addRow([
271
- escapeCSVField(node.id),
272
- escapeCSVField(node.properties.name || ''),
273
- escapeCSVField(node.properties.filePath || ''),
274
- escapeCSVNumber(node.properties.startLine, -1),
275
- escapeCSVNumber(node.properties.endLine, -1),
276
- node.properties.isExported ? 'true' : 'false',
277
- escapeCSVField(content),
278
- escapeCSVField(node.properties.description || ''),
279
- ].join(','));
346
+ await writer.addRow(await toCodeElementCsvRow(node, contentCache));
280
347
  }
281
348
  else {
282
349
  // Multi-language node types (Struct, Impl, Trait, Macro, etc.)
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { FileContentCache, toCodeElementCsvRow } from './csv-generator.js';
4
+ test('FileContentCache evicts oldest entries when byte budget is exceeded', async () => {
5
+ const cache = new FileContentCache('/tmp/repo', 10);
6
+ cache.setForTest('a.cs', '123456');
7
+ cache.setForTest('b.cs', '123456');
8
+ assert.equal(cache.hasForTest('a.cs'), false);
9
+ assert.equal(cache.hasForTest('b.cs'), true);
10
+ });
11
+ test('Unity component CodeElement rows store compact description and empty content', async () => {
12
+ const row = await toCodeElementCsvRow({
13
+ id: 'CodeElement:Assets/A.prefab:114',
14
+ label: 'CodeElement',
15
+ properties: {
16
+ name: 'DoorObj@114',
17
+ filePath: 'Assets/A.prefab',
18
+ startLine: 12,
19
+ endLine: 12,
20
+ description: JSON.stringify({
21
+ bindingKind: 'direct',
22
+ componentObjectId: '114',
23
+ serializedFields: { scalarFields: [], referenceFields: [] },
24
+ }),
25
+ },
26
+ });
27
+ assert.match(row, /,\"\"\,\"\{/);
28
+ });
@@ -1,10 +1,9 @@
1
1
  import fs from 'fs/promises';
2
- import { createReadStream } from 'fs';
3
- import { createInterface } from 'readline';
4
2
  import path from 'path';
5
3
  import kuzu from 'kuzu';
6
4
  import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, } from './schema.js';
7
5
  import { streamAllCSVsToDisk } from './csv-generator.js';
6
+ import { streamRelationshipPairBucketsFromCsv } from './relationship-pair-buckets.js';
8
7
  let db = null;
9
8
  let conn = null;
10
9
  let currentDbPath = null;
@@ -146,45 +145,18 @@ export const loadGraphToKuzu = async (graph, repoPath, storagePath, onProgress)
146
145
  }
147
146
  }
148
147
  // Bulk COPY relationships — split by FROM→TO label pair (KuzuDB requires it)
149
- // Stream-read the relation CSV line by line to avoid exceeding V8 max string length
150
- let relHeader = '';
151
- const relsByPair = new Map();
152
- let skippedRels = 0;
153
- let totalValidRels = 0;
154
- await new Promise((resolve, reject) => {
155
- const rl = createInterface({ input: createReadStream(csvResult.relCsvPath, 'utf-8'), crlfDelay: Infinity });
156
- let isFirst = true;
157
- rl.on('line', (line) => {
158
- if (isFirst) {
159
- relHeader = line;
160
- isFirst = false;
161
- return;
162
- }
163
- if (!line.trim())
164
- return;
165
- const match = line.match(/"([^"]*)","([^"]*)"/);
166
- if (!match) {
167
- skippedRels++;
168
- return;
169
- }
170
- const fromLabel = getNodeLabel(match[1]);
171
- const toLabel = getNodeLabel(match[2]);
172
- if (!validTables.has(fromLabel) || !validTables.has(toLabel)) {
173
- skippedRels++;
174
- return;
175
- }
176
- const pairKey = `${fromLabel}|${toLabel}`;
177
- let list = relsByPair.get(pairKey);
178
- if (!list) {
179
- list = [];
180
- relsByPair.set(pairKey, list);
181
- }
182
- list.push(line);
183
- totalValidRels++;
184
- });
185
- rl.on('close', resolve);
186
- rl.on('error', reject);
148
+ // Stream relation CSV into per-pair temporary CSV files to avoid retaining
149
+ // all relationship lines in memory at once.
150
+ const pairBucketResult = await streamRelationshipPairBucketsFromCsv({
151
+ relCsvPath: csvResult.relCsvPath,
152
+ csvDir,
153
+ validTables,
154
+ getNodeLabel,
187
155
  });
156
+ const relHeader = pairBucketResult.relHeader;
157
+ const relsByPair = pairBucketResult.buckets;
158
+ const skippedRels = pairBucketResult.skippedRels;
159
+ const totalValidRels = pairBucketResult.totalValidRels;
188
160
  const insertedRels = totalValidRels;
189
161
  const warnings = [];
190
162
  let fallbackStats = { attempted: 0, succeeded: 0, failed: 0 };
@@ -192,15 +164,13 @@ export const loadGraphToKuzu = async (graph, repoPath, storagePath, onProgress)
192
164
  log(`Loading edges: ${insertedRels.toLocaleString()} across ${relsByPair.size} types`);
193
165
  let pairIdx = 0;
194
166
  let failedPairEdges = 0;
195
- const failedPairLines = [];
196
- for (const [pairKey, lines] of relsByPair) {
167
+ const failedPairCsvPaths = [];
168
+ for (const [pairKey, bucket] of relsByPair) {
197
169
  pairIdx++;
198
170
  const [fromLabel, toLabel] = pairKey.split('|');
199
- const pairCsvPath = path.join(csvDir, `rel_${fromLabel}_${toLabel}.csv`);
200
- await fs.writeFile(pairCsvPath, relHeader + '\n' + lines.join('\n'), 'utf-8');
201
- const normalizedPath = normalizeCopyPath(pairCsvPath);
171
+ const normalizedPath = normalizeCopyPath(bucket.csvPath);
202
172
  const copyQuery = `COPY ${REL_TABLE_NAME} FROM "${normalizedPath}" (from="${fromLabel}", to="${toLabel}", HEADER=true, ESCAPE='"', DELIM=',', QUOTE='"', PARALLEL=false, auto_detect=false)`;
203
- if (pairIdx % 5 === 0 || lines.length > 1000) {
173
+ if (pairIdx % 5 === 0 || bucket.rowCount > 1000) {
204
174
  log(`Loading edges: ${pairIdx}/${relsByPair.size} types (${fromLabel} -> ${toLabel})`);
205
175
  }
206
176
  try {
@@ -213,20 +183,31 @@ export const loadGraphToKuzu = async (graph, repoPath, storagePath, onProgress)
213
183
  }
214
184
  catch (retryErr) {
215
185
  const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
216
- warnings.push(`${fromLabel}->${toLabel} (${lines.length} edges): ${retryMsg.slice(0, 80)}`);
217
- failedPairEdges += lines.length;
218
- failedPairLines.push(...lines);
186
+ warnings.push(`${fromLabel}->${toLabel} (${bucket.rowCount} edges): ${retryMsg.slice(0, 80)}`);
187
+ failedPairEdges += bucket.rowCount;
188
+ failedPairCsvPaths.push(bucket.csvPath);
219
189
  }
220
190
  }
191
+ }
192
+ if (failedPairCsvPaths.length > 0) {
193
+ const failedPairLines = [relHeader];
194
+ for (const failedPairPath of failedPairCsvPaths) {
195
+ const raw = await fs.readFile(failedPairPath, 'utf-8');
196
+ const lines = raw
197
+ .split('\n')
198
+ .slice(1)
199
+ .filter(line => line.trim().length > 0);
200
+ failedPairLines.push(...lines);
201
+ }
202
+ log(`Inserting ${failedPairEdges} edges individually (missing schema pairs)`);
203
+ fallbackStats = await fallbackRelationshipInserts(failedPairLines, validTables, getNodeLabel);
204
+ }
205
+ for (const [, bucket] of relsByPair) {
221
206
  try {
222
- await fs.unlink(pairCsvPath);
207
+ await fs.unlink(bucket.csvPath);
223
208
  }
224
209
  catch { }
225
210
  }
226
- if (failedPairLines.length > 0) {
227
- log(`Inserting ${failedPairEdges} edges individually (missing schema pairs)`);
228
- fallbackStats = await fallbackRelationshipInserts([relHeader, ...failedPairLines], validTables, getNodeLabel);
229
- }
230
211
  }
231
212
  // Cleanup all CSVs
232
213
  try {
@@ -0,0 +1,17 @@
1
+ export interface RelationshipPairBucket {
2
+ csvPath: string;
3
+ rowCount: number;
4
+ }
5
+ export interface RelationshipPairBucketResult {
6
+ relHeader: string;
7
+ buckets: Map<string, RelationshipPairBucket>;
8
+ skippedRels: number;
9
+ totalValidRels: number;
10
+ }
11
+ export declare function bucketRelationshipLines(lines: string[], getNodeLabel: (id: string) => string): Promise<Map<string, string[]>>;
12
+ export declare function streamRelationshipPairBucketsFromCsv(params: {
13
+ relCsvPath: string;
14
+ csvDir: string;
15
+ validTables: Set<string>;
16
+ getNodeLabel: (nodeId: string) => string;
17
+ }): Promise<RelationshipPairBucketResult>;
@@ -0,0 +1,79 @@
1
+ import fs from 'fs/promises';
2
+ import { createReadStream } from 'fs';
3
+ import { createInterface } from 'readline';
4
+ import path from 'path';
5
+ const REL_ENDPOINTS_PATTERN = /"([^"]*)","([^"]*)"/;
6
+ const parseRelationshipEndpoints = (line) => {
7
+ const match = line.match(REL_ENDPOINTS_PATTERN);
8
+ if (!match)
9
+ return null;
10
+ return { fromId: match[1], toId: match[2] };
11
+ };
12
+ export async function bucketRelationshipLines(lines, getNodeLabel) {
13
+ const buckets = new Map();
14
+ for (const line of lines) {
15
+ const endpoints = parseRelationshipEndpoints(line);
16
+ if (!endpoints)
17
+ continue;
18
+ const key = `${getNodeLabel(endpoints.fromId)}|${getNodeLabel(endpoints.toId)}`;
19
+ const rows = buckets.get(key) || [];
20
+ rows.push(line);
21
+ buckets.set(key, rows);
22
+ }
23
+ return buckets;
24
+ }
25
+ export async function streamRelationshipPairBucketsFromCsv(params) {
26
+ const { relCsvPath, csvDir, validTables, getNodeLabel } = params;
27
+ let relHeader = '';
28
+ const buckets = new Map();
29
+ let skippedRels = 0;
30
+ let totalValidRels = 0;
31
+ let isFirst = true;
32
+ let queue = Promise.resolve();
33
+ await new Promise((resolve, reject) => {
34
+ const rl = createInterface({ input: createReadStream(relCsvPath, 'utf-8'), crlfDelay: Infinity });
35
+ rl.on('line', (line) => {
36
+ queue = queue.then(async () => {
37
+ if (isFirst) {
38
+ relHeader = line;
39
+ isFirst = false;
40
+ return;
41
+ }
42
+ if (!line.trim())
43
+ return;
44
+ const endpoints = parseRelationshipEndpoints(line);
45
+ if (!endpoints) {
46
+ skippedRels++;
47
+ return;
48
+ }
49
+ const fromLabel = getNodeLabel(endpoints.fromId);
50
+ const toLabel = getNodeLabel(endpoints.toId);
51
+ if (!validTables.has(fromLabel) || !validTables.has(toLabel)) {
52
+ skippedRels++;
53
+ return;
54
+ }
55
+ const pairKey = `${fromLabel}|${toLabel}`;
56
+ let bucket = buckets.get(pairKey);
57
+ if (!bucket) {
58
+ bucket = {
59
+ csvPath: path.join(csvDir, `rel_${fromLabel}_${toLabel}.csv`),
60
+ rowCount: 0,
61
+ };
62
+ buckets.set(pairKey, bucket);
63
+ await fs.writeFile(bucket.csvPath, `${relHeader}\n`, 'utf-8');
64
+ }
65
+ await fs.appendFile(bucket.csvPath, `${line}\n`, 'utf-8');
66
+ bucket.rowCount++;
67
+ totalValidRels++;
68
+ }).catch((error) => {
69
+ rl.close();
70
+ reject(error);
71
+ });
72
+ });
73
+ rl.on('close', () => {
74
+ queue.then(() => resolve()).catch(reject);
75
+ });
76
+ rl.on('error', reject);
77
+ });
78
+ return { relHeader, buckets, skippedRels, totalValidRels };
79
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { bucketRelationshipLines } from './relationship-pair-buckets.js';
4
+ test('bucketRelationshipLines groups CSV lines by from/to pair without retaining all lines in one array', async () => {
5
+ const out = await bucketRelationshipLines([
6
+ '"Class:a","File:x","UNITY_RESOURCE_SUMMARY",1,"",0',
7
+ '"Class:a","CodeElement:b","UNITY_COMPONENT_INSTANCE",1,"",0',
8
+ ], (nodeId) => nodeId.split(':')[0]);
9
+ assert.deepEqual([...out.keys()].sort(), ['Class|CodeElement', 'Class|File']);
10
+ });
@@ -11,7 +11,7 @@
11
11
  export declare const NODE_TABLES: readonly ["File", "Folder", "Function", "Class", "Interface", "Method", "CodeElement", "Community", "Process", "Struct", "Enum", "Macro", "Typedef", "Union", "Namespace", "Trait", "Impl", "TypeAlias", "Const", "Static", "Property", "Record", "Delegate", "Annotation", "Constructor", "Template", "Module"];
12
12
  export type NodeTableName = typeof NODE_TABLES[number];
13
13
  export declare const REL_TABLE_NAME = "CodeRelation";
14
- export declare const REL_TYPES: readonly ["CONTAINS", "DEFINES", "IMPORTS", "CALLS", "EXTENDS", "IMPLEMENTS", "MEMBER_OF", "STEP_IN_PROCESS", "UNITY_COMPONENT_IN", "UNITY_COMPONENT_INSTANCE", "UNITY_SERIALIZED_TYPE_IN"];
14
+ export declare const REL_TYPES: readonly ["CONTAINS", "DEFINES", "IMPORTS", "CALLS", "EXTENDS", "IMPLEMENTS", "MEMBER_OF", "STEP_IN_PROCESS", "UNITY_COMPONENT_IN", "UNITY_COMPONENT_INSTANCE", "UNITY_RESOURCE_SUMMARY", "UNITY_SERIALIZED_TYPE_IN"];
15
15
  export type RelType = typeof REL_TYPES[number];
16
16
  export declare const EMBEDDING_TABLE_NAME = "CodeEmbedding";
17
17
  export declare const FILE_SCHEMA = "\nCREATE NODE TABLE File (\n id STRING,\n name STRING,\n filePath STRING,\n content STRING,\n PRIMARY KEY (id)\n)";
@@ -33,6 +33,7 @@ export const REL_TYPES = [
33
33
  'STEP_IN_PROCESS',
34
34
  'UNITY_COMPONENT_IN',
35
35
  'UNITY_COMPONENT_INSTANCE',
36
+ 'UNITY_RESOURCE_SUMMARY',
36
37
  'UNITY_SERIALIZED_TYPE_IN',
37
38
  ];
38
39
  // ============================================================================
@@ -1,2 +1,4 @@
1
1
  export type UnityResourcesMode = 'off' | 'on' | 'auto';
2
+ export type UnityHydrationMode = 'parity' | 'compact';
2
3
  export declare function parseUnityResourcesMode(raw?: string): UnityResourcesMode;
4
+ export declare function parseUnityHydrationMode(raw?: string): UnityHydrationMode;
@@ -7,3 +7,12 @@ export function parseUnityResourcesMode(raw) {
7
7
  }
8
8
  throw new Error('Invalid unity resources mode. Use off|on|auto.');
9
9
  }
10
+ export function parseUnityHydrationMode(raw) {
11
+ if (!raw)
12
+ return 'compact';
13
+ const normalized = raw.trim().toLowerCase();
14
+ if (normalized === 'parity' || normalized === 'compact') {
15
+ return normalized;
16
+ }
17
+ throw new Error('Invalid unity hydration mode. Use parity|compact.');
18
+ }