@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.
- 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.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.js +32 -0
- package/dist/cli/ai-context.js +1 -1
- 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.js +12 -0
- package/dist/cli/analyze-multi-scope-regression.test.js +1 -1
- package/dist/cli/analyze-options.d.ts +19 -0
- package/dist/cli/analyze-options.js +35 -0
- package/dist/cli/analyze-options.test.js +42 -1
- 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.js +14 -0
- package/dist/cli/analyze.d.ts +1 -0
- package/dist/cli/analyze.js +95 -41
- 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.js +28 -0
- package/dist/cli/index.js +9 -2
- package/dist/cli/mcp.js +3 -0
- package/dist/cli/repo-manager-alias.test.js +24 -1
- 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.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/resources.js +1 -1
- package/dist/mcp/staleness.js +1 -1
- package/dist/mcp/tools.js +24 -2
- package/dist/storage/repo-manager.d.ts +6 -0
- package/dist/types/pipeline.d.ts +7 -0
- package/package.json +6 -3
- 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
- package/dist/cli/analyze-custom-modules-regression.test.js +0 -75
- package/dist/cli/analyze-modules-diagnostics.test.js +0 -36
- package/dist/core/ingestion/modules/assignment-engine.d.ts +0 -33
- package/dist/core/ingestion/modules/assignment-engine.js +0 -179
- package/dist/core/ingestion/modules/assignment-engine.test.js +0 -111
- package/dist/core/ingestion/modules/config-loader.d.ts +0 -2
- package/dist/core/ingestion/modules/config-loader.js +0 -186
- package/dist/core/ingestion/modules/config-loader.test.js +0 -57
- package/dist/core/ingestion/modules/rule-matcher.d.ts +0 -12
- package/dist/core/ingestion/modules/rule-matcher.js +0 -63
- package/dist/core/ingestion/modules/rule-matcher.test.js +0 -58
- package/dist/core/ingestion/modules/types.d.ts +0 -44
- package/dist/core/ingestion/modules/types.js +0 -2
- package/dist/mcp/local/cluster-aggregation.d.ts +0 -20
- package/dist/mcp/local/cluster-aggregation.js +0 -48
- package/dist/mcp/local/cluster-aggregation.test.js +0 -22
- /package/dist/{cli/analyze-custom-modules-regression.test.d.ts → benchmark/analyze-memory-sampler.test.d.ts} +0 -0
- /package/dist/{cli/analyze-modules-diagnostics.test.d.ts → benchmark/unity-lazy-context-sampler.test.d.ts} +0 -0
- /package/dist/{core/ingestion/modules/assignment-engine.test.d.ts → cli/analyze-close-policy.test.d.ts} +0 -0
- /package/dist/{core/ingestion/modules/config-loader.test.d.ts → cli/analyze-runtime-summary.test.d.ts} +0 -0
- /package/dist/{core/ingestion/modules/rule-matcher.test.d.ts → cli/exit-code.test.d.ts} +0 -0
- /package/dist/{mcp/local/cluster-aggregation.test.d.ts → core/ingestion/filesystem-walker.test.d.ts} +0 -0
|
@@ -8,7 +8,7 @@ import { processUnityResources } from './unity-resource-processor.js';
|
|
|
8
8
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const fixtureRoot = path.resolve(here, '../../../src/core/unity/__fixtures__/mini-unity');
|
|
10
10
|
const symbols = ['Global', 'BattleMode', 'PlayerActor', 'MainUIManager'];
|
|
11
|
-
test('processUnityResources
|
|
11
|
+
test('processUnityResources does not emit UNITY_COMPONENT_IN or synthetic resource File nodes', async () => {
|
|
12
12
|
const graph = createKnowledgeGraph();
|
|
13
13
|
for (const symbol of symbols) {
|
|
14
14
|
const filePath = `Assets/Scripts/${symbol}.cs`;
|
|
@@ -41,13 +41,17 @@ test('processUnityResources adds Unity resource relationships and component payl
|
|
|
41
41
|
}
|
|
42
42
|
const result = await processUnityResources(graph, { repoPath: fixtureRoot });
|
|
43
43
|
const unityFileRelations = [...graph.iterRelationships()].filter((rel) => rel.type === 'UNITY_COMPONENT_IN');
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
assert.
|
|
48
|
-
assert.
|
|
49
|
-
assert.
|
|
44
|
+
const unitySummaryRelations = [...graph.iterRelationships()].filter((rel) => rel.type === 'UNITY_RESOURCE_SUMMARY');
|
|
45
|
+
const syntheticResourceFiles = [...graph.iterNodes()].filter((node) => node.label === 'File' && /\.(prefab|unity|asset)$/.test(String(node.properties.filePath)));
|
|
46
|
+
const componentNodes = [...graph.iterNodes()].filter((node) => node.label === 'CodeElement');
|
|
47
|
+
assert.equal(result.bindingCount > 0, true);
|
|
48
|
+
assert.equal(unityFileRelations.length, 0);
|
|
49
|
+
assert.equal(syntheticResourceFiles.length, 0);
|
|
50
|
+
assert.ok(unitySummaryRelations.length > 0);
|
|
51
|
+
assert.equal(componentNodes.length, 0);
|
|
50
52
|
assert.ok(result.bindingCount >= symbols.length);
|
|
53
|
+
assert.equal(result.paritySeed?.version, 1);
|
|
54
|
+
assert.ok((result.paritySeed?.scriptPathToGuid || {})['Assets/Scripts/MainUIManager.cs']);
|
|
51
55
|
assert.ok(result.timingsMs.scanContext >= 0);
|
|
52
56
|
assert.ok(result.timingsMs.resolve >= 0);
|
|
53
57
|
assert.ok(result.timingsMs.graphWrite > 0);
|
|
@@ -248,7 +252,7 @@ test('processUnityResources memoizes resolve results by symbol within one run',
|
|
|
248
252
|
assert.equal(result.bindingCount, 1);
|
|
249
253
|
assert.ok(result.diagnostics.some((line) => line.includes('skip-non-canonical=1')));
|
|
250
254
|
});
|
|
251
|
-
test('processUnityResources writes
|
|
255
|
+
test('processUnityResources writes UNITY_RESOURCE_SUMMARY only for canonical class node', async () => {
|
|
252
256
|
const graph = createKnowledgeGraph();
|
|
253
257
|
const canonicalPath = 'Assets/Scripts/PlayerActor.cs';
|
|
254
258
|
const partialPath = 'Assets/Scripts/PlayerActor.Visual.cs';
|
|
@@ -294,15 +298,15 @@ test('processUnityResources writes UNITY_COMPONENT_INSTANCE only for canonical c
|
|
|
294
298
|
unityDiagnostics: [],
|
|
295
299
|
}),
|
|
296
300
|
});
|
|
297
|
-
const
|
|
298
|
-
assert.equal(
|
|
299
|
-
assert.equal(
|
|
301
|
+
const summaryRelations = [...graph.iterRelationships()].filter((rel) => rel.type === 'UNITY_RESOURCE_SUMMARY');
|
|
302
|
+
assert.equal(summaryRelations.length, 1);
|
|
303
|
+
assert.equal(summaryRelations[0]?.sourceId, canonicalClassId);
|
|
300
304
|
assert.equal(result.processedSymbols, 1);
|
|
301
305
|
assert.equal(result.bindingCount, 1);
|
|
302
306
|
assert.ok(result.diagnostics.some((line) => line.includes('selected=1')));
|
|
303
307
|
assert.ok(result.diagnostics.some((line) => line.includes('skip-non-canonical=1')));
|
|
304
308
|
});
|
|
305
|
-
test('processUnityResources writes
|
|
309
|
+
test('processUnityResources writes UNITY_RESOURCE_SUMMARY for serializable class field matches', async () => {
|
|
306
310
|
const graph = createKnowledgeGraph();
|
|
307
311
|
const hostPath = 'Assets/Scripts/HostClass.cs';
|
|
308
312
|
const serializablePath = 'Assets/Scripts/AssetRef.cs';
|
|
@@ -364,11 +368,11 @@ test('processUnityResources writes UNITY_SERIALIZED_TYPE_IN for serializable cla
|
|
|
364
368
|
unityDiagnostics: [],
|
|
365
369
|
}),
|
|
366
370
|
});
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
assert.equal(
|
|
371
|
+
const summaryRelations = [...graph.iterRelationships()].filter((rel) => rel.type === 'UNITY_RESOURCE_SUMMARY');
|
|
372
|
+
const serializableSummary = summaryRelations.filter((rel) => rel.sourceId === serializableClassId);
|
|
373
|
+
assert.equal(serializableSummary.length, 1);
|
|
370
374
|
});
|
|
371
|
-
test('processUnityResources writes compact
|
|
375
|
+
test('processUnityResources writes compact UNITY_RESOURCE_SUMMARY reason by default', async () => {
|
|
372
376
|
const graph = createKnowledgeGraph();
|
|
373
377
|
const classId = generateId('Class', 'Assets/Scripts/Compact.cs:CompactSymbol');
|
|
374
378
|
graph.addNode({
|
|
@@ -407,17 +411,14 @@ test('processUnityResources writes compact unity payload by default', async () =
|
|
|
407
411
|
unityDiagnostics: [],
|
|
408
412
|
}),
|
|
409
413
|
});
|
|
410
|
-
const
|
|
411
|
-
assert.ok(
|
|
412
|
-
const
|
|
413
|
-
assert.
|
|
414
|
-
assert.equal(
|
|
415
|
-
assert.
|
|
416
|
-
assert.equal(payload.resourcePath, undefined);
|
|
417
|
-
assert.equal(payload.resourceType, undefined);
|
|
418
|
-
assert.equal(payload.evidence, undefined);
|
|
414
|
+
const summary = [...graph.iterRelationships()].find((rel) => rel.type === 'UNITY_RESOURCE_SUMMARY');
|
|
415
|
+
assert.ok(summary);
|
|
416
|
+
const reason = JSON.parse(String(summary.reason || '{}'));
|
|
417
|
+
assert.deepEqual(reason.bindingKinds, ['scene-override']);
|
|
418
|
+
assert.equal(reason.lightweight, true);
|
|
419
|
+
assert.equal(reason.resourceType, 'scene');
|
|
419
420
|
});
|
|
420
|
-
test('processUnityResources
|
|
421
|
+
test('processUnityResources keeps UNITY_RESOURCE_SUMMARY reason compact when bindings include assetRefPaths', async () => {
|
|
421
422
|
const graph = createKnowledgeGraph();
|
|
422
423
|
const classId = generateId('Class', 'Assets/Scripts/CharacterList.cs:CharacterList');
|
|
423
424
|
graph.addNode({
|
|
@@ -464,14 +465,11 @@ test('processUnityResources includes structured assetRefPaths in component paylo
|
|
|
464
465
|
unityDiagnostics: [],
|
|
465
466
|
}),
|
|
466
467
|
});
|
|
467
|
-
const
|
|
468
|
-
assert.ok(
|
|
469
|
-
|
|
470
|
-
assert.equal(payload.assetRefPaths?.length, 1);
|
|
471
|
-
assert.equal(payload.assetRefPaths?.[0]?.fieldName, '_Head_Ref');
|
|
472
|
-
assert.equal(payload.assetRefPaths?.[0]?.isSprite, true);
|
|
468
|
+
const summary = [...graph.iterRelationships()].find((rel) => rel.type === 'UNITY_RESOURCE_SUMMARY');
|
|
469
|
+
assert.ok(summary);
|
|
470
|
+
assert.equal(String(summary.reason).includes('assetRefPaths'), false);
|
|
473
471
|
});
|
|
474
|
-
test('processUnityResources
|
|
472
|
+
test('processUnityResources summary-only persistence ignores full payload mode and keeps summary reason', async () => {
|
|
475
473
|
const graph = createKnowledgeGraph();
|
|
476
474
|
const classId = generateId('Class', 'Assets/Scripts/Full.cs:FullSymbol');
|
|
477
475
|
graph.addNode({
|
|
@@ -507,12 +505,12 @@ test('processUnityResources writes full unity payload when payloadMode=full', as
|
|
|
507
505
|
unityDiagnostics: [],
|
|
508
506
|
}),
|
|
509
507
|
});
|
|
510
|
-
const
|
|
511
|
-
assert.ok(
|
|
512
|
-
const
|
|
513
|
-
assert.equal(
|
|
514
|
-
assert.
|
|
515
|
-
assert.equal(
|
|
508
|
+
const summary = [...graph.iterRelationships()].find((rel) => rel.type === 'UNITY_RESOURCE_SUMMARY');
|
|
509
|
+
assert.ok(summary);
|
|
510
|
+
const reason = JSON.parse(String(summary.reason || '{}'));
|
|
511
|
+
assert.equal(reason.resourceType, 'scene');
|
|
512
|
+
assert.deepEqual(reason.bindingKinds, ['scene-override']);
|
|
513
|
+
assert.equal(String(summary.reason).includes('evidence'), false);
|
|
516
514
|
});
|
|
517
515
|
test('processUnityResources aggregates repetitive diagnostics with capped samples', async () => {
|
|
518
516
|
const graph = createKnowledgeGraph();
|
|
@@ -11,8 +11,27 @@
|
|
|
11
11
|
* - Double quotes within fields are escaped by doubling them ("")
|
|
12
12
|
* - All fields are consistently quoted for safety with code content
|
|
13
13
|
*/
|
|
14
|
-
import { KnowledgeGraph } from '../graph/types.js';
|
|
14
|
+
import { KnowledgeGraph, GraphNode } from '../graph/types.js';
|
|
15
15
|
import { NodeTableName } from './schema.js';
|
|
16
|
+
/**
|
|
17
|
+
* LRU content cache — avoids re-reading the same source file for every
|
|
18
|
+
* symbol defined in it. Sized generously so most files stay cached during
|
|
19
|
+
* the single-pass node iteration.
|
|
20
|
+
*/
|
|
21
|
+
export declare class FileContentCache {
|
|
22
|
+
private repoPath;
|
|
23
|
+
private maxBytes;
|
|
24
|
+
private cache;
|
|
25
|
+
private currentBytes;
|
|
26
|
+
constructor(repoPath: string, maxBytes?: number);
|
|
27
|
+
get(relativePath: string): Promise<string>;
|
|
28
|
+
setForTest(key: string, value: string): void;
|
|
29
|
+
hasForTest(key: string): boolean;
|
|
30
|
+
private touch;
|
|
31
|
+
private set;
|
|
32
|
+
private evictIfNeeded;
|
|
33
|
+
}
|
|
34
|
+
export declare function toCodeElementCsvRow(node: GraphNode, contentCache?: FileContentCache): Promise<string>;
|
|
16
35
|
export interface StreamedCSVResult {
|
|
17
36
|
nodeFiles: Map<NodeTableName, {
|
|
18
37
|
csvPath: string;
|
|
@@ -39,6 +39,7 @@ const escapeCSVNumber = (value, defaultValue = -1) => {
|
|
|
39
39
|
return String(defaultValue);
|
|
40
40
|
return String(value);
|
|
41
41
|
};
|
|
42
|
+
const UNITY_RESOURCE_PATH_PATTERN = /\.(prefab|unity|asset)$/i;
|
|
42
43
|
// ============================================================================
|
|
43
44
|
// CONTENT EXTRACTION (lazy — reads from disk on demand)
|
|
44
45
|
// ============================================================================
|
|
@@ -59,21 +60,23 @@ const isBinaryContent = (content) => {
|
|
|
59
60
|
* symbol defined in it. Sized generously so most files stay cached during
|
|
60
61
|
* the single-pass node iteration.
|
|
61
62
|
*/
|
|
62
|
-
class FileContentCache {
|
|
63
|
-
cache = new Map();
|
|
64
|
-
accessOrder = [];
|
|
65
|
-
maxSize;
|
|
63
|
+
export class FileContentCache {
|
|
66
64
|
repoPath;
|
|
67
|
-
|
|
65
|
+
maxBytes;
|
|
66
|
+
cache = new Map();
|
|
67
|
+
currentBytes = 0;
|
|
68
|
+
constructor(repoPath, maxBytes = 128 * 1024 * 1024) {
|
|
68
69
|
this.repoPath = repoPath;
|
|
69
|
-
this.
|
|
70
|
+
this.maxBytes = maxBytes;
|
|
70
71
|
}
|
|
71
72
|
async get(relativePath) {
|
|
72
73
|
if (!relativePath)
|
|
73
74
|
return '';
|
|
74
75
|
const cached = this.cache.get(relativePath);
|
|
75
|
-
if (cached !== undefined)
|
|
76
|
-
|
|
76
|
+
if (cached !== undefined) {
|
|
77
|
+
this.touch(relativePath, cached);
|
|
78
|
+
return cached.content;
|
|
79
|
+
}
|
|
77
80
|
try {
|
|
78
81
|
const fullPath = path.join(this.repoPath, relativePath);
|
|
79
82
|
const content = await fs.readFile(fullPath, 'utf-8');
|
|
@@ -85,14 +88,38 @@ class FileContentCache {
|
|
|
85
88
|
return '';
|
|
86
89
|
}
|
|
87
90
|
}
|
|
91
|
+
setForTest(key, value) {
|
|
92
|
+
this.set(key, value);
|
|
93
|
+
}
|
|
94
|
+
hasForTest(key) {
|
|
95
|
+
return this.cache.has(key);
|
|
96
|
+
}
|
|
97
|
+
touch(key, value) {
|
|
98
|
+
this.cache.delete(key);
|
|
99
|
+
this.cache.set(key, value);
|
|
100
|
+
}
|
|
88
101
|
set(key, value) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
const prev = this.cache.get(key);
|
|
103
|
+
if (prev) {
|
|
104
|
+
this.currentBytes -= prev.sizeBytes;
|
|
105
|
+
this.cache.delete(key);
|
|
106
|
+
}
|
|
107
|
+
const sizeBytes = Buffer.byteLength(value, 'utf-8');
|
|
108
|
+
this.cache.set(key, { content: value, sizeBytes });
|
|
109
|
+
this.currentBytes += sizeBytes;
|
|
110
|
+
this.evictIfNeeded();
|
|
111
|
+
}
|
|
112
|
+
evictIfNeeded() {
|
|
113
|
+
while (this.currentBytes > this.maxBytes && this.cache.size > 0) {
|
|
114
|
+
const oldestKey = this.cache.keys().next().value;
|
|
115
|
+
if (!oldestKey)
|
|
116
|
+
break;
|
|
117
|
+
const oldest = this.cache.get(oldestKey);
|
|
118
|
+
this.cache.delete(oldestKey);
|
|
119
|
+
if (oldest) {
|
|
120
|
+
this.currentBytes -= oldest.sizeBytes;
|
|
121
|
+
}
|
|
93
122
|
}
|
|
94
|
-
this.cache.set(key, value);
|
|
95
|
-
this.accessOrder.push(key);
|
|
96
123
|
}
|
|
97
124
|
}
|
|
98
125
|
const extractContent = async (node, contentCache) => {
|
|
@@ -123,6 +150,56 @@ const extractContent = async (node, contentCache) => {
|
|
|
123
150
|
? snippet.slice(0, MAX_SNIPPET) + '\n... [truncated]'
|
|
124
151
|
: snippet;
|
|
125
152
|
};
|
|
153
|
+
function compactUnityComponentDescription(description) {
|
|
154
|
+
const raw = typeof description === 'string' ? description : '';
|
|
155
|
+
if (!raw)
|
|
156
|
+
return '';
|
|
157
|
+
try {
|
|
158
|
+
const parsed = JSON.parse(raw);
|
|
159
|
+
const compact = {};
|
|
160
|
+
if (parsed.bindingKind)
|
|
161
|
+
compact.bindingKind = parsed.bindingKind;
|
|
162
|
+
if (parsed.componentObjectId)
|
|
163
|
+
compact.componentObjectId = parsed.componentObjectId;
|
|
164
|
+
if (parsed.lightweight)
|
|
165
|
+
compact.lightweight = true;
|
|
166
|
+
const scalarFields = parsed.serializedFields?.scalarFields || [];
|
|
167
|
+
const referenceFields = parsed.serializedFields?.referenceFields || [];
|
|
168
|
+
if (scalarFields.length > 0 || referenceFields.length > 0) {
|
|
169
|
+
compact.serializedFields = { scalarFields, referenceFields };
|
|
170
|
+
}
|
|
171
|
+
if (Array.isArray(parsed.resolvedReferences) && parsed.resolvedReferences.length > 0) {
|
|
172
|
+
compact.resolvedReferences = parsed.resolvedReferences;
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(parsed.assetRefPaths) && parsed.assetRefPaths.length > 0) {
|
|
175
|
+
compact.assetRefPaths = parsed.assetRefPaths;
|
|
176
|
+
}
|
|
177
|
+
return JSON.stringify(Object.keys(compact).length > 0 ? compact : parsed);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return raw;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export async function toCodeElementCsvRow(node, contentCache) {
|
|
184
|
+
const filePath = String(node.properties.filePath || '');
|
|
185
|
+
const isUnityComponentRow = node.label === 'CodeElement' && UNITY_RESOURCE_PATH_PATTERN.test(filePath);
|
|
186
|
+
const content = isUnityComponentRow
|
|
187
|
+
? ''
|
|
188
|
+
: (contentCache ? await extractContent(node, contentCache) : '');
|
|
189
|
+
const description = isUnityComponentRow
|
|
190
|
+
? compactUnityComponentDescription(node.properties.description || '')
|
|
191
|
+
: String(node.properties.description || '');
|
|
192
|
+
return [
|
|
193
|
+
escapeCSVField(node.id),
|
|
194
|
+
escapeCSVField(node.properties.name || ''),
|
|
195
|
+
escapeCSVField(filePath),
|
|
196
|
+
escapeCSVNumber(node.properties.startLine, -1),
|
|
197
|
+
escapeCSVNumber(node.properties.endLine, -1),
|
|
198
|
+
node.properties.isExported ? 'true' : 'false',
|
|
199
|
+
escapeCSVField(content),
|
|
200
|
+
escapeCSVField(description),
|
|
201
|
+
].join(',');
|
|
202
|
+
}
|
|
126
203
|
// ============================================================================
|
|
127
204
|
// BUFFERED CSV WRITER
|
|
128
205
|
// ============================================================================
|
|
@@ -266,17 +343,7 @@ export const streamAllCSVsToDisk = async (graph, repoPath, csvDir) => {
|
|
|
266
343
|
// Code element nodes (Function, Class, Interface, Method, CodeElement)
|
|
267
344
|
const writer = codeWriterMap[node.label];
|
|
268
345
|
if (writer) {
|
|
269
|
-
|
|
270
|
-
await writer.addRow([
|
|
271
|
-
escapeCSVField(node.id),
|
|
272
|
-
escapeCSVField(node.properties.name || ''),
|
|
273
|
-
escapeCSVField(node.properties.filePath || ''),
|
|
274
|
-
escapeCSVNumber(node.properties.startLine, -1),
|
|
275
|
-
escapeCSVNumber(node.properties.endLine, -1),
|
|
276
|
-
node.properties.isExported ? 'true' : 'false',
|
|
277
|
-
escapeCSVField(content),
|
|
278
|
-
escapeCSVField(node.properties.description || ''),
|
|
279
|
-
].join(','));
|
|
346
|
+
await writer.addRow(await toCodeElementCsvRow(node, contentCache));
|
|
280
347
|
}
|
|
281
348
|
else {
|
|
282
349
|
// Multi-language node types (Struct, Impl, Trait, Macro, etc.)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { FileContentCache, toCodeElementCsvRow } from './csv-generator.js';
|
|
4
|
+
test('FileContentCache evicts oldest entries when byte budget is exceeded', async () => {
|
|
5
|
+
const cache = new FileContentCache('/tmp/repo', 10);
|
|
6
|
+
cache.setForTest('a.cs', '123456');
|
|
7
|
+
cache.setForTest('b.cs', '123456');
|
|
8
|
+
assert.equal(cache.hasForTest('a.cs'), false);
|
|
9
|
+
assert.equal(cache.hasForTest('b.cs'), true);
|
|
10
|
+
});
|
|
11
|
+
test('Unity component CodeElement rows store compact description and empty content', async () => {
|
|
12
|
+
const row = await toCodeElementCsvRow({
|
|
13
|
+
id: 'CodeElement:Assets/A.prefab:114',
|
|
14
|
+
label: 'CodeElement',
|
|
15
|
+
properties: {
|
|
16
|
+
name: 'DoorObj@114',
|
|
17
|
+
filePath: 'Assets/A.prefab',
|
|
18
|
+
startLine: 12,
|
|
19
|
+
endLine: 12,
|
|
20
|
+
description: JSON.stringify({
|
|
21
|
+
bindingKind: 'direct',
|
|
22
|
+
componentObjectId: '114',
|
|
23
|
+
serializedFields: { scalarFields: [], referenceFields: [] },
|
|
24
|
+
}),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
assert.match(row, /,\"\"\,\"\{/);
|
|
28
|
+
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
|
-
import { createReadStream } from 'fs';
|
|
3
|
-
import { createInterface } from 'readline';
|
|
4
2
|
import path from 'path';
|
|
5
3
|
import kuzu from 'kuzu';
|
|
6
4
|
import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, } from './schema.js';
|
|
7
5
|
import { streamAllCSVsToDisk } from './csv-generator.js';
|
|
6
|
+
import { streamRelationshipPairBucketsFromCsv } from './relationship-pair-buckets.js';
|
|
8
7
|
let db = null;
|
|
9
8
|
let conn = null;
|
|
10
9
|
let currentDbPath = null;
|
|
@@ -146,45 +145,18 @@ export const loadGraphToKuzu = async (graph, repoPath, storagePath, onProgress)
|
|
|
146
145
|
}
|
|
147
146
|
}
|
|
148
147
|
// Bulk COPY relationships — split by FROM→TO label pair (KuzuDB requires it)
|
|
149
|
-
// Stream
|
|
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
|
+
}
|