@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
|
@@ -2,30 +2,65 @@ import { extractAssetRefPathReferences } from '../../core/unity/resolver.js';
|
|
|
2
2
|
export async function loadUnityContext(_repoId, symbolId, execute) {
|
|
3
3
|
const escapedSymbolId = symbolId.replace(/'/g, "''");
|
|
4
4
|
const rows = await execute(`
|
|
5
|
-
MATCH (symbol {id: '${escapedSymbolId}'})-[r:CodeRelation]->(
|
|
6
|
-
WHERE r.type IN ['UNITY_COMPONENT_INSTANCE', 'UNITY_SERIALIZED_TYPE_IN']
|
|
7
|
-
RETURN
|
|
8
|
-
|
|
5
|
+
MATCH (symbol {id: '${escapedSymbolId}'})-[r:CodeRelation]->(target)
|
|
6
|
+
WHERE r.type IN ['UNITY_COMPONENT_INSTANCE', 'UNITY_SERIALIZED_TYPE_IN', 'UNITY_RESOURCE_SUMMARY']
|
|
7
|
+
RETURN target.filePath AS resourcePath,
|
|
8
|
+
CASE WHEN r.type = 'UNITY_RESOURCE_SUMMARY' THEN '' ELSE target.description END AS payload,
|
|
9
|
+
r.type AS relationType,
|
|
10
|
+
r.reason AS relationReason
|
|
11
|
+
ORDER BY target.filePath, target.id
|
|
9
12
|
`);
|
|
10
13
|
return projectUnityBindings(rows);
|
|
11
14
|
}
|
|
15
|
+
export function formatLazyHydrationBudgetDiagnostic(elapsedMs) {
|
|
16
|
+
return `lazy-expand budget exceeded after ${elapsedMs}ms`;
|
|
17
|
+
}
|
|
12
18
|
export function projectUnityBindings(rows) {
|
|
13
19
|
const resourceBindings = [];
|
|
14
20
|
const scalarFields = [];
|
|
15
21
|
const referenceFields = [];
|
|
16
22
|
const unityDiagnostics = [];
|
|
17
23
|
for (const row of rows) {
|
|
24
|
+
const relationType = String(row?.relationType || '');
|
|
25
|
+
const relationReason = String(row?.relationReason || '');
|
|
26
|
+
const resourcePath = row?.resourcePath || row?.[0] || '';
|
|
18
27
|
const rawPayload = row?.payload ?? row?.description ?? row?.[1];
|
|
28
|
+
if (relationType === 'UNITY_RESOURCE_SUMMARY') {
|
|
29
|
+
const summary = parseUnityResourceSummaryReason(relationReason);
|
|
30
|
+
const bindingKinds = summary.bindingKinds.length > 0
|
|
31
|
+
? summary.bindingKinds
|
|
32
|
+
: ['direct'];
|
|
33
|
+
const resourceType = summary.resourceType || inferResourceType(resourcePath);
|
|
34
|
+
for (const bindingKind of bindingKinds) {
|
|
35
|
+
resourceBindings.push({
|
|
36
|
+
resourcePath,
|
|
37
|
+
resourceType,
|
|
38
|
+
bindingKind,
|
|
39
|
+
componentObjectId: 'summary',
|
|
40
|
+
lightweight: summary.lightweight,
|
|
41
|
+
evidence: buildSyntheticEvidence(row),
|
|
42
|
+
serializedFields: { scalarFields: [], referenceFields: [] },
|
|
43
|
+
resolvedReferences: [],
|
|
44
|
+
assetRefPaths: [],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
19
49
|
if (typeof rawPayload !== 'string' || rawPayload.length === 0) {
|
|
20
50
|
continue;
|
|
21
51
|
}
|
|
22
52
|
try {
|
|
23
53
|
const parsed = JSON.parse(rawPayload);
|
|
54
|
+
const lightweight = Boolean(parsed.lightweight)
|
|
55
|
+
|| (String(parsed.componentObjectId || '').startsWith('line-')
|
|
56
|
+
&& ((parsed.serializedFields?.scalarFields?.length || 0) === 0)
|
|
57
|
+
&& ((parsed.serializedFields?.referenceFields?.length || 0) === 0));
|
|
24
58
|
const binding = {
|
|
25
|
-
resourcePath: parsed.resourcePath ||
|
|
59
|
+
resourcePath: parsed.resourcePath || resourcePath,
|
|
26
60
|
resourceType: parsed.resourceType || inferResourceType(parsed.resourcePath || row?.resourcePath || row?.[0] || ''),
|
|
27
61
|
bindingKind: parsed.bindingKind || 'direct',
|
|
28
62
|
componentObjectId: parsed.componentObjectId || '',
|
|
63
|
+
lightweight,
|
|
29
64
|
evidence: parsed.evidence || buildSyntheticEvidence(row),
|
|
30
65
|
serializedFields: parsed.serializedFields || { scalarFields: [], referenceFields: [] },
|
|
31
66
|
resolvedReferences: parsed.resolvedReferences || [],
|
|
@@ -48,6 +83,35 @@ export function projectUnityBindings(rows) {
|
|
|
48
83
|
unityDiagnostics,
|
|
49
84
|
};
|
|
50
85
|
}
|
|
86
|
+
function parseUnityResourceSummaryReason(input) {
|
|
87
|
+
const fallback = {
|
|
88
|
+
resourceType: 'scene',
|
|
89
|
+
bindingKinds: [],
|
|
90
|
+
lightweight: true,
|
|
91
|
+
};
|
|
92
|
+
if (!input)
|
|
93
|
+
return fallback;
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(input);
|
|
96
|
+
const bindingKinds = Array.isArray(parsed.bindingKinds)
|
|
97
|
+
? parsed.bindingKinds
|
|
98
|
+
.map((value) => String(value || '').trim())
|
|
99
|
+
.filter((value) => (value === 'direct'
|
|
100
|
+
|| value === 'prefab-instance'
|
|
101
|
+
|| value === 'nested'
|
|
102
|
+
|| value === 'variant'
|
|
103
|
+
|| value === 'scene-override'))
|
|
104
|
+
: [];
|
|
105
|
+
return {
|
|
106
|
+
resourceType: parsed.resourceType || fallback.resourceType,
|
|
107
|
+
bindingKinds,
|
|
108
|
+
lightweight: true,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return fallback;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
51
115
|
function inferResourceType(resourcePath) {
|
|
52
116
|
if (resourcePath.endsWith('.prefab'))
|
|
53
117
|
return 'prefab';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { loadUnityContext, projectUnityBindings } from './unity-enrichment.js';
|
|
3
|
+
import { formatLazyHydrationBudgetDiagnostic, loadUnityContext, projectUnityBindings } from './unity-enrichment.js';
|
|
4
4
|
test('projectUnityBindings restores graph-native Unity payload rows', () => {
|
|
5
5
|
const out = projectUnityBindings([
|
|
6
6
|
{
|
|
@@ -128,3 +128,71 @@ _actorPrefabRef:
|
|
|
128
128
|
assert.equal(refs[1]?.fieldName, '_actorPrefabRef');
|
|
129
129
|
assert.equal(refs[1]?.isSprite, false);
|
|
130
130
|
});
|
|
131
|
+
test('projectUnityBindings preserves lightweight marker from payload', () => {
|
|
132
|
+
const out = projectUnityBindings([
|
|
133
|
+
{
|
|
134
|
+
resourcePath: 'Assets/Scene/LargeScene.unity',
|
|
135
|
+
payload: JSON.stringify({
|
|
136
|
+
resourcePath: 'Assets/Scene/LargeScene.unity',
|
|
137
|
+
resourceType: 'scene',
|
|
138
|
+
bindingKind: 'direct',
|
|
139
|
+
componentObjectId: 'line-200',
|
|
140
|
+
lightweight: true,
|
|
141
|
+
serializedFields: { scalarFields: [], referenceFields: [] },
|
|
142
|
+
}),
|
|
143
|
+
},
|
|
144
|
+
]);
|
|
145
|
+
assert.equal(out.resourceBindings.length, 1);
|
|
146
|
+
assert.equal(out.resourceBindings[0]?.lightweight, true);
|
|
147
|
+
});
|
|
148
|
+
test('projectUnityBindings infers lightweight marker from legacy line-* component id', () => {
|
|
149
|
+
const out = projectUnityBindings([
|
|
150
|
+
{
|
|
151
|
+
resourcePath: 'Assets/Scene/LargeScene.unity',
|
|
152
|
+
payload: JSON.stringify({
|
|
153
|
+
resourcePath: 'Assets/Scene/LargeScene.unity',
|
|
154
|
+
resourceType: 'scene',
|
|
155
|
+
bindingKind: 'direct',
|
|
156
|
+
componentObjectId: 'line-54558',
|
|
157
|
+
serializedFields: { scalarFields: [], referenceFields: [] },
|
|
158
|
+
resolvedReferences: [],
|
|
159
|
+
}),
|
|
160
|
+
},
|
|
161
|
+
]);
|
|
162
|
+
assert.equal(out.resourceBindings.length, 1);
|
|
163
|
+
assert.equal(out.resourceBindings[0]?.lightweight, true);
|
|
164
|
+
});
|
|
165
|
+
test('projectUnityBindings restores compact component payload rows without embedded resourcePath', () => {
|
|
166
|
+
const out = projectUnityBindings([
|
|
167
|
+
{
|
|
168
|
+
resourcePath: 'Assets/A.prefab',
|
|
169
|
+
payload: JSON.stringify({
|
|
170
|
+
bindingKind: 'direct',
|
|
171
|
+
componentObjectId: '114',
|
|
172
|
+
serializedFields: { scalarFields: [], referenceFields: [] },
|
|
173
|
+
}),
|
|
174
|
+
},
|
|
175
|
+
]);
|
|
176
|
+
assert.equal(out.resourceBindings.length, 1);
|
|
177
|
+
assert.equal(out.resourceBindings[0]?.resourcePath, 'Assets/A.prefab');
|
|
178
|
+
assert.equal(out.resourceBindings[0]?.bindingKind, 'direct');
|
|
179
|
+
assert.deepEqual(out.unityDiagnostics, []);
|
|
180
|
+
});
|
|
181
|
+
test('loadUnityContext can project UNITY_RESOURCE_SUMMARY rows before hydration', async () => {
|
|
182
|
+
const out = await loadUnityContext('repo-id', 'Class:Assets/Scripts/DoorObj.cs:DoorObj', async () => [
|
|
183
|
+
{
|
|
184
|
+
relationType: 'UNITY_RESOURCE_SUMMARY',
|
|
185
|
+
relationReason: JSON.stringify({ resourceType: 'prefab', bindingKinds: ['direct'], lightweight: true }),
|
|
186
|
+
resourcePath: 'Assets/Doors/Door.prefab',
|
|
187
|
+
payload: '',
|
|
188
|
+
},
|
|
189
|
+
]);
|
|
190
|
+
assert.equal(out.resourceBindings.length, 1);
|
|
191
|
+
assert.equal(out.resourceBindings[0]?.resourcePath, 'Assets/Doors/Door.prefab');
|
|
192
|
+
assert.equal(out.resourceBindings[0]?.lightweight, true);
|
|
193
|
+
});
|
|
194
|
+
test('formatLazyHydrationBudgetDiagnostic returns stable budget warning', () => {
|
|
195
|
+
const message = formatLazyHydrationBudgetDiagnostic(17);
|
|
196
|
+
assert.match(message, /budget exceeded/i);
|
|
197
|
+
assert.match(message, /17ms/);
|
|
198
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function resolveUnityLazyConfig(env) {
|
|
2
|
+
return {
|
|
3
|
+
maxPendingPathsPerRequest: Number(env.GITNEXUS_UNITY_LAZY_MAX_PATHS || 120),
|
|
4
|
+
batchSize: Number(env.GITNEXUS_UNITY_LAZY_BATCH_SIZE || 30),
|
|
5
|
+
maxHydrationMs: Number(env.GITNEXUS_UNITY_LAZY_MAX_MS || 5000),
|
|
6
|
+
};
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { resolveUnityLazyConfig } from './unity-lazy-config.js';
|
|
4
|
+
test('resolveUnityLazyConfig provides safe defaults', () => {
|
|
5
|
+
const cfg = resolveUnityLazyConfig({});
|
|
6
|
+
assert.equal(cfg.maxPendingPathsPerRequest, 120);
|
|
7
|
+
assert.equal(cfg.batchSize, 30);
|
|
8
|
+
assert.equal(cfg.maxHydrationMs, 5000);
|
|
9
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ResolvedUnityBinding } from '../../core/unity/resolver.js';
|
|
2
|
+
import type { UnityLazyConfig } from './unity-lazy-config.js';
|
|
3
|
+
export interface HydrateLazyBindingsInput {
|
|
4
|
+
pendingPaths: string[];
|
|
5
|
+
config: UnityLazyConfig;
|
|
6
|
+
resolveBatch: (paths: string[]) => Promise<Map<string, ResolvedUnityBinding[]>>;
|
|
7
|
+
dedupeKey?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface HydrateLazyBindingsOutput {
|
|
10
|
+
resolvedByPath: Map<string, ResolvedUnityBinding[]>;
|
|
11
|
+
timedOut: boolean;
|
|
12
|
+
elapsedMs: number;
|
|
13
|
+
diagnostics: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function hydrateLazyBindings(input: HydrateLazyBindingsInput): Promise<HydrateLazyBindingsOutput>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const inFlightHydration = new Map();
|
|
2
|
+
export async function hydrateLazyBindings(input) {
|
|
3
|
+
if (!input.dedupeKey) {
|
|
4
|
+
return runHydration(input);
|
|
5
|
+
}
|
|
6
|
+
const existing = inFlightHydration.get(input.dedupeKey);
|
|
7
|
+
if (existing) {
|
|
8
|
+
return existing;
|
|
9
|
+
}
|
|
10
|
+
const pending = runHydration(input).finally(() => {
|
|
11
|
+
inFlightHydration.delete(input.dedupeKey);
|
|
12
|
+
});
|
|
13
|
+
inFlightHydration.set(input.dedupeKey, pending);
|
|
14
|
+
return pending;
|
|
15
|
+
}
|
|
16
|
+
async function runHydration(input) {
|
|
17
|
+
const pending = input.pendingPaths.slice(0, Math.max(0, input.config.maxPendingPathsPerRequest));
|
|
18
|
+
const batchSize = Math.max(1, input.config.batchSize);
|
|
19
|
+
const startedAt = Date.now();
|
|
20
|
+
const resolvedByPath = new Map();
|
|
21
|
+
let timedOut = false;
|
|
22
|
+
const diagnostics = [];
|
|
23
|
+
for (let i = 0; i < pending.length; i += batchSize) {
|
|
24
|
+
if (Date.now() - startedAt > input.config.maxHydrationMs) {
|
|
25
|
+
timedOut = true;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
const chunk = pending.slice(i, i + batchSize);
|
|
29
|
+
const resolved = await input.resolveBatch(chunk);
|
|
30
|
+
for (const [resourcePath, bindings] of resolved.entries()) {
|
|
31
|
+
resolvedByPath.set(resourcePath, bindings);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (timedOut) {
|
|
35
|
+
diagnostics.push(`lazy-expand budget exceeded after ${Date.now() - startedAt}ms`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
resolvedByPath,
|
|
39
|
+
timedOut,
|
|
40
|
+
elapsedMs: Date.now() - startedAt,
|
|
41
|
+
diagnostics,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { hydrateLazyBindings } from './unity-lazy-hydrator.js';
|
|
4
|
+
test('hydrateLazyBindings processes pending paths in bounded chunks', async () => {
|
|
5
|
+
const calls = [];
|
|
6
|
+
await hydrateLazyBindings({
|
|
7
|
+
pendingPaths: ['a', 'b', 'c', 'd', 'e'],
|
|
8
|
+
config: { maxPendingPathsPerRequest: 4, batchSize: 2, maxHydrationMs: 5000 },
|
|
9
|
+
resolveBatch: async (paths) => {
|
|
10
|
+
calls.push(paths);
|
|
11
|
+
return new Map();
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
assert.deepEqual(calls, [['a', 'b'], ['c', 'd']]);
|
|
15
|
+
});
|
|
16
|
+
test('parallel requests dedupe same hydration work', async () => {
|
|
17
|
+
let resolveCalls = 0;
|
|
18
|
+
const sharedInput = {
|
|
19
|
+
pendingPaths: ['Assets/A.prefab'],
|
|
20
|
+
config: { maxPendingPathsPerRequest: 10, batchSize: 5, maxHydrationMs: 5000 },
|
|
21
|
+
dedupeKey: 'symbol:door::Assets/A.prefab',
|
|
22
|
+
resolveBatch: async (_paths) => {
|
|
23
|
+
resolveCalls += 1;
|
|
24
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
25
|
+
return new Map();
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
await Promise.all([
|
|
29
|
+
hydrateLazyBindings(sharedInput),
|
|
30
|
+
hydrateLazyBindings(sharedInput),
|
|
31
|
+
]);
|
|
32
|
+
assert.equal(resolveCalls, 1);
|
|
33
|
+
});
|
|
34
|
+
test('context lazy hydration returns partial results when budget exceeded and reports diagnostics', async () => {
|
|
35
|
+
const out = await hydrateLazyBindings({
|
|
36
|
+
pendingPaths: ['a', 'b', 'c', 'd'],
|
|
37
|
+
config: { maxPendingPathsPerRequest: 4, batchSize: 2, maxHydrationMs: 1 },
|
|
38
|
+
resolveBatch: async (paths) => {
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
40
|
+
return new Map(paths.map((p) => [p, []]));
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
assert.equal(out.resolvedByPath.size, 2);
|
|
44
|
+
assert.match((out.diagnostics || []).join('\n'), /budget exceeded/i);
|
|
45
|
+
});
|
|
46
|
+
test('summary-only Unity analyze persistence still returns full bindings after lazy hydration', async () => {
|
|
47
|
+
const out = await hydrateLazyBindings({
|
|
48
|
+
pendingPaths: ['Assets/Doors/Door.prefab'],
|
|
49
|
+
config: { maxPendingPathsPerRequest: 10, batchSize: 5, maxHydrationMs: 5000 },
|
|
50
|
+
resolveBatch: async () => new Map([
|
|
51
|
+
['Assets/Doors/Door.prefab', [{
|
|
52
|
+
resourcePath: 'Assets/Doors/Door.prefab',
|
|
53
|
+
resourceType: 'prefab',
|
|
54
|
+
bindingKind: 'direct',
|
|
55
|
+
componentObjectId: '114',
|
|
56
|
+
serializedFields: {
|
|
57
|
+
scalarFields: [{ name: 'Shows', value: '1', sourceLayer: 'prefab' }],
|
|
58
|
+
referenceFields: [],
|
|
59
|
+
},
|
|
60
|
+
resolvedReferences: [],
|
|
61
|
+
evidence: { line: 12, lineText: 'm_Script: ...' },
|
|
62
|
+
}]],
|
|
63
|
+
]),
|
|
64
|
+
});
|
|
65
|
+
assert.equal(out.resolvedByPath.get('Assets/Doors/Door.prefab')?.[0]?.serializedFields.scalarFields[0]?.name, 'Shows');
|
|
66
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ResolvedUnityBinding } from '../../core/unity/resolver.js';
|
|
2
|
+
export declare function readUnityOverlayBindings(storagePath: string, indexedCommit: string, symbolUid: string, resourcePaths: string[]): Promise<Map<string, ResolvedUnityBinding[]>>;
|
|
3
|
+
export declare function upsertUnityOverlayBindings(storagePath: string, indexedCommit: string, symbolUid: string, byResourcePath: Map<string, ResolvedUnityBinding[]>): Promise<void>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
const OVERLAY_DIRNAME = 'unity-lazy-overlay';
|
|
5
|
+
function buildKey(symbolUid, resourcePath) {
|
|
6
|
+
return `${symbolUid}::${resourcePath}`;
|
|
7
|
+
}
|
|
8
|
+
function shardKeyForEntry(symbolUid, resourcePath) {
|
|
9
|
+
const key = buildKey(symbolUid, resourcePath);
|
|
10
|
+
return createHash('sha1').update(key).digest('hex').slice(0, 2);
|
|
11
|
+
}
|
|
12
|
+
function getShardPath(storagePath, shardKey) {
|
|
13
|
+
return path.join(storagePath, OVERLAY_DIRNAME, `${shardKey}.json`);
|
|
14
|
+
}
|
|
15
|
+
async function readOverlayDocument(storagePath, indexedCommit, shardKey) {
|
|
16
|
+
const overlayPath = getShardPath(storagePath, shardKey);
|
|
17
|
+
try {
|
|
18
|
+
const raw = await fs.readFile(overlayPath, 'utf-8');
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
if (!parsed || parsed.version !== 1 || !parsed.entries || typeof parsed.entries !== 'object') {
|
|
21
|
+
return { version: 1, indexedCommit, entries: {} };
|
|
22
|
+
}
|
|
23
|
+
if (parsed.indexedCommit !== indexedCommit) {
|
|
24
|
+
return { version: 1, indexedCommit, entries: {} };
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code === 'ENOENT') {
|
|
30
|
+
return { version: 1, indexedCommit, entries: {} };
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function writeOverlayDocument(storagePath, shardKey, doc) {
|
|
36
|
+
const overlayDir = path.join(storagePath, OVERLAY_DIRNAME);
|
|
37
|
+
const overlayPath = getShardPath(storagePath, shardKey);
|
|
38
|
+
const tmpPath = `${overlayPath}.tmp-${process.pid}-${Date.now()}`;
|
|
39
|
+
await fs.mkdir(overlayDir, { recursive: true });
|
|
40
|
+
await fs.writeFile(tmpPath, JSON.stringify(doc), 'utf-8');
|
|
41
|
+
await fs.rename(tmpPath, overlayPath);
|
|
42
|
+
}
|
|
43
|
+
export async function readUnityOverlayBindings(storagePath, indexedCommit, symbolUid, resourcePaths) {
|
|
44
|
+
const output = new Map();
|
|
45
|
+
const shardToPaths = new Map();
|
|
46
|
+
for (const resourcePath of resourcePaths) {
|
|
47
|
+
const shardKey = shardKeyForEntry(symbolUid, resourcePath);
|
|
48
|
+
const list = shardToPaths.get(shardKey) || [];
|
|
49
|
+
list.push(resourcePath);
|
|
50
|
+
shardToPaths.set(shardKey, list);
|
|
51
|
+
}
|
|
52
|
+
for (const [shardKey, paths] of shardToPaths.entries()) {
|
|
53
|
+
const doc = await readOverlayDocument(storagePath, indexedCommit, shardKey);
|
|
54
|
+
for (const resourcePath of paths) {
|
|
55
|
+
const key = buildKey(symbolUid, resourcePath);
|
|
56
|
+
const entry = doc.entries[key];
|
|
57
|
+
if (entry && Array.isArray(entry.bindings)) {
|
|
58
|
+
output.set(resourcePath, entry.bindings);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return output;
|
|
63
|
+
}
|
|
64
|
+
export async function upsertUnityOverlayBindings(storagePath, indexedCommit, symbolUid, byResourcePath) {
|
|
65
|
+
if (byResourcePath.size === 0) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const now = new Date().toISOString();
|
|
69
|
+
const shardToEntries = new Map();
|
|
70
|
+
for (const [resourcePath, bindings] of byResourcePath.entries()) {
|
|
71
|
+
const shardKey = shardKeyForEntry(symbolUid, resourcePath);
|
|
72
|
+
const rows = shardToEntries.get(shardKey) || [];
|
|
73
|
+
rows.push([resourcePath, bindings]);
|
|
74
|
+
shardToEntries.set(shardKey, rows);
|
|
75
|
+
}
|
|
76
|
+
for (const [shardKey, rows] of shardToEntries.entries()) {
|
|
77
|
+
const doc = await readOverlayDocument(storagePath, indexedCommit, shardKey);
|
|
78
|
+
for (const [resourcePath, bindings] of rows) {
|
|
79
|
+
const key = buildKey(symbolUid, resourcePath);
|
|
80
|
+
doc.entries[key] = {
|
|
81
|
+
symbolUid,
|
|
82
|
+
resourcePath,
|
|
83
|
+
bindings,
|
|
84
|
+
updatedAt: now,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
await writeOverlayDocument(storagePath, shardKey, doc);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import { readUnityOverlayBindings, upsertUnityOverlayBindings } from './unity-lazy-overlay.js';
|
|
7
|
+
test('unity lazy overlay reads and writes by symbol/resource key', async () => {
|
|
8
|
+
const storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-unity-overlay-'));
|
|
9
|
+
try {
|
|
10
|
+
const before = await readUnityOverlayBindings(storagePath, 'abc123', 'Class:Foo', ['Assets/A.prefab']);
|
|
11
|
+
assert.equal(before.size, 0);
|
|
12
|
+
await upsertUnityOverlayBindings(storagePath, 'abc123', 'Class:Foo', new Map([
|
|
13
|
+
['Assets/A.prefab', [{
|
|
14
|
+
resourcePath: 'Assets/A.prefab',
|
|
15
|
+
resourceType: 'prefab',
|
|
16
|
+
bindingKind: 'direct',
|
|
17
|
+
componentObjectId: '100',
|
|
18
|
+
serializedFields: { scalarFields: [], referenceFields: [] },
|
|
19
|
+
resolvedReferences: [],
|
|
20
|
+
evidence: { line: 1, lineText: 'm_Script: ...' },
|
|
21
|
+
}]],
|
|
22
|
+
]));
|
|
23
|
+
const after = await readUnityOverlayBindings(storagePath, 'abc123', 'Class:Foo', ['Assets/A.prefab']);
|
|
24
|
+
assert.equal(after.size, 1);
|
|
25
|
+
assert.equal(after.get('Assets/A.prefab')?.[0]?.componentObjectId, '100');
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
await fs.rm(storagePath, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
test('unity lazy overlay invalidates entries on indexed commit change', async () => {
|
|
32
|
+
const storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-unity-overlay-'));
|
|
33
|
+
try {
|
|
34
|
+
await upsertUnityOverlayBindings(storagePath, 'old-commit', 'Class:Foo', new Map([
|
|
35
|
+
['Assets/A.prefab', [{
|
|
36
|
+
resourcePath: 'Assets/A.prefab',
|
|
37
|
+
resourceType: 'prefab',
|
|
38
|
+
bindingKind: 'direct',
|
|
39
|
+
componentObjectId: '100',
|
|
40
|
+
serializedFields: { scalarFields: [], referenceFields: [] },
|
|
41
|
+
resolvedReferences: [],
|
|
42
|
+
evidence: { line: 1, lineText: 'm_Script: ...' },
|
|
43
|
+
}]],
|
|
44
|
+
]));
|
|
45
|
+
const stale = await readUnityOverlayBindings(storagePath, 'new-commit', 'Class:Foo', ['Assets/A.prefab']);
|
|
46
|
+
assert.equal(stale.size, 0);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await fs.rm(storagePath, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
test('overlay persists entries in shard files and supports atomic replace', async () => {
|
|
53
|
+
const storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-unity-overlay-'));
|
|
54
|
+
try {
|
|
55
|
+
await upsertUnityOverlayBindings(storagePath, 'abc123', 'Class:Foo', new Map([
|
|
56
|
+
['Assets/A.prefab', [{
|
|
57
|
+
resourcePath: 'Assets/A.prefab',
|
|
58
|
+
resourceType: 'prefab',
|
|
59
|
+
bindingKind: 'direct',
|
|
60
|
+
componentObjectId: '101',
|
|
61
|
+
serializedFields: { scalarFields: [], referenceFields: [] },
|
|
62
|
+
resolvedReferences: [],
|
|
63
|
+
evidence: { line: 1, lineText: 'm_Script: ...' },
|
|
64
|
+
}]],
|
|
65
|
+
['Assets/B.prefab', [{
|
|
66
|
+
resourcePath: 'Assets/B.prefab',
|
|
67
|
+
resourceType: 'prefab',
|
|
68
|
+
bindingKind: 'direct',
|
|
69
|
+
componentObjectId: '102',
|
|
70
|
+
serializedFields: { scalarFields: [], referenceFields: [] },
|
|
71
|
+
resolvedReferences: [],
|
|
72
|
+
evidence: { line: 1, lineText: 'm_Script: ...' },
|
|
73
|
+
}]],
|
|
74
|
+
]));
|
|
75
|
+
const shardsDir = path.join(storagePath, 'unity-lazy-overlay');
|
|
76
|
+
const shards = await fs.readdir(shardsDir);
|
|
77
|
+
assert.ok(shards.length > 0);
|
|
78
|
+
assert.ok(shards.every((name) => name.endsWith('.json')));
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
await fs.rm(storagePath, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { UnityContextPayload } from './unity-enrichment.js';
|
|
2
|
+
interface UnityParityCacheOptions {
|
|
3
|
+
maxEntries?: number;
|
|
4
|
+
}
|
|
5
|
+
export declare function readUnityParityCache(storagePath: string, indexedCommit: string, symbolUid: string): Promise<UnityContextPayload | null>;
|
|
6
|
+
export declare function upsertUnityParityCache(storagePath: string, indexedCommit: string, symbolUid: string, payload: UnityContextPayload, options?: UnityParityCacheOptions): Promise<void>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
const PARITY_CACHE_DIRNAME = 'unity-parity-cache';
|
|
5
|
+
const DEFAULT_MAX_PARITY_CACHE_ENTRIES = 500;
|
|
6
|
+
function buildKey(symbolUid) {
|
|
7
|
+
return symbolUid;
|
|
8
|
+
}
|
|
9
|
+
function shardKeyForEntry(symbolUid) {
|
|
10
|
+
return createHash('sha1').update(buildKey(symbolUid)).digest('hex').slice(0, 2);
|
|
11
|
+
}
|
|
12
|
+
function getShardPath(storagePath, shardKey) {
|
|
13
|
+
return path.join(storagePath, PARITY_CACHE_DIRNAME, `${shardKey}.json`);
|
|
14
|
+
}
|
|
15
|
+
async function readParityCacheDocument(storagePath, indexedCommit, shardKey) {
|
|
16
|
+
const cachePath = getShardPath(storagePath, shardKey);
|
|
17
|
+
try {
|
|
18
|
+
const raw = await fs.readFile(cachePath, 'utf-8');
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
if (!parsed || parsed.version !== 1 || !parsed.entries || typeof parsed.entries !== 'object') {
|
|
21
|
+
return { version: 1, indexedCommit, entries: {} };
|
|
22
|
+
}
|
|
23
|
+
if (parsed.indexedCommit !== indexedCommit) {
|
|
24
|
+
return { version: 1, indexedCommit, entries: {} };
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code === 'ENOENT') {
|
|
30
|
+
return { version: 1, indexedCommit, entries: {} };
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function writeParityCacheDocument(storagePath, shardKey, doc) {
|
|
36
|
+
const cacheDir = path.join(storagePath, PARITY_CACHE_DIRNAME);
|
|
37
|
+
const cachePath = getShardPath(storagePath, shardKey);
|
|
38
|
+
const tmpPath = `${cachePath}.tmp-${process.pid}-${Date.now()}`;
|
|
39
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
40
|
+
await fs.writeFile(tmpPath, JSON.stringify(doc), 'utf-8');
|
|
41
|
+
await fs.rename(tmpPath, cachePath);
|
|
42
|
+
}
|
|
43
|
+
export async function readUnityParityCache(storagePath, indexedCommit, symbolUid) {
|
|
44
|
+
const shardKey = shardKeyForEntry(symbolUid);
|
|
45
|
+
const doc = await readParityCacheDocument(storagePath, indexedCommit, shardKey);
|
|
46
|
+
const key = buildKey(symbolUid);
|
|
47
|
+
const entry = doc.entries[key];
|
|
48
|
+
if (!entry || !entry.payload) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return entry.payload;
|
|
52
|
+
}
|
|
53
|
+
export async function upsertUnityParityCache(storagePath, indexedCommit, symbolUid, payload, options) {
|
|
54
|
+
const shardKey = shardKeyForEntry(symbolUid);
|
|
55
|
+
const doc = await readParityCacheDocument(storagePath, indexedCommit, shardKey);
|
|
56
|
+
const key = buildKey(symbolUid);
|
|
57
|
+
doc.entries[key] = {
|
|
58
|
+
symbolUid,
|
|
59
|
+
payload,
|
|
60
|
+
updatedAt: new Date().toISOString(),
|
|
61
|
+
};
|
|
62
|
+
doc.entries = pruneOldestEntries(doc.entries, resolveMaxEntries(options));
|
|
63
|
+
await writeParityCacheDocument(storagePath, shardKey, doc);
|
|
64
|
+
}
|
|
65
|
+
function resolveMaxEntries(options) {
|
|
66
|
+
if (Number.isFinite(options?.maxEntries) && Number(options?.maxEntries) > 0) {
|
|
67
|
+
return Math.floor(Number(options?.maxEntries));
|
|
68
|
+
}
|
|
69
|
+
const raw = String(process.env.GITNEXUS_UNITY_PARITY_CACHE_MAX_ENTRIES || '').trim();
|
|
70
|
+
const parsed = Number.parseInt(raw, 10);
|
|
71
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
72
|
+
return parsed;
|
|
73
|
+
}
|
|
74
|
+
return DEFAULT_MAX_PARITY_CACHE_ENTRIES;
|
|
75
|
+
}
|
|
76
|
+
function pruneOldestEntries(entries, maxEntries) {
|
|
77
|
+
const rows = Object.entries(entries);
|
|
78
|
+
if (rows.length <= maxEntries) {
|
|
79
|
+
return entries;
|
|
80
|
+
}
|
|
81
|
+
rows.sort(([, a], [, b]) => toMillis(a.updatedAt) - toMillis(b.updatedAt));
|
|
82
|
+
const keep = rows.slice(rows.length - maxEntries);
|
|
83
|
+
return Object.fromEntries(keep);
|
|
84
|
+
}
|
|
85
|
+
function toMillis(updatedAt) {
|
|
86
|
+
const ts = Date.parse(updatedAt);
|
|
87
|
+
return Number.isFinite(ts) ? ts : 0;
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|