@veewo/gitnexus 1.3.10 → 1.3.11

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 (109) hide show
  1. package/README.md +3 -3
  2. package/dist/benchmark/analyze-memory-sampler.d.ts +10 -0
  3. package/dist/benchmark/analyze-memory-sampler.js +12 -0
  4. package/dist/benchmark/analyze-memory-sampler.test.d.ts +1 -0
  5. package/dist/benchmark/analyze-memory-sampler.test.js +12 -0
  6. package/dist/benchmark/io.test.js +48 -5
  7. package/dist/benchmark/u2-e2e/config.d.ts +1 -0
  8. package/dist/benchmark/u2-e2e/retrieval-runner.js +25 -3
  9. package/dist/benchmark/u2-e2e/retrieval-runner.test.js +44 -1
  10. package/dist/benchmark/unity-lazy-context-sampler.d.ts +58 -0
  11. package/dist/benchmark/unity-lazy-context-sampler.js +217 -0
  12. package/dist/benchmark/unity-lazy-context-sampler.test.d.ts +1 -0
  13. package/dist/benchmark/unity-lazy-context-sampler.test.js +32 -0
  14. package/dist/cli/analyze-close-policy.d.ts +5 -0
  15. package/dist/cli/analyze-close-policy.js +9 -0
  16. package/dist/cli/analyze-close-policy.test.d.ts +1 -0
  17. package/dist/cli/analyze-close-policy.test.js +12 -0
  18. package/dist/cli/analyze-runtime-summary.d.ts +2 -0
  19. package/dist/cli/analyze-runtime-summary.js +9 -0
  20. package/dist/cli/analyze-runtime-summary.test.d.ts +1 -0
  21. package/dist/cli/analyze-runtime-summary.test.js +14 -0
  22. package/dist/cli/analyze.js +42 -15
  23. package/dist/cli/eval-server.js +3 -0
  24. package/dist/cli/exit-code.d.ts +13 -0
  25. package/dist/cli/exit-code.js +25 -0
  26. package/dist/cli/exit-code.test.d.ts +1 -0
  27. package/dist/cli/exit-code.test.js +28 -0
  28. package/dist/cli/index.js +8 -2
  29. package/dist/cli/mcp.js +3 -0
  30. package/dist/cli/setup.js +3 -2
  31. package/dist/cli/setup.test.js +67 -0
  32. package/dist/cli/tool.d.ts +3 -1
  33. package/dist/cli/tool.js +2 -0
  34. package/dist/core/graph/types.d.ts +1 -1
  35. package/dist/core/ingestion/filesystem-walker.d.ts +6 -0
  36. package/dist/core/ingestion/filesystem-walker.js +17 -0
  37. package/dist/core/ingestion/filesystem-walker.test.d.ts +1 -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/tools.js +24 -2
  102. package/dist/types/pipeline.d.ts +7 -0
  103. package/package.json +4 -1
  104. package/skills/gitnexus-cli.md +18 -0
  105. package/skills/gitnexus-debugging.md +16 -2
  106. package/skills/gitnexus-exploring.md +15 -1
  107. package/skills/gitnexus-guide.md +15 -0
  108. package/skills/gitnexus-impact-analysis.md +2 -0
  109. package/skills/gitnexus-refactoring.md +5 -1
@@ -1,11 +1,13 @@
1
1
  import fs from 'node:fs/promises';
2
+ import { createReadStream } from 'node:fs';
2
3
  import path from 'node:path';
4
+ import { createInterface } from 'node:readline';
3
5
  import { glob } from 'glob';
4
6
  import { buildAssetMetaIndex, buildMetaIndex } from './meta-index.js';
5
- import { buildSerializableTypeIndexFromSources } from './serialized-type-index.js';
7
+ import { buildSerializableTypeIndexFromFiles } from './serialized-type-index.js';
6
8
  const DECLARATION_PATTERN = /\b(?:class|struct|interface)\s+([A-Za-z_][A-Za-z0-9_]*)\b/g;
