@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,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
+ }
@@ -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;