@veewo/gitnexus 1.3.10 → 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.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,12 @@
|
|
|
1
|
+
export function buildAnalyzeMemoryReport(input) {
|
|
2
|
+
return {
|
|
3
|
+
capturedAt: new Date().toISOString(),
|
|
4
|
+
summary: {
|
|
5
|
+
analyzeRealSec: input.analyze.realSec,
|
|
6
|
+
analyzeMaxRssBytes: input.analyze.maxRssBytes,
|
|
7
|
+
coldResourceBindings: input.queryCold.resourceBindings,
|
|
8
|
+
warmResourceBindings: input.queryWarm.resourceBindings,
|
|
9
|
+
},
|
|
10
|
+
input,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { buildAnalyzeMemoryReport } from './analyze-memory-sampler.js';
|
|
4
|
+
test('buildAnalyzeMemoryReport summarizes analyze and query measurements', () => {
|
|
5
|
+
const report = buildAnalyzeMemoryReport({
|
|
6
|
+
analyze: { realSec: 10, maxRssBytes: 1024, phases: { pipelineSec: 3, kuzuSec: 5, ftsSec: 1 } },
|
|
7
|
+
queryCold: { realSec: 2, maxRssBytes: 512, resourceBindings: 4, unityDiagnostics: [] },
|
|
8
|
+
queryWarm: { realSec: 1, maxRssBytes: 256, resourceBindings: 4, unityDiagnostics: [] },
|
|
9
|
+
});
|
|
10
|
+
assert.equal(report.summary.analyzeRealSec, 10);
|
|
11
|
+
assert.equal(report.summary.coldResourceBindings, 4);
|
|
12
|
+
});
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import test from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
4
7
|
import { loadBenchmarkDataset } from './io.js';
|
|
8
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const projectRoot = path.resolve(here, '../..');
|
|
5
10
|
test('loadBenchmarkDataset parses thresholds and jsonl rows', async () => {
|
|
6
|
-
const root = path.resolve('../benchmarks/unity-baseline/v1');
|
|
11
|
+
const root = path.resolve(projectRoot, '../benchmarks/unity-baseline/v1');
|
|
7
12
|
const ds = await loadBenchmarkDataset(root);
|
|
8
13
|
assert.equal(typeof ds.thresholds.query.precisionMin, 'number');
|
|
9
14
|
assert.ok(ds.symbols.length > 0);
|
|
@@ -11,11 +16,41 @@ test('loadBenchmarkDataset parses thresholds and jsonl rows', async () => {
|
|
|
11
16
|
assert.ok(ds.tasks.length > 0);
|
|
12
17
|
});
|
|
13
18
|
test('loadBenchmarkDataset rejects missing required fields', async () => {
|
|
14
|
-
const badRoot = path.
|
|
15
|
-
|
|
19
|
+
const badRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-bad-dataset-'));
|
|
20
|
+
try {
|
|
21
|
+
await fs.writeFile(path.join(badRoot, 'thresholds.json'), JSON.stringify({
|
|
22
|
+
query: { precisionMin: 0.7, recallMin: 0.7, avgLatencyMsMax: 200, p95LatencyMsMax: 400 },
|
|
23
|
+
context: { coverageMin: 0.7, latencyMsMax: 400 },
|
|
24
|
+
impact: { recallMin: 0.7, avgLatencyMsMax: 400 },
|
|
25
|
+
}), 'utf-8');
|
|
26
|
+
await fs.writeFile(path.join(badRoot, 'symbols.jsonl'), `${JSON.stringify({
|
|
27
|
+
symbol_uid: 'Class:Foo',
|
|
28
|
+
file_path: 'Assets/Foo.cs',
|
|
29
|
+
symbol_name: 'Foo',
|
|
30
|
+
symbol_type: 'Class',
|
|
31
|
+
start_line: 1,
|
|
32
|
+
end_line: 10,
|
|
33
|
+
})}\n`, 'utf-8');
|
|
34
|
+
await fs.writeFile(path.join(badRoot, 'relations.jsonl'), `${JSON.stringify({
|
|
35
|
+
src_uid: 'Class:Foo',
|
|
36
|
+
edge_type: 'CALLS',
|
|
37
|
+
dst_uid: 'Method:Bar',
|
|
38
|
+
must_exist: true,
|
|
39
|
+
})}\n`, 'utf-8');
|
|
40
|
+
await fs.writeFile(path.join(badRoot, 'tasks.jsonl'), `${JSON.stringify({
|
|
41
|
+
tool: 'query',
|
|
42
|
+
input: {},
|
|
43
|
+
must_hit_uids: [],
|
|
44
|
+
// intentionally omit must_not_hit_uids
|
|
45
|
+
})}\n`, 'utf-8');
|
|
46
|
+
await assert.rejects(() => loadBenchmarkDataset(badRoot), /missing required field/i);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await fs.rm(badRoot, { recursive: true, force: true });
|
|
50
|
+
}
|
|
16
51
|
});
|
|
17
52
|
test('loadBenchmarkDataset parses neonspark-v1 dataset', async () => {
|
|
18
|
-
const root = path.resolve('../benchmarks/unity-baseline/neonspark-v1');
|
|
53
|
+
const root = path.resolve(projectRoot, '../benchmarks/unity-baseline/neonspark-v1');
|
|
19
54
|
const ds = await loadBenchmarkDataset(root);
|
|
20
55
|
assert.equal(ds.symbols.length, 20);
|
|
21
56
|
assert.ok(ds.relations.length > 0);
|
|
@@ -24,7 +59,7 @@ test('loadBenchmarkDataset parses neonspark-v1 dataset', async () => {
|
|
|
24
59
|
assert.ok(ds.tasks.some((t) => t.tool === 'impact'));
|
|
25
60
|
});
|
|
26
61
|
test('loadBenchmarkDataset parses neonspark-v2 dataset', async () => {
|
|
27
|
-
const root = path.resolve('../benchmarks/unity-baseline/neonspark-v2');
|
|
62
|
+
const root = path.resolve(projectRoot, '../benchmarks/unity-baseline/neonspark-v2');
|
|
28
63
|
const ds = await loadBenchmarkDataset(root);
|
|
29
64
|
assert.ok(ds.symbols.length >= 40 && ds.symbols.length <= 60);
|
|
30
65
|
assert.ok(ds.relations.length > 0);
|
|
@@ -33,3 +68,11 @@ test('loadBenchmarkDataset parses neonspark-v2 dataset', async () => {
|
|
|
33
68
|
assert.ok(ds.tasks.some((t) => t.tool === 'context'));
|
|
34
69
|
assert.ok(ds.tasks.some((t) => t.tool === 'impact'));
|
|
35
70
|
});
|
|
71
|
+
test('latest unity hydration gate report includes hydrationMetaSummary schema', async () => {
|
|
72
|
+
const reportPath = path.resolve(projectRoot, 'docs/reports/2026-03-15-unity-hydration-gates.json');
|
|
73
|
+
const raw = await fs.readFile(reportPath, 'utf-8');
|
|
74
|
+
const report = JSON.parse(raw);
|
|
75
|
+
assert.ok(report.hydrationMetaSummary);
|
|
76
|
+
assert.equal(typeof report.hydrationMetaSummary.compactNeedsRetryRate, 'number');
|
|
77
|
+
assert.equal(typeof report.hydrationMetaSummary.parityCompleteRate, 'number');
|
|
78
|
+
});
|
|
@@ -17,6 +17,7 @@ export interface SymbolScenario {
|
|
|
17
17
|
kind: 'component' | 'scriptableobject' | 'serializable-class' | 'partial-component';
|
|
18
18
|
objectives: string[];
|
|
19
19
|
contextFileHint?: string;
|
|
20
|
+
contextUnityHydration?: 'compact' | 'parity';
|
|
20
21
|
deepDivePlan: Array<{
|
|
21
22
|
tool: 'query' | 'context' | 'impact' | 'cypher';
|
|
22
23
|
input: Record<string, unknown>;
|
|
@@ -53,12 +53,29 @@ function hasDeepDiveEvidence(output) {
|
|
|
53
53
|
const outgoingRefs = countRefs(output?.outgoing);
|
|
54
54
|
return processSymbols + definitions + candidates + rows + byDepth + impacted + incomingRefs + outgoingRefs > 0;
|
|
55
55
|
}
|
|
56
|
-
function assertScenario(scenario, contextOnOutput, deepDiveOutputs) {
|
|
56
|
+
function assertScenario(scenario, contextOnOutput, deepDiveOutputs, contextUnityHydration) {
|
|
57
57
|
const failures = [];
|
|
58
58
|
const bindings = Array.isArray(contextOnOutput?.resourceBindings) ? contextOnOutput.resourceBindings : [];
|
|
59
59
|
const hasBindings = bindings.length > 0;
|
|
60
60
|
const hasResolvedReferences = bindings.some((binding) => Array.isArray(binding?.resolvedReferences) && binding.resolvedReferences.length > 0);
|
|
61
61
|
const hasAssetTypeBinding = bindings.some((binding) => typeof binding?.resourceType === 'string' && binding.resourceType.length > 0);
|
|
62
|
+
const hydrationMeta = contextOnOutput?.hydrationMeta && typeof contextOnOutput.hydrationMeta === 'object'
|
|
63
|
+
? contextOnOutput.hydrationMeta
|
|
64
|
+
: null;
|
|
65
|
+
if (!hydrationMeta) {
|
|
66
|
+
failures.push(`${scenario.symbol}: context(on) must include hydrationMeta`);
|
|
67
|
+
}
|
|
68
|
+
else if (contextUnityHydration === 'compact') {
|
|
69
|
+
if (typeof hydrationMeta.needsParityRetry !== 'boolean') {
|
|
70
|
+
failures.push(`${scenario.symbol}: context(on) hydrationMeta.needsParityRetry must be boolean`);
|
|
71
|
+
}
|
|
72
|
+
if (hydrationMeta.isComplete === false && hydrationMeta.needsParityRetry !== true) {
|
|
73
|
+
failures.push(`${scenario.symbol}: context(on) incomplete compact response must set hydrationMeta.needsParityRetry=true`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (hydrationMeta.isComplete !== true) {
|
|
77
|
+
failures.push(`${scenario.symbol}: context(on) parity response must set hydrationMeta.isComplete=true`);
|
|
78
|
+
}
|
|
62
79
|
if (scenario.symbol === 'MainUIManager' || scenario.symbol === 'PlayerActor') {
|
|
63
80
|
if (!hasBindings) {
|
|
64
81
|
failures.push(`${scenario.symbol}: context(on) must include resourceBindings`);
|
|
@@ -142,7 +159,12 @@ export async function runSymbolScenario(runner, scenario, repo) {
|
|
|
142
159
|
const t0 = performance.now();
|
|
143
160
|
const contextOff = await runContextWithDisambiguation(runner, scenario, contextOffInput);
|
|
144
161
|
steps.push(buildMetric('context-off', 'context', performance.now() - t0, contextOffInput, contextOff));
|
|
145
|
-
const
|
|
162
|
+
const contextUnityHydration = scenario.contextUnityHydration === 'parity' ? 'parity' : 'compact';
|
|
163
|
+
const contextOnInput = {
|
|
164
|
+
...baseContextInput,
|
|
165
|
+
unity_resources: 'on',
|
|
166
|
+
unity_hydration_mode: contextUnityHydration,
|
|
167
|
+
};
|
|
146
168
|
const t1 = performance.now();
|
|
147
169
|
const contextOn = await runContextWithDisambiguation(runner, scenario, contextOnInput);
|
|
148
170
|
steps.push(buildMetric('context-on', 'context', performance.now() - t1, contextOnInput, contextOn));
|
|
@@ -161,6 +183,6 @@ export async function runSymbolScenario(runner, scenario, repo) {
|
|
|
161
183
|
return {
|
|
162
184
|
symbol: scenario.symbol,
|
|
163
185
|
steps,
|
|
164
|
-
assertions: assertScenario(scenario, contextOn, deepDiveOutputs),
|
|
186
|
+
assertions: assertScenario(scenario, contextOn, deepDiveOutputs, contextUnityHydration),
|
|
165
187
|
};
|
|
166
188
|
}
|
|
@@ -8,6 +8,12 @@ test('runSymbolScenario executes context off/on + deepDive and records metrics',
|
|
|
8
8
|
if (input.unity_resources === 'on') {
|
|
9
9
|
return {
|
|
10
10
|
status: 'found',
|
|
11
|
+
hydrationMeta: {
|
|
12
|
+
requestedMode: 'compact',
|
|
13
|
+
effectiveMode: 'compact',
|
|
14
|
+
isComplete: false,
|
|
15
|
+
needsParityRetry: true,
|
|
16
|
+
},
|
|
11
17
|
resourceBindings: [
|
|
12
18
|
{
|
|
13
19
|
resourcePath: 'Assets/Prefabs/UI.prefab',
|
|
@@ -36,7 +42,11 @@ test('runSymbolScenario executes context off/on + deepDive and records metrics',
|
|
|
36
42
|
});
|
|
37
43
|
test('AssetRef requires context(on) resourceBindings after serializable-class coverage', async () => {
|
|
38
44
|
const noEvidenceRunner = {
|
|
39
|
-
context: async () => ({
|
|
45
|
+
context: async () => ({
|
|
46
|
+
status: 'found',
|
|
47
|
+
hydrationMeta: { requestedMode: 'compact', effectiveMode: 'compact', isComplete: false, needsParityRetry: true },
|
|
48
|
+
resourceBindings: [],
|
|
49
|
+
}),
|
|
40
50
|
query: async () => ({ process_symbols: [] }),
|
|
41
51
|
impact: async () => ({ impactedCount: 0 }),
|
|
42
52
|
cypher: async () => ({ rows: [] }),
|
|
@@ -54,6 +64,7 @@ test('AssetRef requires deep-dive evidence even when context(on) has resourceBin
|
|
|
54
64
|
const noDeepDiveEvidenceRunner = {
|
|
55
65
|
context: async () => ({
|
|
56
66
|
status: 'found',
|
|
67
|
+
hydrationMeta: { requestedMode: 'compact', effectiveMode: 'compact', isComplete: false, needsParityRetry: true },
|
|
57
68
|
resourceBindings: [{ resourcePath: 'Assets/Data/Unlock.asset', resourceType: 'asset' }],
|
|
58
69
|
}),
|
|
59
70
|
query: async () => ({ process_symbols: [] }),
|
|
@@ -73,6 +84,7 @@ test('AssetRef passes when context(on) bindings and deep-dive evidence are both
|
|
|
73
84
|
const satisfiedRunner = {
|
|
74
85
|
context: async () => ({
|
|
75
86
|
status: 'found',
|
|
87
|
+
hydrationMeta: { requestedMode: 'compact', effectiveMode: 'compact', isComplete: false, needsParityRetry: true },
|
|
76
88
|
resourceBindings: [{ resourcePath: 'Assets/Data/Unlock.asset', resourceType: 'asset' }],
|
|
77
89
|
}),
|
|
78
90
|
query: async () => ({ process_symbols: [{ id: 'Class:Assets/Scripts/UnlockContent.cs:UnlockContent' }] }),
|
|
@@ -107,6 +119,12 @@ test('runSymbolScenario retries context with file hint when response is ambiguou
|
|
|
107
119
|
if (input.file_path === hint) {
|
|
108
120
|
return {
|
|
109
121
|
status: 'found',
|
|
122
|
+
hydrationMeta: {
|
|
123
|
+
requestedMode: 'compact',
|
|
124
|
+
effectiveMode: 'compact',
|
|
125
|
+
isComplete: false,
|
|
126
|
+
needsParityRetry: true,
|
|
127
|
+
},
|
|
110
128
|
resourceBindings: [
|
|
111
129
|
{
|
|
112
130
|
resourcePath: 'Assets/Prefabs/Player.prefab',
|
|
@@ -143,3 +161,28 @@ test('runSymbolScenario retries context with file hint when response is ambiguou
|
|
|
143
161
|
assert.equal(out.steps[1]?.output?.status, 'found');
|
|
144
162
|
assert.equal(out.assertions.pass, true);
|
|
145
163
|
});
|
|
164
|
+
test('runSymbolScenario fails when compact context hydrationMeta.needsParityRetry is missing', async () => {
|
|
165
|
+
const runner = {
|
|
166
|
+
context: async (input) => {
|
|
167
|
+
if (input.unity_resources === 'on') {
|
|
168
|
+
return {
|
|
169
|
+
status: 'found',
|
|
170
|
+
hydrationMeta: { requestedMode: 'compact', effectiveMode: 'compact', isComplete: false },
|
|
171
|
+
resourceBindings: [{ resourcePath: 'Assets/Prefabs/A.prefab', resourceType: 'prefab' }],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return { status: 'found' };
|
|
175
|
+
},
|
|
176
|
+
query: async () => ({ process_symbols: [{ id: 'Class:A' }] }),
|
|
177
|
+
impact: async () => ({ impactedCount: 1 }),
|
|
178
|
+
cypher: async () => ({ rows: [] }),
|
|
179
|
+
};
|
|
180
|
+
const out = await runSymbolScenario(runner, {
|
|
181
|
+
symbol: 'MainUIManager',
|
|
182
|
+
kind: 'component',
|
|
183
|
+
objectives: ['verify hydration contract'],
|
|
184
|
+
deepDivePlan: [{ tool: 'query', input: { query: 'MainUIManager' } }],
|
|
185
|
+
});
|
|
186
|
+
assert.equal(out.assertions.pass, false);
|
|
187
|
+
assert.ok(out.assertions.failures.some((f) => f.includes('hydrationMeta.needsParityRetry')));
|
|
188
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface UnityLazyContextMetrics {
|
|
2
|
+
coldMs: number;
|
|
3
|
+
warmMs: number;
|
|
4
|
+
coldMaxRssBytes: number;
|
|
5
|
+
warmMaxRssBytes: number;
|
|
6
|
+
}
|
|
7
|
+
export interface UnityLazyContextThresholds {
|
|
8
|
+
coldMsMax?: number;
|
|
9
|
+
warmMsMax?: number;
|
|
10
|
+
coldMaxRssBytesMax?: number;
|
|
11
|
+
warmMaxRssBytesMax?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface UnityLazyThresholdVerdict {
|
|
14
|
+
pass: boolean;
|
|
15
|
+
checks: Record<string, {
|
|
16
|
+
pass: boolean;
|
|
17
|
+
actual: number;
|
|
18
|
+
expected: number;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export interface UnityLazyContextSample {
|
|
22
|
+
durationMs: number;
|
|
23
|
+
maxRssBytes: number;
|
|
24
|
+
exitCode: number;
|
|
25
|
+
stdout: string;
|
|
26
|
+
stderr: string;
|
|
27
|
+
hydrationMeta?: {
|
|
28
|
+
requestedMode?: string;
|
|
29
|
+
effectiveMode?: string;
|
|
30
|
+
isComplete?: boolean;
|
|
31
|
+
needsParityRetry?: boolean;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export interface UnityHydrationMetaSummary {
|
|
35
|
+
compactSamples: number;
|
|
36
|
+
paritySamples: number;
|
|
37
|
+
compactNeedsRetryRate: number;
|
|
38
|
+
parityCompleteRate: number;
|
|
39
|
+
}
|
|
40
|
+
export interface UnityLazyContextSamplerConfig {
|
|
41
|
+
targetPath: string;
|
|
42
|
+
repo: string;
|
|
43
|
+
symbol: string;
|
|
44
|
+
file: string;
|
|
45
|
+
unityHydration?: 'compact' | 'parity';
|
|
46
|
+
thresholds?: UnityLazyContextThresholds;
|
|
47
|
+
}
|
|
48
|
+
export type UnityLazyContextRunner = (input: UnityLazyContextSamplerConfig & {
|
|
49
|
+
warm: boolean;
|
|
50
|
+
}) => Promise<UnityLazyContextSample>;
|
|
51
|
+
export declare function evaluateUnityLazyContextThresholds(metrics: UnityLazyContextMetrics, thresholds?: UnityLazyContextThresholds): UnityLazyThresholdVerdict;
|
|
52
|
+
export declare function runUnityLazyContextSampler(runner: UnityLazyContextRunner, config: UnityLazyContextSamplerConfig): Promise<{
|
|
53
|
+
capturedAt: string;
|
|
54
|
+
config: Omit<UnityLazyContextSamplerConfig, 'thresholds'>;
|
|
55
|
+
metrics: UnityLazyContextMetrics;
|
|
56
|
+
hydrationMetaSummary: UnityHydrationMetaSummary;
|
|
57
|
+
thresholdVerdict: UnityLazyThresholdVerdict;
|
|
58
|
+
}>;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
export function evaluateUnityLazyContextThresholds(metrics, thresholds) {
|
|
6
|
+
const verdict = { pass: true, checks: {} };
|
|
7
|
+
if (!thresholds) {
|
|
8
|
+
return verdict;
|
|
9
|
+
}
|
|
10
|
+
const checks = [
|
|
11
|
+
['coldMs', metrics.coldMs, thresholds.coldMsMax],
|
|
12
|
+
['warmMs', metrics.warmMs, thresholds.warmMsMax],
|
|
13
|
+
['coldMaxRssBytes', metrics.coldMaxRssBytes, thresholds.coldMaxRssBytesMax],
|
|
14
|
+
['warmMaxRssBytes', metrics.warmMaxRssBytes, thresholds.warmMaxRssBytesMax],
|
|
15
|
+
];
|
|
16
|
+
for (const [name, actual, expected] of checks) {
|
|
17
|
+
if (typeof expected !== 'number')
|
|
18
|
+
continue;
|
|
19
|
+
const pass = actual <= expected;
|
|
20
|
+
verdict.checks[name] = { pass, actual, expected };
|
|
21
|
+
if (!pass)
|
|
22
|
+
verdict.pass = false;
|
|
23
|
+
}
|
|
24
|
+
return verdict;
|
|
25
|
+
}
|
|
26
|
+
export async function runUnityLazyContextSampler(runner, config) {
|
|
27
|
+
const cold = await runner({ ...config, warm: false });
|
|
28
|
+
if (cold.exitCode !== 0) {
|
|
29
|
+
throw new Error(`Cold run failed: ${cold.stderr || cold.stdout}`);
|
|
30
|
+
}
|
|
31
|
+
const warm = await runner({ ...config, warm: true });
|
|
32
|
+
if (warm.exitCode !== 0) {
|
|
33
|
+
throw new Error(`Warm run failed: ${warm.stderr || warm.stdout}`);
|
|
34
|
+
}
|
|
35
|
+
const metrics = {
|
|
36
|
+
coldMs: round1(cold.durationMs),
|
|
37
|
+
warmMs: round1(warm.durationMs),
|
|
38
|
+
coldMaxRssBytes: cold.maxRssBytes,
|
|
39
|
+
warmMaxRssBytes: warm.maxRssBytes,
|
|
40
|
+
};
|
|
41
|
+
const hydrationMetaSummary = summarizeHydrationMeta([cold, warm]);
|
|
42
|
+
return {
|
|
43
|
+
capturedAt: new Date().toISOString(),
|
|
44
|
+
config: {
|
|
45
|
+
targetPath: config.targetPath,
|
|
46
|
+
repo: config.repo,
|
|
47
|
+
symbol: config.symbol,
|
|
48
|
+
file: config.file,
|
|
49
|
+
unityHydration: config.unityHydration || 'compact',
|
|
50
|
+
},
|
|
51
|
+
metrics,
|
|
52
|
+
hydrationMetaSummary,
|
|
53
|
+
thresholdVerdict: evaluateUnityLazyContextThresholds(metrics, config.thresholds),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function runCliContextSample(input) {
|
|
57
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
58
|
+
const thisDir = path.dirname(thisFile);
|
|
59
|
+
const cliPath = path.resolve(thisDir, '../cli/index.js');
|
|
60
|
+
const args = [
|
|
61
|
+
'-l',
|
|
62
|
+
'node',
|
|
63
|
+
cliPath,
|
|
64
|
+
'context',
|
|
65
|
+
input.symbol,
|
|
66
|
+
'--repo',
|
|
67
|
+
input.repo,
|
|
68
|
+
'--file',
|
|
69
|
+
input.file,
|
|
70
|
+
'--unity-resources',
|
|
71
|
+
'auto',
|
|
72
|
+
'--unity-hydration',
|
|
73
|
+
input.unityHydration || 'compact',
|
|
74
|
+
];
|
|
75
|
+
const startedAt = Date.now();
|
|
76
|
+
const proc = spawn('/usr/bin/time', args, { cwd: input.targetPath, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
77
|
+
let stdout = '';
|
|
78
|
+
let stderr = '';
|
|
79
|
+
proc.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
80
|
+
proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
81
|
+
const exitCode = await new Promise((resolve) => {
|
|
82
|
+
proc.on('close', (code) => resolve(code ?? 1));
|
|
83
|
+
});
|
|
84
|
+
const rssMatch = stderr.match(/maximum resident set size[^0-9]*([0-9]+)|([0-9]+)\s+maximum resident set size/i);
|
|
85
|
+
const maxRssBytes = rssMatch ? Number(rssMatch[1] || rssMatch[2] || 0) : 0;
|
|
86
|
+
const parsedPayload = extractFirstJsonObject(stdout) || extractFirstJsonObject(stderr);
|
|
87
|
+
const hydrationMeta = parsedPayload && typeof parsedPayload === 'object' && parsedPayload.hydrationMeta
|
|
88
|
+
? parsedPayload.hydrationMeta
|
|
89
|
+
: undefined;
|
|
90
|
+
return {
|
|
91
|
+
durationMs: Date.now() - startedAt,
|
|
92
|
+
maxRssBytes,
|
|
93
|
+
exitCode,
|
|
94
|
+
stdout,
|
|
95
|
+
stderr,
|
|
96
|
+
hydrationMeta,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function parseArgs(argv) {
|
|
100
|
+
const get = (name) => {
|
|
101
|
+
const index = argv.indexOf(name);
|
|
102
|
+
if (index === -1 || index + 1 >= argv.length)
|
|
103
|
+
return undefined;
|
|
104
|
+
return argv[index + 1];
|
|
105
|
+
};
|
|
106
|
+
const targetPath = get('--target-path');
|
|
107
|
+
const repo = get('--repo');
|
|
108
|
+
const symbol = get('--symbol');
|
|
109
|
+
const file = get('--file');
|
|
110
|
+
const unityHydrationRaw = String(get('--unity-hydration') || 'compact').trim().toLowerCase();
|
|
111
|
+
const unityHydration = unityHydrationRaw === 'parity' ? 'parity' : 'compact';
|
|
112
|
+
if (!targetPath)
|
|
113
|
+
throw new Error('Missing required arg: --target-path <path>');
|
|
114
|
+
if (!repo)
|
|
115
|
+
throw new Error('Missing required arg: --repo <repo>');
|
|
116
|
+
if (!symbol)
|
|
117
|
+
throw new Error('Missing required arg: --symbol <symbol>');
|
|
118
|
+
if (!file)
|
|
119
|
+
throw new Error('Missing required arg: --file <file>');
|
|
120
|
+
return {
|
|
121
|
+
targetPath: path.resolve(targetPath),
|
|
122
|
+
repo,
|
|
123
|
+
symbol,
|
|
124
|
+
file,
|
|
125
|
+
unityHydration,
|
|
126
|
+
thresholds: get('--thresholds') ? path.resolve(get('--thresholds')) : undefined,
|
|
127
|
+
report: get('--report') ? path.resolve(get('--report')) : undefined,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function round1(value) {
|
|
131
|
+
return Number(value.toFixed(1));
|
|
132
|
+
}
|
|
133
|
+
async function main() {
|
|
134
|
+
const args = parseArgs(process.argv.slice(2));
|
|
135
|
+
const thresholds = args.thresholds
|
|
136
|
+
? JSON.parse(await fs.readFile(args.thresholds, 'utf-8'))
|
|
137
|
+
: undefined;
|
|
138
|
+
const report = await runUnityLazyContextSampler(runCliContextSample, {
|
|
139
|
+
targetPath: args.targetPath,
|
|
140
|
+
repo: args.repo,
|
|
141
|
+
symbol: args.symbol,
|
|
142
|
+
file: args.file,
|
|
143
|
+
unityHydration: args.unityHydration,
|
|
144
|
+
thresholds,
|
|
145
|
+
});
|
|
146
|
+
const payload = JSON.stringify(report, null, 2);
|
|
147
|
+
if (args.report) {
|
|
148
|
+
await fs.mkdir(path.dirname(args.report), { recursive: true });
|
|
149
|
+
await fs.writeFile(args.report, payload, 'utf-8');
|
|
150
|
+
console.log(`[unity-lazy-context-sampler] report written: ${args.report}`);
|
|
151
|
+
}
|
|
152
|
+
console.log(payload);
|
|
153
|
+
if (!report.thresholdVerdict.pass) {
|
|
154
|
+
process.exitCode = 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const modulePath = process.argv[1] ? path.resolve(process.argv[1]) : '';
|
|
158
|
+
if (import.meta.url === `file://${modulePath}`) {
|
|
159
|
+
main().catch((error) => {
|
|
160
|
+
console.error(`[unity-lazy-context-sampler] failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
161
|
+
process.exitCode = 1;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function summarizeHydrationMeta(samples) {
|
|
165
|
+
let compactSamples = 0;
|
|
166
|
+
let compactNeedsRetry = 0;
|
|
167
|
+
let paritySamples = 0;
|
|
168
|
+
let parityComplete = 0;
|
|
169
|
+
for (const sample of samples) {
|
|
170
|
+
const mode = String(sample.hydrationMeta?.effectiveMode || '').toLowerCase();
|
|
171
|
+
if (mode === 'parity') {
|
|
172
|
+
paritySamples += 1;
|
|
173
|
+
if (sample.hydrationMeta?.isComplete === true) {
|
|
174
|
+
parityComplete += 1;
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (mode === 'compact') {
|
|
179
|
+
compactSamples += 1;
|
|
180
|
+
if (sample.hydrationMeta?.needsParityRetry === true) {
|
|
181
|
+
compactNeedsRetry += 1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
compactSamples,
|
|
187
|
+
paritySamples,
|
|
188
|
+
compactNeedsRetryRate: compactSamples > 0 ? round1(compactNeedsRetry / compactSamples) : 0,
|
|
189
|
+
parityCompleteRate: paritySamples > 0 ? round1(parityComplete / paritySamples) : 0,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function extractFirstJsonObject(text) {
|
|
193
|
+
if (!text)
|
|
194
|
+
return null;
|
|
195
|
+
const start = text.indexOf('{');
|
|
196
|
+
if (start < 0)
|
|
197
|
+
return null;
|
|
198
|
+
let depth = 0;
|
|
199
|
+
for (let i = start; i < text.length; i += 1) {
|
|
200
|
+
const char = text[i];
|
|
201
|
+
if (char === '{')
|
|
202
|
+
depth += 1;
|
|
203
|
+
if (char === '}') {
|
|
204
|
+
depth -= 1;
|
|
205
|
+
if (depth === 0) {
|
|
206
|
+
const candidate = text.slice(start, i + 1);
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(candidate);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { runUnityLazyContextSampler } from './unity-lazy-context-sampler.js';
|
|
4
|
+
test('sampler emits cold/warm latency and rss metrics with threshold verdict', async () => {
|
|
5
|
+
const fakeRunner = async ({ warm }) => ({
|
|
6
|
+
durationMs: warm ? 420 : 6200,
|
|
7
|
+
maxRssBytes: warm ? 650 * 1024 * 1024 : 1700 * 1024 * 1024,
|
|
8
|
+
exitCode: 0,
|
|
9
|
+
stdout: '',
|
|
10
|
+
stderr: '',
|
|
11
|
+
hydrationMeta: warm
|
|
12
|
+
? { requestedMode: 'compact', effectiveMode: 'parity', isComplete: true, needsParityRetry: false }
|
|
13
|
+
: { requestedMode: 'compact', effectiveMode: 'compact', isComplete: false, needsParityRetry: true },
|
|
14
|
+
});
|
|
15
|
+
const report = await runUnityLazyContextSampler(fakeRunner, {
|
|
16
|
+
targetPath: '/tmp/repo',
|
|
17
|
+
repo: 'neonnew-core',
|
|
18
|
+
symbol: 'DoorObj',
|
|
19
|
+
file: 'Assets/NEON/Code/Game/Doors/DoorObj.cs',
|
|
20
|
+
thresholds: {
|
|
21
|
+
coldMsMax: 7000,
|
|
22
|
+
warmMsMax: 1000,
|
|
23
|
+
coldMaxRssBytesMax: 2 * 1024 * 1024 * 1024,
|
|
24
|
+
warmMaxRssBytesMax: 1 * 1024 * 1024 * 1024,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
assert.ok(report.metrics.coldMs > 0);
|
|
28
|
+
assert.equal(typeof report.hydrationMetaSummary.compactNeedsRetryRate, 'number');
|
|
29
|
+
assert.equal(typeof report.hydrationMetaSummary.parityCompleteRate, 'number');
|
|
30
|
+
assert.ok(typeof report.thresholdVerdict.pass === 'boolean');
|
|
31
|
+
assert.equal(report.thresholdVerdict.pass, true);
|
|
32
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kuzu native close may segfault on some macOS environments.
|
|
3
|
+
* We skip explicit close on analyze exit there, unless force-enabled.
|
|
4
|
+
*/
|
|
5
|
+
export declare function shouldCloseKuzuOnAnalyzeExit(platform?: NodeJS.Platform, forceCloseOnExit?: string | undefined): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kuzu native close may segfault on some macOS environments.
|
|
3
|
+
* We skip explicit close on analyze exit there, unless force-enabled.
|
|
4
|
+
*/
|
|
5
|
+
export function shouldCloseKuzuOnAnalyzeExit(platform = process.platform, forceCloseOnExit = process.env.GITNEXUS_FORCE_KUZU_CLOSE_ON_EXIT) {
|
|
6
|
+
if (forceCloseOnExit === '1')
|
|
7
|
+
return true;
|
|
8
|
+
return platform !== 'darwin';
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { shouldCloseKuzuOnAnalyzeExit } from './analyze-close-policy.js';
|
|
4
|
+
test('shouldCloseKuzuOnAnalyzeExit skips close on darwin by default', () => {
|
|
5
|
+
assert.equal(shouldCloseKuzuOnAnalyzeExit('darwin', undefined), false);
|
|
6
|
+
});
|
|
7
|
+
test('shouldCloseKuzuOnAnalyzeExit closes on non-darwin platforms', () => {
|
|
8
|
+
assert.equal(shouldCloseKuzuOnAnalyzeExit('linux', undefined), true);
|
|
9
|
+
});
|
|
10
|
+
test('shouldCloseKuzuOnAnalyzeExit can be force-enabled on darwin', () => {
|
|
11
|
+
assert.equal(shouldCloseKuzuOnAnalyzeExit('darwin', '1'), true);
|
|
12
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function toPipelineRuntimeSummary(input) {
|
|
2
|
+
return {
|
|
3
|
+
totalFileCount: input.totalFileCount,
|
|
4
|
+
communityResult: input.communityResult,
|
|
5
|
+
processResult: input.processResult,
|
|
6
|
+
unityResult: input.unityResult,
|
|
7
|
+
scopeDiagnostics: input.scopeDiagnostics,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { toPipelineRuntimeSummary } from './analyze-runtime-summary.js';
|
|
4
|
+
test('toPipelineRuntimeSummary drops graph reference and preserves reporting fields', () => {
|
|
5
|
+
const out = toPipelineRuntimeSummary({
|
|
6
|
+
totalFileCount: 12,
|
|
7
|
+
communityResult: { stats: { totalCommunities: 3 } },
|
|
8
|
+
processResult: { stats: { totalProcesses: 2 } },
|
|
9
|
+
unityResult: { diagnostics: ['scanContext: scripts=1'] },
|
|
10
|
+
});
|
|
11
|
+
assert.equal('graph' in out, false);
|
|
12
|
+
assert.equal(out.totalFileCount, 12);
|
|
13
|
+
assert.equal(out.communityResult?.stats.totalCommunities, 3);
|
|
14
|
+
});
|