7
- const GUID_IN_LINE_PATTERN = /\bguid:\s*([0-9a-f]{32})\b/gi;
8
- const RESOURCE_HIT_SCAN_CONCURRENCY = 16;
9
+ const SCRIPT_GUID_IN_LINE_PATTERN = /\bm_Script\s*:\s*\{[^}]*\bguid\s*:\s*([0-9a-f]{32})\b/gi;
10
+ const RESOURCE_HIT_SCAN_CONCURRENCY = 4;
9
11
  export async function buildUnityScanContext(input) {
10
12
  const scriptFiles = input.symbolDeclarations && input.symbolDeclarations.length > 0
11
13
  ? resolveScriptFilesFromSymbolDeclarations(input.repoRoot, input.symbolDeclarations, input.scopedPaths)
@@ -13,8 +15,13 @@ export async function buildUnityScanContext(input) {
13
15
  const symbolToScriptPaths = input.symbolDeclarations && input.symbolDeclarations.length > 0
14
16
  ? buildSymbolScriptPathIndexFromDeclarations(input.repoRoot, input.symbolDeclarations, input.scopedPaths)
15
17
  : await buildSymbolScriptPathIndex(input.repoRoot, scriptFiles);
16
- const scriptSources = await loadScriptSources(input.repoRoot, scriptFiles);
17
- const serializableTypeIndex = buildSerializableTypeIndexFromSources(scriptSources);
18
+ const serializableTypeIndex = await buildSerializableTypeIndexFromFiles(scriptFiles.map((scriptPath) => {
19
+ const normalizedPath = normalizeSlashes(scriptPath);
20
+ return {
21
+ filePath: normalizedPath,
22
+ read: async () => fs.readFile(path.join(input.repoRoot, normalizedPath), 'utf-8'),
23
+ };
24
+ }));
18
25
  const metaFiles = scriptFiles.map((scriptPath) => `${scriptPath}.meta`);
19
26
  const guidToScriptPath = await buildMetaIndex(input.repoRoot, { metaFiles });
20
27
  const scriptPathToGuid = new Map();
@@ -39,21 +46,91 @@ export async function buildUnityScanContext(input) {
39
46
  resourceDocCache: new Map(),
40
47
  };
41
48
  }
42
- async function loadScriptSources(repoRoot, scriptFiles) {
43
- const sources = [];
44
- for (const scriptPath of scriptFiles) {
49
+ export function buildUnityScanContextFromSeed(input) {
50
+ const seed = input.seed;
51
+ const requestedSymbols = new Set((input.symbolDeclarations || [])
52
+ .map((entry) => String(entry.symbol || '').trim())
53
+ .filter((value) => value.length > 0));
54
+ const requestedScripts = new Set((input.symbolDeclarations || [])
55
+ .map((entry) => normalizeSlashes(String(entry.scriptPath || '').trim()))
56
+ .filter((value) => value.length > 0));
57
+ const symbolToScriptPath = new Map();
58
+ for (const [symbol, scriptPath] of Object.entries(seed.symbolToScriptPath || {})) {
45
59
  const normalizedPath = normalizeSlashes(scriptPath);
46
- try {
47
- const content = await fs.readFile(path.join(repoRoot, normalizedPath), 'utf-8');
48
- sources.push({ filePath: normalizedPath, content });
60
+ if (!normalizedPath)
61
+ continue;
62
+ if (requestedSymbols.size > 0 && !requestedSymbols.has(symbol))
63
+ continue;
64
+ symbolToScriptPath.set(symbol, normalizedPath);
65
+ requestedScripts.add(normalizedPath);
66
+ }
67
+ if (symbolToScriptPath.size === 0 && requestedSymbols.size > 0) {
68
+ for (const declaration of input.symbolDeclarations || []) {
69
+ const symbol = String(declaration.symbol || '').trim();
70
+ const scriptPath = normalizeSlashes(String(declaration.scriptPath || '').trim());
71
+ if (!symbol || !scriptPath)
72
+ continue;
73
+ symbolToScriptPath.set(symbol, scriptPath);
74
+ requestedScripts.add(scriptPath);
49
75
  }
50
- catch (error) {
51
- if (error.code === 'ENOENT')
76
+ }
77
+ const symbolToCanonicalScriptPath = new Map(symbolToScriptPath);
78
+ const symbolToScriptPaths = new Map();
79
+ for (const [symbol, scriptPath] of symbolToScriptPath.entries()) {
80
+ symbolToScriptPaths.set(symbol, [scriptPath]);
81
+ }
82
+ const scriptPathToGuid = new Map();
83
+ const normalizedScriptPathToGuidEntries = Object.entries(seed.scriptPathToGuid || {})
84
+ .map(([scriptPath, guid]) => [normalizeSlashes(scriptPath), String(guid || '').trim()])
85
+ .filter(([scriptPath, guid]) => scriptPath.length > 0 && guid.length > 0);
86
+ for (const [scriptPath, guid] of normalizedScriptPathToGuidEntries) {
87
+ if (requestedScripts.size > 0 && !requestedScripts.has(scriptPath))
88
+ continue;
89
+ scriptPathToGuid.set(scriptPath, guid);
90
+ }
91
+ if (scriptPathToGuid.size === 0 && requestedScripts.size > 0) {
92
+ for (const [scriptPath, guid] of normalizedScriptPathToGuidEntries) {
93
+ if (!requestedScripts.has(scriptPath))
52
94
  continue;
53
- throw error;
95
+ scriptPathToGuid.set(scriptPath, guid);
96
+ }
97
+ }
98
+ const selectedGuids = new Set(scriptPathToGuid.values());
99
+ const guidToResourceHits = new Map();
100
+ for (const [guid, resourcePaths] of Object.entries(seed.guidToResourcePaths || {})) {
101
+ if (selectedGuids.size > 0 && !selectedGuids.has(guid))
102
+ continue;
103
+ const hits = (resourcePaths || [])
104
+ .map((resourcePathRaw) => normalizeSlashes(String(resourcePathRaw || '').trim()))
105
+ .filter((resourcePath) => resourcePath.length > 0)
106
+ .map((resourcePath) => ({
107
+ resourcePath,
108
+ resourceType: inferResourceType(resourcePath),
109
+ line: 0,
110
+ lineText: 'seed',
111
+ }));
112
+ if (hits.length > 0) {
113
+ guidToResourceHits.set(guid, hits);
54
114
  }
55
115
  }
56
- return sources;
116
+ const assetGuidToPath = new Map();
117
+ for (const [guid, assetPath] of Object.entries(seed.assetGuidToPath || {})) {
118
+ const normalizedPath = normalizeSlashes(String(assetPath || '').trim());
119
+ if (!guid || !normalizedPath)
120
+ continue;
121
+ assetGuidToPath.set(guid, normalizedPath);
122
+ }
123
+ return {
124
+ symbolToScriptPaths,
125
+ symbolToCanonicalScriptPath,
126
+ symbolToScriptPath,
127
+ scriptPathToGuid,
128
+ guidToResourceHits,
129
+ serializableSymbols: new Set(),
130
+ hostFieldTypeHints: new Map(),
131
+ assetGuidToPath,
132
+ resourceDocCache: new Map(),
133
+ };
57
134
  }
58
135
  async function buildSymbolScriptPathIndex(repoRoot, scriptFiles) {
59
136
  const candidates = new Map();
@@ -93,53 +170,65 @@ async function buildGuidHitIndex(repoRoot, scriptPathToGuid, resourceFiles) {
93
170
  }
94
171
  const perResourceHits = await mapWithConcurrency(resourceFiles, RESOURCE_HIT_SCAN_CONCURRENCY, async (resourcePathRaw) => {
95
172
  const resourcePath = normalizeSlashes(resourcePathRaw);
96
- const absolutePath = path.join(repoRoot, resourcePath);
97
- let content = '';
98
- try {
99
- content = await fs.readFile(absolutePath, 'utf-8');
100
- }
101
- catch (error) {
102
- const code = error.code;
103
- if (code === 'ENOENT' || code === 'EISDIR') {
104
- return new Map();
105
- }
106
- throw error;
107
- }
108
173
  const resourceType = inferResourceType(resourcePath);
109
- const lines = content.split(/\r?\n/);
110
174
  const hits = new Map();
111
- for (let index = 0; index < lines.length; index += 1) {
112
- const line = lines[index];
175
+ await scanGuidHitsInResourceFile(repoRoot, resourcePath, resourceType, guidLookup, hits);
176
+ return hits;
177
+ });
178
+ const guidToResourceHits = new Map();
179
+ for (const hitMap of perResourceHits) {
180
+ for (const [guid, hits] of hitMap.entries()) {
181
+ const existing = guidToResourceHits.get(guid) || [];
182
+ existing.push(...hits);
183
+ guidToResourceHits.set(guid, existing);
184
+ }
185
+ }
186
+ return guidToResourceHits;
187
+ }
188
+ async function scanGuidHitsInResourceFile(repoRoot, resourcePath, resourceType, guidLookup, hits) {
189
+ const absolutePath = path.join(repoRoot, resourcePath);
190
+ const stream = createReadStream(absolutePath, { encoding: 'utf-8' });
191
+ const reader = createInterface({
192
+ input: stream,
193
+ crlfDelay: Infinity,
194
+ });
195
+ const seenGuidInResource = new Set();
196
+ let lineNumber = 0;
197
+ try {
198
+ for await (const line of reader) {
199
+ lineNumber += 1;
113
200
  const seenCanonical = new Set();
114
- GUID_IN_LINE_PATTERN.lastIndex = 0;
115
- let match = GUID_IN_LINE_PATTERN.exec(line);
201
+ SCRIPT_GUID_IN_LINE_PATTERN.lastIndex = 0;
202
+ let match = SCRIPT_GUID_IN_LINE_PATTERN.exec(line);
116
203
  while (match) {
117
204
  const canonicalGuid = guidLookup.get(match[1].toLowerCase());
118
- if (canonicalGuid && !seenCanonical.has(canonicalGuid)) {
205
+ if (canonicalGuid && !seenCanonical.has(canonicalGuid) && !seenGuidInResource.has(canonicalGuid)) {
119
206
  seenCanonical.add(canonicalGuid);
207
+ seenGuidInResource.add(canonicalGuid);
120
208
  const existing = hits.get(canonicalGuid) || [];
121
209
  existing.push({
122
210
  resourcePath,
123
211
  resourceType,
124
- line: index + 1,
212
+ line: lineNumber,
125
213
  lineText: line,
126
214
  });
127
215
  hits.set(canonicalGuid, existing);
128
216
  }
129
- match = GUID_IN_LINE_PATTERN.exec(line);
217
+ match = SCRIPT_GUID_IN_LINE_PATTERN.exec(line);
130
218
  }
131
219
  }
132
- return hits;
133
- });
134
- const guidToResourceHits = new Map();
135
- for (const hitMap of perResourceHits) {
136
- for (const [guid, hits] of hitMap.entries()) {
137
- const existing = guidToResourceHits.get(guid) || [];
138
- existing.push(...hits);
139
- guidToResourceHits.set(guid, existing);
220
+ }
221
+ catch (error) {
222
+ const code = error.code;
223
+ if (code === 'ENOENT' || code === 'EISDIR') {
224
+ return;
140
225
  }
226
+ throw error;
227
+ }
228
+ finally {
229
+ reader.close();
230
+ stream.destroy();
141
231
  }
142
- return guidToResourceHits;
143
232
  }
144
233
  async function resolveScriptFiles(repoRoot, scopedPaths) {
145
234
  if (!scopedPaths || scopedPaths.length === 0) {
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import os from 'node:os';
5
5
  import fs from 'node:fs/promises';
6
6
  import { fileURLToPath } from 'node:url';
7
- import { buildUnityScanContext } from './scan-context.js';
7
+ import { buildUnityScanContext, buildUnityScanContextFromSeed } from './scan-context.js';
8
8
  import { resolveUnityBindings } from './resolver.js';
9
9
  const here = path.dirname(fileURLToPath(import.meta.url));
10
10
  const fixtureRoot = path.resolve(here, '../../../src/core/unity/__fixtures__/mini-unity');
@@ -74,7 +74,7 @@ test('buildUnityScanContext selects canonical script for duplicated symbol decla
74
74
  await fs.writeFile(path.join(scriptsDir, 'PlayerActor.Visual.cs'), 'public partial class PlayerActor {}', 'utf-8');
75
75
  await fs.writeFile(path.join(scriptsDir, 'PlayerActor.cs.meta'), 'guid: 11111111111111111111111111111111\n', 'utf-8');
76
76
  await fs.writeFile(path.join(scriptsDir, 'PlayerActor.Visual.cs.meta'), 'guid: 22222222222222222222222222222222\n', 'utf-8');
77
- await fs.writeFile(path.join(sceneDir, 'Test.unity'), '--- !u!1 &1\nguid: 11111111111111111111111111111111\n', 'utf-8');
77
+ await fs.writeFile(path.join(sceneDir, 'Test.unity'), '--- !u!114 &1\nMonoBehaviour:\n m_Script: {fileID: 11500000, guid: 11111111111111111111111111111111, type: 3}\n', 'utf-8');
78
78
  const context = await buildUnityScanContext({
79
79
  repoRoot: tempRoot,
80
80
  symbolDeclarations: [
@@ -116,3 +116,42 @@ test('buildUnityScanContext exposes serializable symbol index and host field typ
116
116
  await fs.rm(tempRoot, { recursive: true, force: true });
117
117
  }
118
118
  });
119
+ test('buildUnityScanContext builds serializable index from files without preloading source array', async () => {
120
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-serializable-streaming-'));
121
+ const scriptsDir = path.join(tempRoot, 'Assets/Scripts');
122
+ await fs.mkdir(scriptsDir, { recursive: true });
123
+ try {
124
+ await fs.writeFile(path.join(scriptsDir, 'AssetRef.cs'), '[Serializable] class AssetRef {}', 'utf-8');
125
+ await fs.writeFile(path.join(scriptsDir, 'Host.cs'), 'class Host { AssetRef icon; }', 'utf-8');
126
+ const context = await buildUnityScanContext({ repoRoot: tempRoot });
127
+ assert.equal(context.serializableSymbols.has('AssetRef'), true);
128
+ assert.equal(context.hostFieldTypeHints.get('Host')?.get('icon'), 'AssetRef');
129
+ }
130
+ finally {
131
+ await fs.rm(tempRoot, { recursive: true, force: true });
132
+ }
133
+ });
134
+ test('buildUnityScanContextFromSeed reconstructs lookup maps for resolver fast path', async () => {
135
+ const context = buildUnityScanContextFromSeed({
136
+ seed: {
137
+ version: 1,
138
+ symbolToScriptPath: {
139
+ MainUIManager: 'Assets/Scripts/MainUIManager.cs',
140
+ },
141
+ scriptPathToGuid: {
142
+ 'Assets/Scripts/MainUIManager.cs': '11111111111111111111111111111111',
143
+ },
144
+ guidToResourcePaths: {
145
+ '11111111111111111111111111111111': ['Assets/Scene/MainUIManager.unity'],
146
+ },
147
+ assetGuidToPath: {
148
+ '44444444444444444444444444444444': 'Assets/Config/MainUIDocument.asset',
149
+ },
150
+ },
151
+ symbolDeclarations: [{ symbol: 'MainUIManager', scriptPath: 'Assets/Scripts/MainUIManager.cs' }],
152
+ });
153
+ assert.equal(context.symbolToScriptPath.get('MainUIManager'), 'Assets/Scripts/MainUIManager.cs');
154
+ assert.equal(context.scriptPathToGuid.get('Assets/Scripts/MainUIManager.cs'), '11111111111111111111111111111111');
155
+ assert.equal(context.guidToResourceHits.get('11111111111111111111111111111111')?.length, 1);
156
+ assert.equal(context.assetGuidToPath?.get('44444444444444444444444444444444'), 'Assets/Config/MainUIDocument.asset');
157
+ });
@@ -6,5 +6,10 @@ interface SourceFile {
6
6
  filePath: string;
7
7
  content: string;
8
8
  }
9
+ interface SourceFileReader {
10
+ filePath: string;
11
+ read: () => Promise<string>;
12
+ }
9
13
  export declare function buildSerializableTypeIndexFromSources(sources: SourceFile[]): SerializableTypeIndex;
14
+ export declare function buildSerializableTypeIndexFromFiles(files: SourceFileReader[]): Promise<SerializableTypeIndex>;
10
15
  export {};
@@ -4,25 +4,56 @@ const FIELD_DECLARATION_PATTERN = /(?:\[[^\]]+\]\s*)*(?:(?:public|private|protec
4
4
  export function buildSerializableTypeIndexFromSources(sources) {
5
5
  const serializableSymbols = new Set();
6
6
  for (const source of sources) {
7
- SERIALIZABLE_DECLARATION_PATTERN.lastIndex = 0;
8
- let match = SERIALIZABLE_DECLARATION_PATTERN.exec(source.content);
9
- while (match) {
10
- serializableSymbols.add(match[1]);
11
- match = SERIALIZABLE_DECLARATION_PATTERN.exec(source.content);
12
- }
7
+ collectSerializableDeclarations(source.content, serializableSymbols);
13
8
  }
14
9
  const hostFieldTypeHints = new Map();
15
10
  for (const source of sources) {
16
- const classBodies = extractClassBodies(source.content);
17
- for (const classBody of classBodies) {
18
- const fieldHints = extractHostFieldHints(classBody.body, serializableSymbols);
19
- if (fieldHints.size > 0) {
20
- hostFieldTypeHints.set(classBody.name, fieldHints);
21
- }
22
- }
11
+ collectHostFieldTypeHints(source.content, serializableSymbols, hostFieldTypeHints);
12
+ }
13
+ return { serializableSymbols, hostFieldTypeHints };
14
+ }
15
+ export async function buildSerializableTypeIndexFromFiles(files) {
16
+ const serializableSymbols = new Set();
17
+ for (const file of files) {
18
+ const content = await safeReadFileContent(file);
19
+ collectSerializableDeclarations(content, serializableSymbols);
20
+ }
21
+ const hostFieldTypeHints = new Map();
22
+ for (const file of files) {
23
+ const content = await safeReadFileContent(file);
24
+ collectHostFieldTypeHints(content, serializableSymbols, hostFieldTypeHints);
23
25
  }
24
26
  return { serializableSymbols, hostFieldTypeHints };
25
27
  }
28
+ function collectSerializableDeclarations(content, serializableSymbols) {
29
+ SERIALIZABLE_DECLARATION_PATTERN.lastIndex = 0;
30
+ let match = SERIALIZABLE_DECLARATION_PATTERN.exec(content);
31
+ while (match) {
32
+ serializableSymbols.add(match[1]);
33
+ match = SERIALIZABLE_DECLARATION_PATTERN.exec(content);
34
+ }
35
+ }
36
+ function collectHostFieldTypeHints(content, serializableSymbols, hostFieldTypeHints) {
37
+ const classBodies = extractClassBodies(content);
38
+ for (const classBody of classBodies) {
39
+ const fieldHints = extractHostFieldHints(classBody.body, serializableSymbols);
40
+ if (fieldHints.size > 0) {
41
+ hostFieldTypeHints.set(classBody.name, fieldHints);
42
+ }
43
+ }
44
+ }
45
+ async function safeReadFileContent(file) {
46
+ try {
47
+ return await file.read();
48
+ }
49
+ catch (error) {
50
+ const code = error.code;
51
+ if (code === 'ENOENT' || code === 'EISDIR') {
52
+ return '';
53
+ }
54
+ throw error;
55
+ }
56
+ }
26
57
  function extractClassBodies(content) {
27
58
  const result = [];
28
59
  CLASS_DECLARATION_PATTERN.lastIndex = 0;
@@ -1,6 +1,6 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { buildSerializableTypeIndexFromSources } from './serialized-type-index.js';
3
+ import { buildSerializableTypeIndexFromFiles, buildSerializableTypeIndexFromSources } from './serialized-type-index.js';
4
4
  test('buildSerializableTypeIndex extracts serializable symbols and host field declared types', () => {
5
5
  const index = buildSerializableTypeIndexFromSources([
6
6
  {
@@ -32,3 +32,11 @@ test('buildSerializableTypeIndex extracts serializable symbols and host field de
32
32
  assert.equal(index.hostFieldTypeHints.get('InventoryConfig')?.get('iconVariants'), 'AssetRef');
33
33
  assert.equal(index.hostFieldTypeHints.get('InventoryConfig')?.has('ignored'), false);
34
34
  });
35
+ test('buildSerializableTypeIndexFromFiles does not require preloaded source array', async () => {
36
+ const out = await buildSerializableTypeIndexFromFiles([
37
+ { filePath: 'Assets/A.cs', read: async () => '[Serializable] class AssetRef {}' },
38
+ { filePath: 'Assets/B.cs', read: async () => 'class Host { AssetRef icon; }' },
39
+ ]);
40
+ assert.equal(out.serializableSymbols.has('AssetRef'), true);
41
+ assert.equal(out.hostFieldTypeHints.get('Host')?.get('icon'), 'AssetRef');
42
+ });
@@ -5,7 +5,14 @@
5
5
  * Supports multiple indexed repositories via a global registry.
6
6
  * KuzuDB connections are opened lazily per repo on first query.
7
7
  */
8
+ import { type ResolvedUnityBinding } from '../../core/unity/resolver.js';
9
+ import { type UnityContextPayload, type UnityHydrationMeta } from './unity-enrichment.js';
8
10
  import { type RegistryEntry } from '../../storage/repo-manager.js';
11
+ export declare function mergeUnityBindings(baseBindings: ResolvedUnityBinding[], resolvedByPath: Map<string, ResolvedUnityBinding[]>): ResolvedUnityBinding[];
12
+ export declare function mergeParityUnityBindings(baseNonLightweightBindings: ResolvedUnityBinding[], resolvedBindings: ResolvedUnityBinding[]): ResolvedUnityBinding[];
13
+ export declare function attachUnityHydrationMeta(payload: UnityContextPayload, input: Pick<UnityHydrationMeta, 'requestedMode' | 'effectiveMode' | 'elapsedMs' | 'fallbackToCompact'> & {
14
+ hasExpandableBindings: boolean;
15
+ }): UnityContextPayload;
9
16
  export interface CodebaseContext {
10
17
  projectName: string;
11
18
  stats: {
@@ -119,6 +126,15 @@ export declare class LocalBackend {
119
126
  * Routes cluster/process types to direct graph queries.
120
127
  */
121
128
  private explore;
129
+ private hydrateUnityContext;
130
+ private buildParityWarmupKey;
131
+ private scheduleParityWarmup;
132
+ private shouldEnableParityWarmup;
133
+ private getOrRunParityHydration;
134
+ private computeParityPayload;
135
+ private hydrateUnityContextParity;
136
+ private hydrateUnityContextCompact;
137
+ private toUnityContextPayload;
122
138
  /**
123
139
  * Detect changes — git-diff based impact analysis.
124
140
  * Maps changed lines to indexed symbols, then finds affected processes.