@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
@@ -1,6 +1,6 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { parseUnityResourcesMode } from './options.js';
3
+ import { parseUnityHydrationMode, parseUnityResourcesMode } from './options.js';
4
4
  test('parseUnityResourcesMode defaults to off', () => {
5
5
  assert.equal(parseUnityResourcesMode(undefined), 'off');
6
6
  });
@@ -8,3 +8,10 @@ test('parseUnityResourcesMode validates mode', () => {
8
8
  assert.equal(parseUnityResourcesMode('on'), 'on');
9
9
  assert.throws(() => parseUnityResourcesMode('bad'), /unity resources mode/i);
10
10
  });
11
+ test('parseUnityHydrationMode defaults to compact', () => {
12
+ assert.equal(parseUnityHydrationMode(undefined), 'compact');
13
+ });
14
+ test('parseUnityHydrationMode validates mode', () => {
15
+ assert.equal(parseUnityHydrationMode('compact'), 'compact');
16
+ assert.throws(() => parseUnityHydrationMode('bad'), /unity hydration mode/i);
17
+ });
@@ -5,6 +5,8 @@ export interface ResolveInput {
5
5
  repoRoot: string;
6
6
  symbol: string;
7
7
  scanContext?: UnityScanContext;
8
+ resourcePathAllowlist?: string[];
9
+ deepParseLargeResources?: boolean;
8
10
  }
