@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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { buildUnityParitySeed } from './unity-parity-seed.js';
|
|
4
|
+
test('buildUnityParitySeed extracts canonical script/guid/resource indexes', () => {
|
|
5
|
+
const seed = buildUnityParitySeed({
|
|
6
|
+
symbolToScriptPaths: new Map([
|
|
7
|
+
['DoorObj', ['Assets/Code/DoorObj.generated.cs', 'Assets/Code/DoorObj.cs']],
|
|
8
|
+
]),
|
|
9
|
+
symbolToCanonicalScriptPath: new Map([
|
|
10
|
+
['DoorObj', 'Assets/Code/DoorObj.cs'],
|
|
11
|
+
]),
|
|
12
|
+
symbolToScriptPath: new Map([
|
|
13
|
+
['DoorObj', 'Assets/Code/DoorObj.cs'],
|
|
14
|
+
]),
|
|
15
|
+
scriptPathToGuid: new Map([
|
|
16
|
+
['Assets/Code/DoorObj.cs', 'abc123abc123abc123abc123abc123ab'],
|
|
17
|
+
]),
|
|
18
|
+
guidToResourceHits: new Map([
|
|
19
|
+
['abc123abc123abc123abc123abc123ab', [
|
|
20
|
+
{ resourcePath: 'Assets/Prefabs/Door.prefab', resourceType: 'prefab', line: 12, lineText: 'guid: abc123' },
|
|
21
|
+
]],
|
|
22
|
+
]),
|
|
23
|
+
assetGuidToPath: new Map([
|
|
24
|
+
['asset0000000000000000000000000001', 'Assets/Config/Ref.asset'],
|
|
25
|
+
]),
|
|
26
|
+
serializableSymbols: new Set(),
|
|
27
|
+
hostFieldTypeHints: new Map(),
|
|
28
|
+
resourceDocCache: new Map(),
|
|
29
|
+
});
|
|
30
|
+
assert.equal(seed.version, 1);
|
|
31
|
+
assert.equal(seed.symbolToScriptPath.DoorObj, 'Assets/Code/DoorObj.cs');
|
|
32
|
+
assert.equal(seed.scriptPathToGuid['Assets/Code/DoorObj.cs'], 'abc123abc123abc123abc123abc123ab');
|
|
33
|
+
assert.deepEqual(seed.guidToResourcePaths['abc123abc123abc123abc123abc123ab'], ['Assets/Prefabs/Door.prefab']);
|
|
34
|
+
assert.equal(seed.assetGuidToPath?.asset0000000000000000000000000001, 'Assets/Config/Ref.asset');
|
|
35
|
+
});
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { KnowledgeGraph } from '../graph/types.js';
|
|
2
2
|
import { buildUnityScanContext } from '../unity/scan-context.js';
|
|
3
3
|
import { resolveUnityBindings } from '../unity/resolver.js';
|
|
4
|
+
import { type UnityParitySeed } from './unity-parity-seed.js';
|
|
4
5
|
export interface UnityResourceProcessingResult {
|
|
5
6
|
processedSymbols: number;
|
|
6
7
|
bindingCount: number;
|
|
7
8
|
componentCount: number;
|
|
8
9
|
diagnostics: string[];
|
|
10
|
+
paritySeed?: UnityParitySeed;
|
|
9
11
|
timingsMs: {
|
|
10
12
|
scanContext: number;
|
|
11
13
|
resolve: number;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import { performance } from 'node:perf_hooks';
|
|
3
2
|
import { generateId } from '../../lib/utils.js';
|
|
4
3
|
import { buildUnityScanContext } from '../unity/scan-context.js';
|
|
5
4
|
import { resolveUnityBindings } from '../unity/resolver.js';
|
|
5
|
+
import { buildUnityParitySeed } from './unity-parity-seed.js';
|
|
6
6
|
const UNITY_DIAGNOSTIC_SAMPLE_LIMIT = 3;
|
|
7
7
|
export async function processUnityResources(graph, options, deps) {
|
|
8
8
|
const tStart = performance.now();
|
|
@@ -98,37 +98,53 @@ export async function processUnityResources(graph, options, deps) {
|
|
|
98
98
|
continue;
|
|
99
99
|
}
|
|
100
100
|
processedSymbols += 1;
|
|
101
|
+
const tWriteStart = performance.now();
|
|
102
|
+
const summaryBySource = new Map();
|
|
103
|
+
const appendSummary = (sourceNodeId, binding) => {
|
|
104
|
+
const normalizedPath = normalizePath(binding.resourcePath);
|
|
105
|
+
const perPath = summaryBySource.get(sourceNodeId) || new Map();
|
|
106
|
+
const existing = perPath.get(normalizedPath) || {
|
|
107
|
+
resourceType: binding.resourceType,
|
|
108
|
+
bindingKinds: new Set(),
|
|
109
|
+
lightweight: true,
|
|
110
|
+
};
|
|
111
|
+
existing.resourceType = binding.resourceType || existing.resourceType;
|
|
112
|
+
existing.bindingKinds.add(binding.bindingKind);
|
|
113
|
+
existing.lightweight = existing.lightweight && Boolean(binding.lightweight);
|
|
114
|
+
perPath.set(normalizedPath, existing);
|
|
115
|
+
summaryBySource.set(sourceNodeId, perPath);
|
|
116
|
+
};
|
|
101
117
|
for (const binding of resolved.resourceBindings) {
|
|
102
|
-
const tWriteStart = performance.now();
|
|
103
118
|
bindingCount += 1;
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
const componentNode = createComponentNode(symbol, binding, payloadMode);
|
|
107
|
-
graph.addNode(componentNode);
|
|
108
|
-
graph.addRelationship({
|
|
109
|
-
id: generateId('UNITY_COMPONENT_IN', `${classNode.id}:${binding.componentObjectId}->${resourceFileNode.id}`),
|
|
110
|
-
type: 'UNITY_COMPONENT_IN',
|
|
111
|
-
sourceId: classNode.id,
|
|
112
|
-
targetId: resourceFileNode.id,
|
|
113
|
-
confidence: 1.0,
|
|
114
|
-
reason: binding.bindingKind,
|
|
115
|
-
});
|
|
116
|
-
graph.addRelationship({
|
|
117
|
-
id: generateId('UNITY_COMPONENT_INSTANCE', `${classNode.id}->${componentNode.id}`),
|
|
118
|
-
type: 'UNITY_COMPONENT_INSTANCE',
|
|
119
|
-
sourceId: classNode.id,
|
|
120
|
-
targetId: componentNode.id,
|
|
121
|
-
confidence: 1.0,
|
|
122
|
-
reason: binding.bindingKind,
|
|
123
|
-
});
|
|
124
|
-
const serializableTypeLinking = linkSerializableTypeEdges(graph, componentNode, symbol, binding, scanContext, canonicalClassNodeBySymbol);
|
|
119
|
+
appendSummary(classNode.id, binding);
|
|
120
|
+
const serializableTypeLinking = collectSerializableTypeTargetsForBinding(symbol, binding, scanContext, canonicalClassNodeBySymbol);
|
|
125
121
|
serializedTypeEdgeCount += serializableTypeLinking.edgeCount;
|
|
126
122
|
serializedTypeMissCount += serializableTypeLinking.missCount;
|
|
127
123
|
for (const hitSymbol of serializableTypeLinking.symbols) {
|
|
128
124
|
serializedTypeSymbols.add(hitSymbol);
|
|
129
125
|
}
|
|
130
|
-
|
|
126
|
+
for (const targetClassId of serializableTypeLinking.targetClassIds) {
|
|
127
|
+
appendSummary(targetClassId, binding);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const [sourceNodeId, perPath] of summaryBySource.entries()) {
|
|
131
|
+
for (const [resourcePath, summary] of perPath.entries()) {
|
|
132
|
+
const resourceFileId = generateId('File', resourcePath);
|
|
133
|
+
graph.addRelationship({
|
|
134
|
+
id: generateId('UNITY_RESOURCE_SUMMARY', `${sourceNodeId}->${resourceFileId}`),
|
|
135
|
+
type: 'UNITY_RESOURCE_SUMMARY',
|
|
136
|
+
sourceId: sourceNodeId,
|
|
137
|
+
targetId: resourceFileId,
|
|
138
|
+
confidence: 1.0,
|
|
139
|
+
reason: JSON.stringify({
|
|
140
|
+
resourceType: summary.resourceType,
|
|
141
|
+
bindingKinds: [...summary.bindingKinds.values()].sort(),
|
|
142
|
+
lightweight: true,
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
131
146
|
}
|
|
147
|
+
graphWriteMs += performance.now() - tWriteStart;
|
|
132
148
|
}
|
|
133
149
|
catch (error) {
|
|
134
150
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -150,6 +166,7 @@ export async function processUnityResources(graph, options, deps) {
|
|
|
150
166
|
bindingCount,
|
|
151
167
|
componentCount,
|
|
152
168
|
diagnostics,
|
|
169
|
+
paritySeed: scanContext ? buildUnityParitySeed(scanContext) : undefined,
|
|
153
170
|
timingsMs: {
|
|
154
171
|
scanContext: roundMs(scanContextMs),
|
|
155
172
|
resolve: roundMs(resolveMs),
|
|
@@ -193,24 +210,6 @@ function resolveUnityPayloadMode(explicit) {
|
|
|
193
210
|
return 'full';
|
|
194
211
|
return 'compact';
|
|
195
212
|
}
|
|
196
|
-
function ensureResourceFileNode(graph, resourcePath) {
|
|
197
|
-
const normalizedResourcePath = resourcePath.replace(/\\/g, '/');
|
|
198
|
-
const fileId = generateId('File', normalizedResourcePath);
|
|
199
|
-
const existing = graph.getNode(fileId);
|
|
200
|
-
if (existing) {
|
|
201
|
-
return existing;
|
|
202
|
-
}
|
|
203
|
-
const node = {
|
|
204
|
-
id: fileId,
|
|
205
|
-
label: 'File',
|
|
206
|
-
properties: {
|
|
207
|
-
name: path.basename(normalizedResourcePath),
|
|
208
|
-
filePath: normalizedResourcePath,
|
|
209
|
-
},
|
|
210
|
-
};
|
|
211
|
-
graph.addNode(node);
|
|
212
|
-
return node;
|
|
213
|
-
}
|
|
214
213
|
function createComponentNode(symbol, binding, payloadMode) {
|
|
215
214
|
const payload = buildUnityPayload(binding, payloadMode);
|
|
216
215
|
return {
|
|
@@ -230,8 +229,12 @@ function buildUnityPayload(binding, mode) {
|
|
|
230
229
|
bindingKind: binding.bindingKind,
|
|
231
230
|
componentObjectId: binding.componentObjectId,
|
|
232
231
|
};
|
|
233
|
-
if (binding.
|
|
234
|
-
payload.
|
|
232
|
+
if (binding.lightweight) {
|
|
233
|
+
payload.lightweight = true;
|
|
234
|
+
}
|
|
235
|
+
const serializedFields = compactSerializedFieldsForStorage(binding.serializedFields);
|
|
236
|
+
if (serializedFields.scalarFields.length > 0 || serializedFields.referenceFields.length > 0) {
|
|
237
|
+
payload.serializedFields = serializedFields;
|
|
235
238
|
}
|
|
236
239
|
if (binding.resolvedReferences && binding.resolvedReferences.length > 0) {
|
|
237
240
|
payload.resolvedReferences = binding.resolvedReferences;
|
|
@@ -246,6 +249,22 @@ function buildUnityPayload(binding, mode) {
|
|
|
246
249
|
}
|
|
247
250
|
return payload;
|
|
248
251
|
}
|
|
252
|
+
function compactSerializedFieldsForStorage(input) {
|
|
253
|
+
return {
|
|
254
|
+
scalarFields: input.scalarFields.map((field) => ({
|
|
255
|
+
name: field.name,
|
|
256
|
+
sourceLayer: field.sourceLayer,
|
|
257
|
+
value: field.value,
|
|
258
|
+
valueType: field.valueType,
|
|
259
|
+
})),
|
|
260
|
+
referenceFields: input.referenceFields.map((field) => ({
|
|
261
|
+
name: field.name,
|
|
262
|
+
guid: field.guid,
|
|
263
|
+
fileId: field.fileId,
|
|
264
|
+
sourceLayer: field.sourceLayer,
|
|
265
|
+
})),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
249
268
|
function buildCanonicalClassNodeIndex(classNodes, scanContext) {
|
|
250
269
|
const index = new Map();
|
|
251
270
|
for (const classNode of classNodes) {
|
|
@@ -261,11 +280,12 @@ function buildCanonicalClassNodeIndex(classNodes, scanContext) {
|
|
|
261
280
|
}
|
|
262
281
|
return index;
|
|
263
282
|
}
|
|
264
|
-
function
|
|
283
|
+
function collectSerializableTypeTargetsForBinding(hostSymbol, binding, scanContext, canonicalClassNodeBySymbol) {
|
|
265
284
|
const stats = {
|
|
266
285
|
edgeCount: 0,
|
|
267
286
|
missCount: 0,
|
|
268
287
|
symbols: new Set(),
|
|
288
|
+
targetClassIds: new Set(),
|
|
269
289
|
};
|
|
270
290
|
if (!scanContext)
|
|
271
291
|
return stats;
|
|
@@ -290,14 +310,7 @@ function linkSerializableTypeEdges(graph, componentNode, hostSymbol, binding, sc
|
|
|
290
310
|
stats.missCount += 1;
|
|
291
311
|
continue;
|
|
292
312
|
}
|
|
293
|
-
|
|
294
|
-
id: generateId('UNITY_SERIALIZED_TYPE_IN', `${serializableNode.id}->${componentNode.id}:${fieldName}`),
|
|
295
|
-
type: 'UNITY_SERIALIZED_TYPE_IN',
|
|
296
|
-
sourceId: serializableNode.id,
|
|
297
|
-
targetId: componentNode.id,
|
|
298
|
-
confidence: 1.0,
|
|
299
|
-
reason: JSON.stringify({ hostSymbol, fieldName, declaredType, sourceLayer }),
|
|
300
|
-
});
|
|
313
|
+
stats.targetClassIds.add(serializableNode.id);
|
|
301
314
|
stats.edgeCount += 1;
|
|
302
315
|
stats.symbols.add(declaredType);
|
|
303
316
|
}
|
|
@@ -317,6 +330,27 @@ function collectBindingFieldSources(binding) {
|
|
|
317
330
|
}
|
|
318
331
|
return fieldSources;
|
|
319
332
|
}
|
|
333
|
+
function collectResourceSummaryRows(bindings) {
|
|
334
|
+
const summaryByPath = new Map();
|
|
335
|
+
for (const binding of bindings) {
|
|
336
|
+
const resourcePath = normalizePath(binding.resourcePath);
|
|
337
|
+
const row = summaryByPath.get(resourcePath) || {
|
|
338
|
+
resourceType: binding.resourceType,
|
|
339
|
+
bindingKinds: new Set(),
|
|
340
|
+
lightweight: true,
|
|
341
|
+
};
|
|
342
|
+
row.resourceType = binding.resourceType || row.resourceType;
|
|
343
|
+
row.bindingKinds.add(binding.bindingKind);
|
|
344
|
+
row.lightweight = row.lightweight && Boolean(binding.lightweight);
|
|
345
|
+
summaryByPath.set(resourcePath, row);
|
|
346
|
+
}
|
|
347
|
+
return [...summaryByPath.entries()].map(([resourcePath, value]) => ({
|
|
348
|
+
resourcePath,
|
|
349
|
+
resourceType: value.resourceType,
|
|
350
|
+
bindingKinds: [...value.bindingKinds.values()].sort(),
|
|
351
|
+
lightweight: value.lightweight,
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
320
354
|
function roundMs(value) {
|
|
321
355
|
return Number(value.toFixed(1));
|
|
322
356
|
}
|
|
@@ -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
|
+
});
|