@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.
- package/README.md +3 -3
- package/dist/benchmark/analyze-memory-sampler.d.ts +10 -0
- package/dist/benchmark/analyze-memory-sampler.js +12 -0
- package/dist/benchmark/analyze-memory-sampler.test.d.ts +1 -0
- package/dist/benchmark/analyze-memory-sampler.test.js +12 -0
- package/dist/benchmark/io.test.js +48 -5
- package/dist/benchmark/u2-e2e/config.d.ts +1 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.js +25 -3
- package/dist/benchmark/u2-e2e/retrieval-runner.test.js +44 -1
- package/dist/benchmark/unity-lazy-context-sampler.d.ts +58 -0
- package/dist/benchmark/unity-lazy-context-sampler.js +217 -0
- package/dist/benchmark/unity-lazy-context-sampler.test.d.ts +1 -0
- package/dist/benchmark/unity-lazy-context-sampler.test.js +32 -0
- package/dist/cli/analyze-close-policy.d.ts +5 -0
- package/dist/cli/analyze-close-policy.js +9 -0
- package/dist/cli/analyze-close-policy.test.d.ts +1 -0
- package/dist/cli/analyze-close-policy.test.js +12 -0
- package/dist/cli/analyze-runtime-summary.d.ts +2 -0
- package/dist/cli/analyze-runtime-summary.js +9 -0
- package/dist/cli/analyze-runtime-summary.test.d.ts +1 -0
- package/dist/cli/analyze-runtime-summary.test.js +14 -0
- package/dist/cli/analyze.js +42 -15
- package/dist/cli/eval-server.js +3 -0
- package/dist/cli/exit-code.d.ts +13 -0
- package/dist/cli/exit-code.js +25 -0
- package/dist/cli/exit-code.test.d.ts +1 -0
- package/dist/cli/exit-code.test.js +28 -0
- package/dist/cli/index.js +8 -2
- package/dist/cli/mcp.js +3 -0
- package/dist/cli/setup.js +3 -2
- package/dist/cli/setup.test.js +67 -0
- package/dist/cli/tool.d.ts +3 -1
- package/dist/cli/tool.js +2 -0
- package/dist/core/graph/types.d.ts +1 -1
- package/dist/core/ingestion/filesystem-walker.d.ts +6 -0
- package/dist/core/ingestion/filesystem-walker.js +17 -0
- package/dist/core/ingestion/filesystem-walker.test.d.ts +1 -0
- package/dist/core/ingestion/filesystem-walker.test.js +51 -0
- package/dist/core/ingestion/pipeline.js +4 -3
- package/dist/core/ingestion/unity-parity-seed.d.ts +9 -0
- package/dist/core/ingestion/unity-parity-seed.js +69 -0
- package/dist/core/ingestion/unity-parity-seed.test.d.ts +1 -0
- package/dist/core/ingestion/unity-parity-seed.test.js +35 -0
- package/dist/core/ingestion/unity-resource-processor.d.ts +2 -0
- package/dist/core/ingestion/unity-resource-processor.js +87 -53
- package/dist/core/ingestion/unity-resource-processor.test.js +37 -39
- package/dist/core/kuzu/csv-generator.d.ts +20 -1
- package/dist/core/kuzu/csv-generator.js +92 -25
- package/dist/core/kuzu/csv-generator.test.d.ts +1 -0
- package/dist/core/kuzu/csv-generator.test.js +28 -0
- package/dist/core/kuzu/kuzu-adapter.js +35 -54
- package/dist/core/kuzu/relationship-pair-buckets.d.ts +17 -0
- package/dist/core/kuzu/relationship-pair-buckets.js +79 -0
- package/dist/core/kuzu/relationship-pair-buckets.test.d.ts +1 -0
- package/dist/core/kuzu/relationship-pair-buckets.test.js +10 -0
- package/dist/core/kuzu/schema.d.ts +1 -1
- package/dist/core/kuzu/schema.js +1 -0
- package/dist/core/unity/options.d.ts +2 -0
- package/dist/core/unity/options.js +9 -0
- package/dist/core/unity/options.test.js +8 -1
- package/dist/core/unity/resolver.d.ts +3 -0
- package/dist/core/unity/resolver.js +56 -2
- package/dist/core/unity/resolver.test.js +46 -0
- package/dist/core/unity/scan-context.d.ts +5 -0
- package/dist/core/unity/scan-context.js +133 -44
- package/dist/core/unity/scan-context.test.js +41 -2
- package/dist/core/unity/serialized-type-index.d.ts +5 -0
- package/dist/core/unity/serialized-type-index.js +44 -13
- package/dist/core/unity/serialized-type-index.test.js +9 -1
- package/dist/mcp/local/local-backend.d.ts +16 -0
- package/dist/mcp/local/local-backend.js +320 -4
- package/dist/mcp/local/local-backend.unity-merge.test.d.ts +1 -0
- package/dist/mcp/local/local-backend.unity-merge.test.js +261 -0
- package/dist/mcp/local/unity-enrichment.d.ts +15 -0
- package/dist/mcp/local/unity-enrichment.js +69 -5
- package/dist/mcp/local/unity-enrichment.test.js +69 -1
- package/dist/mcp/local/unity-lazy-config.d.ts +6 -0
- package/dist/mcp/local/unity-lazy-config.js +7 -0
- package/dist/mcp/local/unity-lazy-config.test.d.ts +1 -0
- package/dist/mcp/local/unity-lazy-config.test.js +9 -0
- package/dist/mcp/local/unity-lazy-hydrator.d.ts +15 -0
- package/dist/mcp/local/unity-lazy-hydrator.js +43 -0
- package/dist/mcp/local/unity-lazy-hydrator.test.d.ts +1 -0
- package/dist/mcp/local/unity-lazy-hydrator.test.js +66 -0
- package/dist/mcp/local/unity-lazy-overlay.d.ts +3 -0
- package/dist/mcp/local/unity-lazy-overlay.js +89 -0
- package/dist/mcp/local/unity-lazy-overlay.test.d.ts +1 -0
- package/dist/mcp/local/unity-lazy-overlay.test.js +83 -0
- package/dist/mcp/local/unity-parity-cache.d.ts +7 -0
- package/dist/mcp/local/unity-parity-cache.js +88 -0
- package/dist/mcp/local/unity-parity-cache.test.d.ts +1 -0
- package/dist/mcp/local/unity-parity-cache.test.js +143 -0
- package/dist/mcp/local/unity-parity-seed-loader.d.ts +2 -0
- package/dist/mcp/local/unity-parity-seed-loader.js +30 -0
- package/dist/mcp/local/unity-parity-seed-loader.test.d.ts +1 -0
- package/dist/mcp/local/unity-parity-seed-loader.test.js +25 -0
- package/dist/mcp/local/unity-parity-warmup-queue.d.ts +6 -0
- package/dist/mcp/local/unity-parity-warmup-queue.js +28 -0
- package/dist/mcp/local/unity-parity-warmup-queue.test.d.ts +1 -0
- package/dist/mcp/local/unity-parity-warmup-queue.test.js +15 -0
- package/dist/mcp/tools.js +24 -2
- package/dist/types/pipeline.d.ts +7 -0
- package/package.json +4 -1
- package/skills/gitnexus-cli.md +18 -0
- package/skills/gitnexus-debugging.md +16 -2
- package/skills/gitnexus-exploring.md +15 -1
- package/skills/gitnexus-guide.md +15 -0
- package/skills/gitnexus-impact-analysis.md +2 -0
- 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
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
196
|
-
for (const [pairKey,
|
|
167
|
+
const failedPairCsvPaths = [];
|
|
168
|
+
for (const [pairKey, bucket] of relsByPair) {
|
|
197
169
|
pairIdx++;
|
|
198
170
|
const [fromLabel, toLabel] = pairKey.split('|');
|
|
199
|
-
const
|
|
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 ||
|
|
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} (${
|
|
217
|
-
failedPairEdges +=
|
|
218
|
-
|
|
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(
|
|
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)";
|
package/dist/core/kuzu/schema.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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;
|