9
11
  export interface UnityScalarField {
10
12
  name: string;
@@ -58,6 +60,7 @@ export interface ResolvedUnityBinding {
58
60
  resourceType: 'prefab' | 'scene' | 'asset';
59
61
  bindingKind: UnityBindingKind;
60
62
  componentObjectId: string;
63
+ lightweight?: boolean;
61
64
  evidence: UnityBindingEvidence;
62
65
  serializedFields: UnitySerializedFields;
63
66
  resolvedReferences: UnityResolvedReference[];
@@ -5,15 +5,24 @@ import { buildMetaIndex } from './meta-index.js';
5
5
  import { mergeOverrideChain } from './override-merger.js';
6
6
  import { findGuidHits } from './resource-hit-scanner.js';
7
7
  import { parseUnityYamlObjects } from './yaml-object-graph.js';
8
+ const MAX_CACHED_RESOURCE_BYTES = 512 * 1024;
8
9
  export async function resolveUnityBindings(input) {
9
10
  const scriptPath = await resolveSymbolScriptPath(input.repoRoot, input.symbol, input.scanContext);
10
11
  const scriptGuid = await resolveScriptGuid(input.repoRoot, scriptPath, input.scanContext);
11
- const hits = input.scanContext
12
+ const rawHits = input.scanContext
12
13
  ? (input.scanContext.guidToResourceHits.get(scriptGuid) ?? [])
13
14
  : await findGuidHits(input.repoRoot, scriptGuid);
15
+ const hits = applyResourceAllowlist(rawHits, input.resourcePathAllowlist);
14
16
  const resourceBindings = [];
15
17
  const unityDiagnostics = [];
18
+ const resourceSizeCache = new Map();
16
19
  for (const hit of hits) {
20
+ const shouldUseLightweightBinding = !input.deepParseLargeResources
21
+ && await isLargeResourceForDeepParse(input.repoRoot, hit.resourcePath, resourceSizeCache);
22
+ if (shouldUseLightweightBinding) {
23
+ resourceBindings.push(createLightweightBinding(hit));
24
+ continue;
25
+ }
17
26
  const blocks = await getResourceBlocks(input.repoRoot, hit.resourcePath, input.scanContext);
18
27
  const matchedComponents = blocks.filter((block) => block.objectType === 'MonoBehaviour' && block.fields.m_Script?.includes(scriptGuid));
19
28
  if (matchedComponents.length === 0) {
@@ -46,6 +55,44 @@ export async function resolveUnityBindings(input) {
46
55
  unityDiagnostics,
47
56
  };
48
57
  }
58
+ function applyResourceAllowlist(hits, allowlist) {
59
+ if (!allowlist || allowlist.length === 0) {
60
+ return hits;
61
+ }
62
+ const normalizedAllowlist = new Set(allowlist.map((value) => normalizePath(value)));
63
+ return hits.filter((hit) => normalizedAllowlist.has(normalizePath(hit.resourcePath)));
64
+ }
65
+ function createLightweightBinding(hit) {
66
+ return {
67
+ resourcePath: hit.resourcePath,
68
+ resourceType: hit.resourceType,
69
+ bindingKind: hit.resourceType === 'scene' ? 'scene-override' : 'direct',
70
+ componentObjectId: `line-${hit.line}`,
71
+ lightweight: true,
72
+ evidence: {
73
+ line: hit.line,
74
+ lineText: hit.lineText,
75
+ },
76
+ serializedFields: {
77
+ scalarFields: [],
78
+ referenceFields: [],
79
+ },
80
+ resolvedReferences: [],
81
+ assetRefPaths: [],
82
+ };
83
+ }
84
+ async function isLargeResourceForDeepParse(repoRoot, resourcePath, cache) {
85
+ const normalizedPath = normalizePath(resourcePath);
86
+ const cached = cache.get(normalizedPath);
87
+ if (cached !== undefined) {
88
+ return cached;
89
+ }
90
+ const absolutePath = path.join(repoRoot, normalizedPath);
91
+ const stat = await fs.stat(absolutePath);
92
+ const isLarge = stat.size > MAX_CACHED_RESOURCE_BYTES;
93
+ cache.set(normalizedPath, isLarge);
94
+ return isLarge;
95
+ }
49
96
  export function hasCoverage(resultSet) {
50
97
  return {
51
98
  hasScalar: resultSet.some((result) => result.serializedFields.scalarFields.length > 0),
@@ -102,9 +149,16 @@ async function getResourceBlocks(repoRoot, resourcePath, scanContext) {
102
149
  return cached;
103
150
  }
104
151
  const absoluteResourcePath = path.join(repoRoot, normalizedResourcePath);
152
+ let allowCache = Boolean(scanContext);
153
+ if (allowCache) {
154
+ const stat = await fs.stat(absoluteResourcePath);
155
+ allowCache = stat.size <= MAX_CACHED_RESOURCE_BYTES;
156
+ }
105
157
  const raw = await fs.readFile(absoluteResourcePath, 'utf-8');
106
158
  const blocks = parseUnityYamlObjects(raw);
107
- scanContext?.resourceDocCache.set(normalizedResourcePath, blocks);
159
+ if (allowCache) {
160
+ scanContext?.resourceDocCache.set(normalizedResourcePath, blocks);
161
+ }
108
162
  return blocks;
109
163
  }
110
164
  function resolveBindingForComponent(componentBlock, blocks, hit, scanContext) {
@@ -188,6 +188,52 @@ test('resolveUnityBindings keeps existing scene serializedFields stable when .as
188
188
  assert.equal(mainUIDocument?.sourceLayer, 'scene');
189
189
  assert.equal(mainUIDocument?.guid, '44444444444444444444444444444444');
190
190
  });
191
+ test('resolveUnityBindings supports resourcePathAllowlist filtering', async () => {
192
+ const result = await resolveUnityBindings({
193
+ repoRoot: fixtureRoot,
194
+ symbol: 'MainUIManager',
195
+ resourcePathAllowlist: ['Assets/Scene/NonExisting.unity'],
196
+ });
197
+ assert.equal(result.resourceBindings.length, 0);
198
+ });
199
+ test('resolveUnityBindings deepParseLargeResources can override lightweight fallback', async () => {
200
+ const tempRoot = await fs.mkdtemp(path.join(path.dirname(fixtureRoot), 'tmp-large-unity-'));
201
+ const scriptsDir = path.join(tempRoot, 'Assets/Scripts');
202
+ const sceneDir = path.join(tempRoot, 'Assets/Scene');
203
+ await fs.mkdir(scriptsDir, { recursive: true });
204
+ await fs.mkdir(sceneDir, { recursive: true });
205
+ try {
206
+ const scriptPath = 'Assets/Scripts/LargeSymbol.cs';
207
+ const scenePath = 'Assets/Scene/LargeScene.unity';
208
+ const scriptGuid = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
209
+ const padding = '#'.repeat(600 * 1024);
210
+ await fs.writeFile(path.join(tempRoot, scriptPath), 'public class LargeSymbol {}', 'utf-8');
211
+ await fs.writeFile(path.join(tempRoot, `${scriptPath}.meta`), `guid: ${scriptGuid}\n`, 'utf-8');
212
+ await fs.writeFile(path.join(tempRoot, scenePath), `--- !u!114 &11400000\nMonoBehaviour:\n m_Script: {fileID: 11500000, guid: ${scriptGuid}, type: 3}\n needPause: 1\n${padding}\n`, 'utf-8');
213
+ const scanContext = await buildUnityScanContext({
214
+ repoRoot: tempRoot,
215
+ scopedPaths: [scriptPath, `${scriptPath}.meta`, scenePath],
216
+ symbolDeclarations: [{ symbol: 'LargeSymbol', scriptPath }],
217
+ });
218
+ const lightweight = await resolveUnityBindings({
219
+ repoRoot: tempRoot,
220
+ symbol: 'LargeSymbol',
221
+ scanContext,
222
+ });
223
+ assert.equal(lightweight.resourceBindings[0]?.lightweight, true);
224
+ const expanded = await resolveUnityBindings({
225
+ repoRoot: tempRoot,
226
+ symbol: 'LargeSymbol',
227
+ scanContext,
228
+ deepParseLargeResources: true,
229
+ });
230
+ assert.equal(expanded.resourceBindings[0]?.lightweight, undefined);
231
+ assert.equal(expanded.resourceBindings[0]?.componentObjectId, '11400000');
232
+ }
233
+ finally {
234
+ await fs.rm(tempRoot, { recursive: true, force: true });
235
+ }
236
+ });
191
237
  test('extractAssetRefPathReferences parses nested _relativePath rows and marks sprite assets', () => {
192
238
  const refs = extractAssetRefPathReferences({
193
239
  scalarFields: [
@@ -1,5 +1,6 @@
1
1
  import type { UnityResourceGuidHit } from './resource-hit-scanner.js';
2
2
  import type { UnityObjectBlock } from './yaml-object-graph.js';
3
+ import type { UnityParitySeed } from '../ingestion/unity-parity-seed.js';
3
4
  export interface BuildScanContextInput {
4
5
  repoRoot: string;
5
6
  scopedPaths?: string[];
@@ -21,3 +22,7 @@ export interface UnityScanContext {
21
22
  resourceDocCache: Map<string, UnityObjectBlock[]>;
22
23
  }
23
24
  export declare function buildUnityScanContext(input: BuildScanContextInput): Promise<UnityScanContext>;
25
+ export declare function buildUnityScanContextFromSeed(input: {
26
+ seed: UnityParitySeed;
27
+ symbolDeclarations?: UnitySymbolDeclaration[];
28
+ }): UnityScanContext;
@@ -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.