@veewo/gitnexus 1.3.8 → 1.3.10
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 +5 -0
- package/dist/benchmark/runner.test.js +1 -1
- package/dist/benchmark/u2-e2e/analyze-parser.d.ts +22 -0
- package/dist/benchmark/u2-e2e/analyze-parser.js +89 -0
- package/dist/benchmark/u2-e2e/analyze-parser.test.js +13 -0
- package/dist/benchmark/u2-e2e/characterlist-assetref.d.ts +19 -0
- package/dist/benchmark/u2-e2e/characterlist-assetref.js +80 -0
- package/dist/benchmark/u2-e2e/characterlist-assetref.test.js +108 -0
- package/dist/benchmark/u2-e2e/config.d.ts +25 -0
- package/dist/benchmark/u2-e2e/config.js +86 -0
- package/dist/benchmark/u2-e2e/config.test.js +29 -0
- package/dist/benchmark/u2-e2e/metrics.d.ts +20 -0
- package/dist/benchmark/u2-e2e/metrics.js +34 -0
- package/dist/benchmark/u2-e2e/metrics.test.js +13 -0
- package/dist/benchmark/u2-e2e/neonspark-full-e2e.d.ts +33 -0
- package/dist/benchmark/u2-e2e/neonspark-full-e2e.js +439 -0
- package/dist/benchmark/u2-e2e/neonspark-full-e2e.test.js +40 -0
- package/dist/benchmark/u2-e2e/report.d.ts +58 -0
- package/dist/benchmark/u2-e2e/report.js +130 -0
- package/dist/benchmark/u2-e2e/report.test.js +58 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.d.ts +21 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.js +166 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.test.d.ts +1 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.test.js +145 -0
- package/dist/benchmark/u2-performance-sampler.d.ts +33 -0
- package/dist/benchmark/u2-performance-sampler.js +178 -0
- package/dist/benchmark/u2-performance-sampler.test.d.ts +1 -0
- package/dist/benchmark/u2-performance-sampler.test.js +34 -0
- package/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze-multi-scope-regression.test.js +10 -0
- package/dist/cli/analyze-options.d.ts +19 -0
- package/dist/cli/analyze-options.js +35 -0
- package/dist/cli/analyze-options.test.js +42 -1
- package/dist/cli/analyze-summary.d.ts +7 -0
- package/dist/cli/analyze-summary.js +37 -0
- package/dist/cli/analyze-summary.test.d.ts +1 -0
- package/dist/cli/analyze-summary.test.js +58 -0
- package/dist/cli/analyze.d.ts +1 -0
- package/dist/cli/analyze.js +64 -32
- package/dist/cli/benchmark-u2-e2e.d.ts +9 -0
- package/dist/cli/benchmark-u2-e2e.js +35 -0
- package/dist/cli/benchmark-u2-e2e.test.d.ts +1 -0
- package/dist/cli/benchmark-u2-e2e.test.js +7 -0
- package/dist/cli/index.js +21 -0
- package/dist/cli/repo-manager-alias.test.js +24 -1
- package/dist/cli/tool.d.ts +3 -0
- package/dist/cli/tool.js +2 -0
- package/dist/cli/unity-bindings.d.ts +8 -0
- package/dist/cli/unity-bindings.js +33 -0
- package/dist/cli/unity-bindings.test.d.ts +1 -0
- package/dist/cli/unity-bindings.test.js +24 -0
- package/dist/core/graph/types.d.ts +1 -1
- package/dist/core/ingestion/pipeline.d.ts +2 -4
- package/dist/core/ingestion/pipeline.js +12 -0
- package/dist/core/ingestion/unity-resource-processor.d.ts +26 -0
- package/dist/core/ingestion/unity-resource-processor.js +363 -0
- package/dist/core/ingestion/unity-resource-processor.test.d.ts +1 -0
- package/dist/core/ingestion/unity-resource-processor.test.js +599 -0
- package/dist/core/kuzu/kuzu-adapter.d.ts +6 -0
- package/dist/core/kuzu/kuzu-adapter.js +18 -7
- package/dist/core/kuzu/schema.d.ts +2 -2
- package/dist/core/kuzu/schema.js +22 -1
- package/dist/core/kuzu/schema.test.d.ts +1 -0
- package/dist/core/kuzu/schema.test.js +17 -0
- package/dist/core/unity/meta-index.d.ts +5 -0
- package/dist/core/unity/meta-index.js +113 -0
- package/dist/core/unity/meta-index.test.d.ts +1 -0
- package/dist/core/unity/meta-index.test.js +11 -0
- package/dist/core/unity/options.d.ts +2 -0
- package/dist/core/unity/options.js +9 -0
- package/dist/core/unity/options.test.d.ts +1 -0
- package/dist/core/unity/options.test.js +10 -0
- package/dist/core/unity/override-merger.d.ts +27 -0
- package/dist/core/unity/override-merger.js +35 -0
- package/dist/core/unity/override-merger.test.d.ts +1 -0
- package/dist/core/unity/override-merger.test.js +47 -0
- package/dist/core/unity/resolver.d.ts +79 -0
- package/dist/core/unity/resolver.js +384 -0
- package/dist/core/unity/resolver.test.d.ts +1 -0
- package/dist/core/unity/resolver.test.js +244 -0
- package/dist/core/unity/resource-hit-scanner.d.ts +10 -0
- package/dist/core/unity/resource-hit-scanner.js +60 -0
- package/dist/core/unity/resource-hit-scanner.test.d.ts +1 -0
- package/dist/core/unity/resource-hit-scanner.test.js +20 -0
- package/dist/core/unity/scan-context.d.ts +23 -0
- package/dist/core/unity/scan-context.js +318 -0
- package/dist/core/unity/scan-context.test.d.ts +1 -0
- package/dist/core/unity/scan-context.test.js +118 -0
- package/dist/core/unity/serialized-type-index.d.ts +10 -0
- package/dist/core/unity/serialized-type-index.js +105 -0
- package/dist/core/unity/serialized-type-index.test.d.ts +1 -0
- package/dist/core/unity/serialized-type-index.test.js +34 -0
- package/dist/core/unity/u2-thresholds.test.d.ts +1 -0
- package/dist/core/unity/u2-thresholds.test.js +71 -0
- package/dist/core/unity/yaml-object-graph.d.ts +9 -0
- package/dist/core/unity/yaml-object-graph.js +92 -0
- package/dist/core/unity/yaml-object-graph.test.d.ts +1 -0
- package/dist/core/unity/yaml-object-graph.test.js +49 -0
- package/dist/mcp/local/local-backend.js +12 -1
- package/dist/mcp/local/unity-enrichment.d.ts +6 -0
- package/dist/mcp/local/unity-enrichment.js +91 -0
- package/dist/mcp/local/unity-enrichment.test.d.ts +1 -0
- package/dist/mcp/local/unity-enrichment.test.js +130 -0
- package/dist/mcp/resources.js +1 -1
- package/dist/mcp/staleness.js +1 -1
- package/dist/mcp/tools.js +12 -0
- package/dist/storage/repo-manager.d.ts +6 -0
- package/dist/types/pipeline.d.ts +7 -0
- package/dist/types/pipeline.js +2 -0
- package/hooks/check-release-path-hygiene.mjs +108 -0
- package/package.json +16 -9
- package/dist/cli/analyze-custom-modules-regression.test.js +0 -75
- package/dist/cli/analyze-modules-diagnostics.test.js +0 -36
- package/dist/core/ingestion/modules/assignment-engine.d.ts +0 -33
- package/dist/core/ingestion/modules/assignment-engine.js +0 -179
- package/dist/core/ingestion/modules/assignment-engine.test.js +0 -111
- package/dist/core/ingestion/modules/config-loader.d.ts +0 -2
- package/dist/core/ingestion/modules/config-loader.js +0 -186
- package/dist/core/ingestion/modules/config-loader.test.js +0 -57
- package/dist/core/ingestion/modules/rule-matcher.d.ts +0 -12
- package/dist/core/ingestion/modules/rule-matcher.js +0 -63
- package/dist/core/ingestion/modules/rule-matcher.test.js +0 -58
- package/dist/core/ingestion/modules/types.d.ts +0 -44
- package/dist/core/ingestion/modules/types.js +0 -2
- package/dist/mcp/local/cluster-aggregation.d.ts +0 -20
- package/dist/mcp/local/cluster-aggregation.js +0 -48
- package/dist/mcp/local/cluster-aggregation.test.js +0 -22
- /package/dist/{cli/analyze-custom-modules-regression.test.d.ts → benchmark/u2-e2e/analyze-parser.test.d.ts} +0 -0
- /package/dist/{cli/analyze-modules-diagnostics.test.d.ts → benchmark/u2-e2e/characterlist-assetref.test.d.ts} +0 -0
- /package/dist/{core/ingestion/modules/assignment-engine.test.d.ts → benchmark/u2-e2e/config.test.d.ts} +0 -0
- /package/dist/{core/ingestion/modules/config-loader.test.d.ts → benchmark/u2-e2e/metrics.test.d.ts} +0 -0
- /package/dist/{core/ingestion/modules/rule-matcher.test.d.ts → benchmark/u2-e2e/neonspark-full-e2e.test.d.ts} +0 -0
- /package/dist/{mcp/local/cluster-aggregation.test.d.ts → benchmark/u2-e2e/report.test.d.ts} +0 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { buildFinalVerdictMarkdown } from './report.js';
|
|
4
|
+
test('buildFinalVerdictMarkdown includes estimate comparison and symbol outcomes', () => {
|
|
5
|
+
const md = buildFinalVerdictMarkdown({
|
|
6
|
+
runId: 'test-run',
|
|
7
|
+
buildTimings: { buildMs: 1200, pipelineProfileMs: 2500, analyzeSec: 114.8 },
|
|
8
|
+
estimateComparison: { status: 'in-range', inRange: true, actualSec: 500, lower: 322.6, upper: 540.1, deltaSec: 0 },
|
|
9
|
+
retrievalSummary: {
|
|
10
|
+
symbols: [
|
|
11
|
+
{ symbol: 'MainUIManager', pass: true, stepCount: 3 },
|
|
12
|
+
{ symbol: 'CoinPowerUp', pass: true, stepCount: 3 },
|
|
13
|
+
],
|
|
14
|
+
tokenSummary: { totalTokensEst: 3456, totalDurationMs: 876 },
|
|
15
|
+
failures: [],
|
|
16
|
+
},
|
|
17
|
+
failures: [],
|
|
18
|
+
});
|
|
19
|
+
assert.match(md, /Estimate Comparison/);
|
|
20
|
+
assert.match(md, /MainUIManager/);
|
|
21
|
+
assert.match(md, /CoinPowerUp/);
|
|
22
|
+
});
|
|
23
|
+
test('buildFinalVerdictMarkdown deduplicates repeated failures and renders serialized edge count', () => {
|
|
24
|
+
const md = buildFinalVerdictMarkdown({
|
|
25
|
+
runId: 'test-run',
|
|
26
|
+
retrievalSummary: {
|
|
27
|
+
symbols: [{ symbol: 'AssetRef', pass: true, stepCount: 4 }],
|
|
28
|
+
tokenSummary: { totalTokensEst: 100, totalDurationMs: 12.3 },
|
|
29
|
+
serializedTypeEdgeCount: 12,
|
|
30
|
+
failures: ['duration.min=1.1ms median=2.2ms max=3.3ms'],
|
|
31
|
+
},
|
|
32
|
+
failures: ['duration.min=1.1ms median=2.2ms max=3.3ms'],
|
|
33
|
+
});
|
|
34
|
+
const duplicateMatches = md.match(/duration\.min=1\.1ms median=2\.2ms max=3\.3ms/g) || [];
|
|
35
|
+
assert.equal(duplicateMatches.length, 1);
|
|
36
|
+
assert.match(md, /UNITY_SERIALIZED_TYPE_IN Edges: 12/);
|
|
37
|
+
});
|
|
38
|
+
test('buildFinalVerdictMarkdown renders CharacterList AssetRef sprite summary when provided', () => {
|
|
39
|
+
const md = buildFinalVerdictMarkdown({
|
|
40
|
+
runId: 'test-run',
|
|
41
|
+
retrievalSummary: {
|
|
42
|
+
symbols: [{ symbol: 'AssetRef', pass: true, stepCount: 4 }],
|
|
43
|
+
tokenSummary: { totalTokensEst: 100, totalDurationMs: 12.3 },
|
|
44
|
+
serializedTypeEdgeCount: 12,
|
|
45
|
+
characterListAssetRefSprite: {
|
|
46
|
+
extractedAssetRefInstances: 127,
|
|
47
|
+
nonEmptyAssetRefInstances: 123,
|
|
48
|
+
spriteAssetRefInstances: 63,
|
|
49
|
+
spriteRatioInNonEmpty: 0.5122,
|
|
50
|
+
uniqueSpriteAssets: 54,
|
|
51
|
+
},
|
|
52
|
+
failures: [],
|
|
53
|
+
},
|
|
54
|
+
failures: [],
|
|
55
|
+
});
|
|
56
|
+
assert.match(md, /CharacterList AssetRef Sprite Instances: 63/);
|
|
57
|
+
assert.match(md, /CharacterList AssetRef Sprite Ratio: 51.22%/);
|
|
58
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SymbolScenario } from './config.js';
|
|
2
|
+
import { type StepMetric } from './metrics.js';
|
|
3
|
+
export interface ToolRunner {
|
|
4
|
+
query: (params: Record<string, unknown>) => Promise<any>;
|
|
5
|
+
context: (params: Record<string, unknown>) => Promise<any>;
|
|
6
|
+
impact: (params: Record<string, unknown>) => Promise<any>;
|
|
7
|
+
cypher: (params: Record<string, unknown>) => Promise<any>;
|
|
8
|
+
}
|
|
9
|
+
export interface RetrievalStepResult extends StepMetric {
|
|
10
|
+
input: Record<string, unknown>;
|
|
11
|
+
output: any;
|
|
12
|
+
}
|
|
13
|
+
export interface SymbolScenarioResult {
|
|
14
|
+
symbol: string;
|
|
15
|
+
steps: RetrievalStepResult[];
|
|
16
|
+
assertions: {
|
|
17
|
+
pass: boolean;
|
|
18
|
+
failures: string[];
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare function runSymbolScenario(runner: ToolRunner, scenario: SymbolScenario, repo?: string): Promise<SymbolScenarioResult>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { estimateTokens } from './metrics.js';
|
|
4
|
+
function stringify(value) {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.stringify(value ?? null);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return String(value);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function buildMetric(stepId, tool, durationMs, input, output) {
|
|
13
|
+
const inputText = stringify(input);
|
|
14
|
+
const outputText = stringify(output);
|
|
15
|
+
const inputChars = inputText.length;
|
|
16
|
+
const outputChars = outputText.length;
|
|
17
|
+
const inputTokensEst = estimateTokens(inputText);
|
|
18
|
+
const outputTokensEst = estimateTokens(outputText);
|
|
19
|
+
return {
|
|
20
|
+
stepId,
|
|
21
|
+
tool,
|
|
22
|
+
durationMs: Number(durationMs.toFixed(1)),
|
|
23
|
+
inputChars,
|
|
24
|
+
outputChars,
|
|
25
|
+
inputTokensEst,
|
|
26
|
+
outputTokensEst,
|
|
27
|
+
totalTokensEst: inputTokensEst + outputTokensEst,
|
|
28
|
+
input,
|
|
29
|
+
output,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function countRefs(value) {
|
|
33
|
+
if (!value)
|
|
34
|
+
return 0;
|
|
35
|
+
let total = 0;
|
|
36
|
+
for (const rows of Object.values(value)) {
|
|
37
|
+
if (Array.isArray(rows)) {
|
|
38
|
+
total += rows.length;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return total;
|
|
42
|
+
}
|
|
43
|
+
function hasDeepDiveEvidence(output) {
|
|
44
|
+
const processSymbols = Array.isArray(output?.process_symbols) ? output.process_symbols.length : 0;
|
|
45
|
+
const definitions = Array.isArray(output?.definitions) ? output.definitions.length : 0;
|
|
46
|
+
const candidates = Array.isArray(output?.candidates) ? output.candidates.length : 0;
|
|
47
|
+
const rows = Array.isArray(output?.rows) ? output.rows.length : 0;
|
|
48
|
+
const byDepth = output?.byDepth && typeof output.byDepth === 'object'
|
|
49
|
+
? Object.values(output.byDepth).reduce((sum, value) => sum + (Array.isArray(value) ? value.length : 0), 0)
|
|
50
|
+
: 0;
|
|
51
|
+
const impacted = Number(output?.impactedCount || 0);
|
|
52
|
+
const incomingRefs = countRefs(output?.incoming);
|
|
53
|
+
const outgoingRefs = countRefs(output?.outgoing);
|
|
54
|
+
return processSymbols + definitions + candidates + rows + byDepth + impacted + incomingRefs + outgoingRefs > 0;
|
|
55
|
+
}
|
|
56
|
+
function assertScenario(scenario, contextOnOutput, deepDiveOutputs) {
|
|
57
|
+
const failures = [];
|
|
58
|
+
const bindings = Array.isArray(contextOnOutput?.resourceBindings) ? contextOnOutput.resourceBindings : [];
|
|
59
|
+
const hasBindings = bindings.length > 0;
|
|
60
|
+
const hasResolvedReferences = bindings.some((binding) => Array.isArray(binding?.resolvedReferences) && binding.resolvedReferences.length > 0);
|
|
61
|
+
const hasAssetTypeBinding = bindings.some((binding) => typeof binding?.resourceType === 'string' && binding.resourceType.length > 0);
|
|
62
|
+
if (scenario.symbol === 'MainUIManager' || scenario.symbol === 'PlayerActor') {
|
|
63
|
+
if (!hasBindings) {
|
|
64
|
+
failures.push(`${scenario.symbol}: context(on) must include resourceBindings`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (scenario.symbol === 'CoinPowerUp' || scenario.symbol === 'GlobalDataAssets') {
|
|
68
|
+
if (!hasAssetTypeBinding && !hasResolvedReferences) {
|
|
69
|
+
failures.push(`${scenario.symbol}: require asset-type binding or resolved references evidence`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (scenario.symbol === 'AssetRef') {
|
|
73
|
+
if (!hasBindings) {
|
|
74
|
+
failures.push('AssetRef: context(on) must include resourceBindings');
|
|
75
|
+
}
|
|
76
|
+
const deepDiveEvidence = deepDiveOutputs.some((output) => hasDeepDiveEvidence(output));
|
|
77
|
+
if (!deepDiveEvidence) {
|
|
78
|
+
failures.push('AssetRef: deep-dive must provide usage/dependency evidence');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
pass: failures.length === 0,
|
|
83
|
+
failures,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function invokeTool(runner, tool, input) {
|
|
87
|
+
if (tool === 'query') {
|
|
88
|
+
return runner.query(input);
|
|
89
|
+
}
|
|
90
|
+
if (tool === 'context') {
|
|
91
|
+
return runner.context(input);
|
|
92
|
+
}
|
|
93
|
+
if (tool === 'impact') {
|
|
94
|
+
return runner.impact(input);
|
|
95
|
+
}
|
|
96
|
+
return runner.cypher(input);
|
|
97
|
+
}
|
|
98
|
+
function selectDisambiguationUid(symbol, output) {
|
|
99
|
+
const expectedFile = `${symbol}.cs`.toLowerCase();
|
|
100
|
+
const candidates = Array.isArray(output?.candidates) ? output.candidates : [];
|
|
101
|
+
for (const candidate of candidates) {
|
|
102
|
+
const kind = String(candidate?.kind || '').toLowerCase();
|
|
103
|
+
if (kind !== 'class')
|
|
104
|
+
continue;
|
|
105
|
+
const filePath = String(candidate?.filePath || candidate?.file_path || '').trim();
|
|
106
|
+
if (!filePath)
|
|
107
|
+
continue;
|
|
108
|
+
if (path.basename(filePath).toLowerCase() !== expectedFile)
|
|
109
|
+
continue;
|
|
110
|
+
const uid = String(candidate?.uid || '').trim();
|
|
111
|
+
if (uid) {
|
|
112
|
+
return uid;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
async function runContextWithDisambiguation(runner, scenario, input) {
|
|
118
|
+
const first = await runner.context(input);
|
|
119
|
+
if (first?.status !== 'ambiguous') {
|
|
120
|
+
return first;
|
|
121
|
+
}
|
|
122
|
+
const hint = typeof scenario.contextFileHint === 'string' ? scenario.contextFileHint.trim() : '';
|
|
123
|
+
if (hint) {
|
|
124
|
+
const hinted = await runner.context({ ...input, file_path: hint });
|
|
125
|
+
if (hinted?.status !== 'ambiguous') {
|
|
126
|
+
return hinted;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const uid = selectDisambiguationUid(scenario.symbol, first);
|
|
130
|
+
if (uid) {
|
|
131
|
+
return runner.context({ ...input, uid });
|
|
132
|
+
}
|
|
133
|
+
return first;
|
|
134
|
+
}
|
|
135
|
+
export async function runSymbolScenario(runner, scenario, repo) {
|
|
136
|
+
const steps = [];
|
|
137
|
+
const baseContextInput = { name: scenario.symbol };
|
|
138
|
+
if (repo) {
|
|
139
|
+
baseContextInput.repo = repo;
|
|
140
|
+
}
|
|
141
|
+
const contextOffInput = { ...baseContextInput, unity_resources: 'off' };
|
|
142
|
+
const t0 = performance.now();
|
|
143
|
+
const contextOff = await runContextWithDisambiguation(runner, scenario, contextOffInput);
|
|
144
|
+
steps.push(buildMetric('context-off', 'context', performance.now() - t0, contextOffInput, contextOff));
|
|
145
|
+
const contextOnInput = { ...baseContextInput, unity_resources: 'on' };
|
|
146
|
+
const t1 = performance.now();
|
|
147
|
+
const contextOn = await runContextWithDisambiguation(runner, scenario, contextOnInput);
|
|
148
|
+
steps.push(buildMetric('context-on', 'context', performance.now() - t1, contextOnInput, contextOn));
|
|
149
|
+
const deepDiveOutputs = [];
|
|
150
|
+
for (let i = 0; i < scenario.deepDivePlan.length; i += 1) {
|
|
151
|
+
const step = scenario.deepDivePlan[i];
|
|
152
|
+
const input = { ...(step.input || {}) };
|
|
153
|
+
if (repo) {
|
|
154
|
+
input.repo = repo;
|
|
155
|
+
}
|
|
156
|
+
const ts = performance.now();
|
|
157
|
+
const output = await invokeTool(runner, step.tool, input);
|
|
158
|
+
deepDiveOutputs.push(output);
|
|
159
|
+
steps.push(buildMetric(`deep-dive-${i + 1}`, step.tool, performance.now() - ts, input, output));
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
symbol: scenario.symbol,
|
|
163
|
+
steps,
|
|
164
|
+
assertions: assertScenario(scenario, contextOn, deepDiveOutputs),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { runSymbolScenario } from './retrieval-runner.js';
|
|
4
|
+
import { loadE2EConfig } from './config.js';
|
|
5
|
+
test('runSymbolScenario executes context off/on + deepDive and records metrics', async () => {
|
|
6
|
+
const mockToolRunner = {
|
|
7
|
+
context: async (input) => {
|
|
8
|
+
if (input.unity_resources === 'on') {
|
|
9
|
+
return {
|
|
10
|
+
status: 'found',
|
|
11
|
+
resourceBindings: [
|
|
12
|
+
{
|
|
13
|
+
resourcePath: 'Assets/Prefabs/UI.prefab',
|
|
14
|
+
resourceType: 'prefab',
|
|
15
|
+
resolvedReferences: [{ uid: 'Class:Foo' }],
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return { status: 'found' };
|
|
21
|
+
},
|
|
22
|
+
query: async () => ({ process_symbols: [{ id: 'Class:MainUIManager' }] }),
|
|
23
|
+
impact: async () => ({ impactedCount: 1 }),
|
|
24
|
+
cypher: async () => ({ rows: [] }),
|
|
25
|
+
};
|
|
26
|
+
const out = await runSymbolScenario(mockToolRunner, {
|
|
27
|
+
symbol: 'MainUIManager',
|
|
28
|
+
kind: 'component',
|
|
29
|
+
objectives: ['verify context'],
|
|
30
|
+
deepDivePlan: [{ tool: 'query', input: { query: 'MainUIManager' } }],
|
|
31
|
+
});
|
|
32
|
+
assert.equal(out.steps.length, 3);
|
|
33
|
+
assert.ok(out.steps.every((s) => s.durationMs >= 0));
|
|
34
|
+
assert.ok(out.steps.every((s) => s.totalTokensEst >= 0));
|
|
35
|
+
assert.equal(out.assertions.pass, true);
|
|
36
|
+
});
|
|
37
|
+
test('AssetRef requires context(on) resourceBindings after serializable-class coverage', async () => {
|
|
38
|
+
const noEvidenceRunner = {
|
|
39
|
+
context: async () => ({ status: 'found', resourceBindings: [] }),
|
|
40
|
+
query: async () => ({ process_symbols: [] }),
|
|
41
|
+
impact: async () => ({ impactedCount: 0 }),
|
|
42
|
+
cypher: async () => ({ rows: [] }),
|
|
43
|
+
};
|
|
44
|
+
const out = await runSymbolScenario(noEvidenceRunner, {
|
|
45
|
+
symbol: 'AssetRef',
|
|
46
|
+
kind: 'serializable-class',
|
|
47
|
+
objectives: ['verify usage evidence'],
|
|
48
|
+
deepDivePlan: [{ tool: 'query', input: { query: 'AssetRef usage' } }],
|
|
49
|
+
});
|
|
50
|
+
assert.equal(out.assertions.pass, false);
|
|
51
|
+
assert.ok(out.assertions.failures.some((f) => f.includes('context(on) must include resourceBindings')));
|
|
52
|
+
});
|
|
53
|
+
test('AssetRef requires deep-dive evidence even when context(on) has resourceBindings', async () => {
|
|
54
|
+
const noDeepDiveEvidenceRunner = {
|
|
55
|
+
context: async () => ({
|
|
56
|
+
status: 'found',
|
|
57
|
+
resourceBindings: [{ resourcePath: 'Assets/Data/Unlock.asset', resourceType: 'asset' }],
|
|
58
|
+
}),
|
|
59
|
+
query: async () => ({ process_symbols: [] }),
|
|
60
|
+
impact: async () => ({ impactedCount: 0 }),
|
|
61
|
+
cypher: async () => ({ rows: [] }),
|
|
62
|
+
};
|
|
63
|
+
const out = await runSymbolScenario(noDeepDiveEvidenceRunner, {
|
|
64
|
+
symbol: 'AssetRef',
|
|
65
|
+
kind: 'serializable-class',
|
|
66
|
+
objectives: ['verify usage evidence'],
|
|
67
|
+
deepDivePlan: [{ tool: 'query', input: { query: 'AssetRef usage' } }],
|
|
68
|
+
});
|
|
69
|
+
assert.equal(out.assertions.pass, false);
|
|
70
|
+
assert.ok(out.assertions.failures.some((f) => f.includes('deep-dive must provide usage/dependency evidence')));
|
|
71
|
+
});
|
|
72
|
+
test('AssetRef passes when context(on) bindings and deep-dive evidence are both present', async () => {
|
|
73
|
+
const satisfiedRunner = {
|
|
74
|
+
context: async () => ({
|
|
75
|
+
status: 'found',
|
|
76
|
+
resourceBindings: [{ resourcePath: 'Assets/Data/Unlock.asset', resourceType: 'asset' }],
|
|
77
|
+
}),
|
|
78
|
+
query: async () => ({ process_symbols: [{ id: 'Class:Assets/Scripts/UnlockContent.cs:UnlockContent' }] }),
|
|
79
|
+
impact: async () => ({ impactedCount: 0 }),
|
|
80
|
+
cypher: async () => ({ rows: [] }),
|
|
81
|
+
};
|
|
82
|
+
const out = await runSymbolScenario(satisfiedRunner, {
|
|
83
|
+
symbol: 'AssetRef',
|
|
84
|
+
kind: 'serializable-class',
|
|
85
|
+
objectives: ['verify usage evidence'],
|
|
86
|
+
deepDivePlan: [{ tool: 'query', input: { query: 'AssetRef usage' } }],
|
|
87
|
+
});
|
|
88
|
+
assert.equal(out.assertions.pass, true);
|
|
89
|
+
assert.equal(out.assertions.failures.length, 0);
|
|
90
|
+
});
|
|
91
|
+
test('PlayerActor scenario uses context file hint and valid context deep-dive input', async () => {
|
|
92
|
+
const config = await loadE2EConfig('benchmarks/u2-e2e/neonspark-full-u2-e2e.config.json');
|
|
93
|
+
const player = config.symbolScenarios.find((s) => s.symbol === 'PlayerActor');
|
|
94
|
+
assert.equal(player?.contextFileHint, 'Assets/NEON/Code/Game/Actors/PlayerActor/PlayerActor.cs');
|
|
95
|
+
assert.equal(player?.deepDivePlan[0]?.tool, 'context');
|
|
96
|
+
assert.equal(player?.deepDivePlan[0]?.input?.name, 'PlayerActor');
|
|
97
|
+
});
|
|
98
|
+
test('runSymbolScenario retries context with file hint when response is ambiguous', async () => {
|
|
99
|
+
const hint = 'Assets/NEON/Code/Game/Actors/PlayerActor/PlayerActor.cs';
|
|
100
|
+
const contextCalls = [];
|
|
101
|
+
const runner = {
|
|
102
|
+
context: async (input) => {
|
|
103
|
+
contextCalls.push(input);
|
|
104
|
+
if (input.unity_resources === 'off') {
|
|
105
|
+
return { status: 'found' };
|
|
106
|
+
}
|
|
107
|
+
if (input.file_path === hint) {
|
|
108
|
+
return {
|
|
109
|
+
status: 'found',
|
|
110
|
+
resourceBindings: [
|
|
111
|
+
{
|
|
112
|
+
resourcePath: 'Assets/Prefabs/Player.prefab',
|
|
113
|
+
resourceType: 'prefab',
|
|
114
|
+
resolvedReferences: [{ uid: 'Class:PlayerActor' }],
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
status: 'ambiguous',
|
|
121
|
+
candidates: [
|
|
122
|
+
{
|
|
123
|
+
uid: 'Class:Assets/NEON/Code/Game/Actors/PlayerActor/PlayerActor.Visual.cs:PlayerActor',
|
|
124
|
+
kind: 'Class',
|
|
125
|
+
filePath: 'Assets/NEON/Code/Game/Actors/PlayerActor/PlayerActor.Visual.cs',
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
query: async () => ({ process_symbols: [] }),
|
|
131
|
+
impact: async () => ({ impactedCount: 0 }),
|
|
132
|
+
cypher: async () => ({ rows: [] }),
|
|
133
|
+
};
|
|
134
|
+
const out = await runSymbolScenario(runner, {
|
|
135
|
+
symbol: 'PlayerActor',
|
|
136
|
+
kind: 'partial-component',
|
|
137
|
+
contextFileHint: hint,
|
|
138
|
+
objectives: ['verify fallback'],
|
|
139
|
+
deepDivePlan: [{ tool: 'query', input: { query: 'PlayerActor resource binding' } }],
|
|
140
|
+
});
|
|
141
|
+
assert.equal(contextCalls.length, 3);
|
|
142
|
+
assert.equal(contextCalls[2]?.file_path, hint);
|
|
143
|
+
assert.equal(out.steps[1]?.output?.status, 'found');
|
|
144
|
+
assert.equal(out.assertions.pass, true);
|
|
145
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface NumericStats {
|
|
2
|
+
mean: number;
|
|
3
|
+
median: number;
|
|
4
|
+
min: number;
|
|
5
|
+
max: number;
|
|
6
|
+
spread: number;
|
|
7
|
+
}
|
|
8
|
+
export interface MetricThreshold {
|
|
9
|
+
medianMax: number;
|
|
10
|
+
maxMax: number;
|
|
11
|
+
}
|
|
12
|
+
export interface U2PerformanceThresholds {
|
|
13
|
+
metaIndexMs?: MetricThreshold;
|
|
14
|
+
referenceResolveMs?: MetricThreshold;
|
|
15
|
+
graphReferenceWriteMs?: MetricThreshold;
|
|
16
|
+
}
|
|
17
|
+
export interface U2ThresholdVerdict {
|
|
18
|
+
pass: boolean;
|
|
19
|
+
metrics: Record<string, {
|
|
20
|
+
pass: boolean;
|
|
21
|
+
actual: {
|
|
22
|
+
median: number;
|
|
23
|
+
max: number;
|
|
24
|
+
};
|
|
25
|
+
expected: MetricThreshold;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
export declare function computeNumericStats(values: number[]): NumericStats;
|
|
29
|
+
export declare function evaluateMetricsThresholds(metrics: {
|
|
30
|
+
metaIndexMs: number[];
|
|
31
|
+
referenceResolveMs: number[];
|
|
32
|
+
graphReferenceWriteMs: number[];
|
|
33
|
+
}, thresholds: U2PerformanceThresholds): U2ThresholdVerdict;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { performance } from 'node:perf_hooks';
|
|
4
|
+
import { runPipelineFromRepo } from '../core/ingestion/pipeline.js';
|
|
5
|
+
import { resolveAnalyzeScopeRules } from '../cli/analyze-options.js';
|
|
6
|
+
export function computeNumericStats(values) {
|
|
7
|
+
if (values.length === 0) {
|
|
8
|
+
return { mean: 0, median: 0, min: 0, max: 0, spread: 0 };
|
|
9
|
+
}
|
|
10
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
11
|
+
const mean = sorted.reduce((sum, value) => sum + value, 0) / sorted.length;
|
|
12
|
+
const mid = Math.floor(sorted.length / 2);
|
|
13
|
+
const median = sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
14
|
+
const min = sorted[0];
|
|
15
|
+
const max = sorted[sorted.length - 1];
|
|
16
|
+
return {
|
|
17
|
+
mean: round1(mean),
|
|
18
|
+
median: round1(median),
|
|
19
|
+
min: round1(min),
|
|
20
|
+
max: round1(max),
|
|
21
|
+
spread: round1(max - min),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function evaluateMetricsThresholds(metrics, thresholds) {
|
|
25
|
+
const verdict = { pass: true, metrics: {} };
|
|
26
|
+
const entries = [
|
|
27
|
+
['metaIndexMs', thresholds.metaIndexMs],
|
|
28
|
+
['referenceResolveMs', thresholds.referenceResolveMs],
|
|
29
|
+
['graphReferenceWriteMs', thresholds.graphReferenceWriteMs],
|
|
30
|
+
];
|
|
31
|
+
for (const [name, threshold] of entries) {
|
|
32
|
+
if (!threshold)
|
|
33
|
+
continue;
|
|
34
|
+
const stats = computeNumericStats(metrics[name]);
|
|
35
|
+
const metricPass = stats.median <= threshold.medianMax && stats.max <= threshold.maxMax;
|
|
36
|
+
verdict.metrics[name] = {
|
|
37
|
+
pass: metricPass,
|
|
38
|
+
actual: { median: stats.median, max: stats.max },
|
|
39
|
+
expected: threshold,
|
|
40
|
+
};
|
|
41
|
+
if (!metricPass) {
|
|
42
|
+
verdict.pass = false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return verdict;
|
|
46
|
+
}
|
|
47
|
+
async function samplePipeline(targetPath, run, scopeRules) {
|
|
48
|
+
const phases = [];
|
|
49
|
+
let currentPhase = null;
|
|
50
|
+
let phaseStart = performance.now();
|
|
51
|
+
const onProgress = (progress) => {
|
|
52
|
+
const now = performance.now();
|
|
53
|
+
if (!currentPhase) {
|
|
54
|
+
currentPhase = progress.phase;
|
|
55
|
+
phaseStart = now;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (progress.phase !== currentPhase) {
|
|
59
|
+
phases.push({ phase: currentPhase, durationMs: round1(now - phaseStart) });
|
|
60
|
+
currentPhase = progress.phase;
|
|
61
|
+
phaseStart = now;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const t0 = performance.now();
|
|
65
|
+
const result = await runPipelineFromRepo(targetPath, onProgress, {
|
|
66
|
+
scopeRules,
|
|
67
|
+
});
|
|
68
|
+
const t1 = performance.now();
|
|
69
|
+
if (currentPhase) {
|
|
70
|
+
phases.push({ phase: currentPhase, durationMs: round1(t1 - phaseStart) });
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
run,
|
|
74
|
+
totalMs: round1(t1 - t0),
|
|
75
|
+
phases,
|
|
76
|
+
unity: {
|
|
77
|
+
processedSymbols: result.unityResult.processedSymbols,
|
|
78
|
+
bindingCount: result.unityResult.bindingCount,
|
|
79
|
+
diagnosticsTotal: result.unityResult.diagnostics.length,
|
|
80
|
+
timingsMs: {
|
|
81
|
+
scanContext: result.unityResult.timingsMs.scanContext,
|
|
82
|
+
resolve: result.unityResult.timingsMs.resolve,
|
|
83
|
+
graphWrite: result.unityResult.timingsMs.graphWrite,
|
|
84
|
+
total: result.unityResult.timingsMs.total,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function parseArgs(argv) {
|
|
90
|
+
const get = (name) => {
|
|
91
|
+
const index = argv.indexOf(name);
|
|
92
|
+
if (index === -1 || index + 1 >= argv.length)
|
|
93
|
+
return undefined;
|
|
94
|
+
return argv[index + 1];
|
|
95
|
+
};
|
|
96
|
+
const targetPath = get('--target-path');
|
|
97
|
+
const reportPath = get('--report');
|
|
98
|
+
const thresholdsPath = get('--thresholds');
|
|
99
|
+
const scopeManifest = get('--scope-manifest');
|
|
100
|
+
const runs = Number(get('--runs') || '3');
|
|
101
|
+
const scopePrefix = [];
|
|
102
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
103
|
+
if (argv[i] === '--scope-prefix' && i + 1 < argv.length) {
|
|
104
|
+
scopePrefix.push(argv[i + 1]);
|
|
105
|
+
i += 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!targetPath)
|
|
109
|
+
throw new Error('Missing required arg: --target-path <path>');
|
|
110
|
+
if (!reportPath)
|
|
111
|
+
throw new Error('Missing required arg: --report <path>');
|
|
112
|
+
if (!Number.isFinite(runs) || runs <= 0)
|
|
113
|
+
throw new Error('Invalid --runs, must be positive integer');
|
|
114
|
+
return { targetPath: path.resolve(targetPath), runs: Math.floor(runs), reportPath: path.resolve(reportPath), thresholdsPath: thresholdsPath ? path.resolve(thresholdsPath) : undefined, scopeManifest, scopePrefix };
|
|
115
|
+
}
|
|
116
|
+
function round1(value) {
|
|
117
|
+
return Number(value.toFixed(1));
|
|
118
|
+
}
|
|
119
|
+
async function main() {
|
|
120
|
+
const args = parseArgs(process.argv.slice(2));
|
|
121
|
+
const scopeRules = await resolveAnalyzeScopeRules({
|
|
122
|
+
scopeManifest: args.scopeManifest,
|
|
123
|
+
scopePrefix: args.scopePrefix,
|
|
124
|
+
});
|
|
125
|
+
const runs = [];
|
|
126
|
+
for (let run = 1; run <= args.runs; run += 1) {
|
|
127
|
+
console.log(`[u2-sampler] run ${run}/${args.runs} ...`);
|
|
128
|
+
runs.push(await samplePipeline(args.targetPath, run, scopeRules));
|
|
129
|
+
}
|
|
130
|
+
const metaIndexMsSeries = runs.map((entry) => entry.unity.timingsMs.scanContext);
|
|
131
|
+
const referenceResolveMsSeries = runs.map((entry) => entry.unity.timingsMs.resolve);
|
|
132
|
+
const graphReferenceWriteMsSeries = runs.map((entry) => entry.unity.timingsMs.graphWrite);
|
|
133
|
+
let thresholdVerdict = null;
|
|
134
|
+
if (args.thresholdsPath) {
|
|
135
|
+
const raw = await fs.readFile(args.thresholdsPath, 'utf-8');
|
|
136
|
+
const thresholds = JSON.parse(raw);
|
|
137
|
+
thresholdVerdict = evaluateMetricsThresholds({
|
|
138
|
+
metaIndexMs: metaIndexMsSeries,
|
|
139
|
+
referenceResolveMs: referenceResolveMsSeries,
|
|
140
|
+
graphReferenceWriteMs: graphReferenceWriteMsSeries,
|
|
141
|
+
}, thresholds);
|
|
142
|
+
}
|
|
143
|
+
const report = {
|
|
144
|
+
capturedAt: new Date().toISOString(),
|
|
145
|
+
targetPath: args.targetPath,
|
|
146
|
+
runs: args.runs,
|
|
147
|
+
scope: {
|
|
148
|
+
scopeManifest: args.scopeManifest || null,
|
|
149
|
+
scopePrefix: args.scopePrefix,
|
|
150
|
+
scopeRuleCount: scopeRules.length,
|
|
151
|
+
},
|
|
152
|
+
samples: runs,
|
|
153
|
+
metrics: {
|
|
154
|
+
metaIndexMs: computeNumericStats(metaIndexMsSeries),
|
|
155
|
+
referenceResolveMs: computeNumericStats(referenceResolveMsSeries),
|
|
156
|
+
graphReferenceWriteMs: computeNumericStats(graphReferenceWriteMsSeries),
|
|
157
|
+
unityTotalMs: computeNumericStats(runs.map((entry) => entry.unity.timingsMs.total)),
|
|
158
|
+
pipelineTotalMs: computeNumericStats(runs.map((entry) => entry.totalMs)),
|
|
159
|
+
},
|
|
160
|
+
notes: {
|
|
161
|
+
metaIndexMsDefinition: 'Current implementation uses unity.timingsMs.scanContext as metaIndexMs proxy (scanContext includes meta index + resource scan + asset meta index).',
|
|
162
|
+
},
|
|
163
|
+
thresholdVerdict,
|
|
164
|
+
};
|
|
165
|
+
await fs.mkdir(path.dirname(args.reportPath), { recursive: true });
|
|
166
|
+
await fs.writeFile(args.reportPath, JSON.stringify(report, null, 2));
|
|
167
|
+
console.log(`[u2-sampler] report written: ${args.reportPath}`);
|
|
168
|
+
if (thresholdVerdict && !thresholdVerdict.pass) {
|
|
169
|
+
process.exitCode = 1;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const modulePath = process.argv[1] ? path.resolve(process.argv[1]) : '';
|
|
173
|
+
if (import.meta.url === `file://${modulePath}`) {
|
|
174
|
+
main().catch((error) => {
|
|
175
|
+
console.error(`[u2-sampler] failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { computeNumericStats, evaluateMetricsThresholds } from './u2-performance-sampler.js';
|
|
4
|
+
test('computeNumericStats returns stable summary fields', () => {
|
|
5
|
+
const stats = computeNumericStats([120, 80, 100]);
|
|
6
|
+
assert.equal(stats.mean, 100);
|
|
7
|
+
assert.equal(stats.median, 100);
|
|
8
|
+
assert.equal(stats.min, 80);
|
|
9
|
+
assert.equal(stats.max, 120);
|
|
10
|
+
assert.equal(stats.spread, 40);
|
|
11
|
+
});
|
|
12
|
+
test('evaluateMetricsThresholds marks pass/fail per metric', () => {
|
|
13
|
+
const metrics = {
|
|
14
|
+
metaIndexMs: [4100, 4000, 4200],
|
|
15
|
+
referenceResolveMs: [3000, 2900, 3100],
|
|
16
|
+
graphReferenceWriteMs: [250, 240, 260],
|
|
17
|
+
};
|
|
18
|
+
const thresholds = {
|
|
19
|
+
metaIndexMs: { medianMax: 4500, maxMax: 5000 },
|
|
20
|
+
referenceResolveMs: { medianMax: 3200, maxMax: 3600 },
|
|
21
|
+
graphReferenceWriteMs: { medianMax: 260, maxMax: 300 },
|
|
22
|
+
};
|
|
23
|
+
const verdict = evaluateMetricsThresholds(metrics, thresholds);
|
|
24
|
+
assert.equal(verdict.pass, true);
|
|
25
|
+
assert.equal(verdict.metrics.metaIndexMs?.pass, true);
|
|
26
|
+
assert.equal(verdict.metrics.referenceResolveMs?.pass, true);
|
|
27
|
+
assert.equal(verdict.metrics.graphReferenceWriteMs?.pass, true);
|
|
28
|
+
const failing = evaluateMetricsThresholds(metrics, {
|
|
29
|
+
...thresholds,
|
|
30
|
+
referenceResolveMs: { medianMax: 2500, maxMax: 2800 },
|
|
31
|
+
});
|
|
32
|
+
assert.equal(failing.pass, false);
|
|
33
|
+
assert.equal(failing.metrics.referenceResolveMs?.pass, false);
|
|
34
|
+
});
|
package/dist/cli/ai-context.js
CHANGED
|
@@ -38,7 +38,7 @@ This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} s
|
|
|
38
38
|
2. **Match your task to a skill below** and **read that skill file**
|
|
39
39
|
3. **Follow the skill's workflow and checklist**
|
|
40
40
|
|
|
41
|
-
> If step 1 warns the index is stale,
|
|
41
|
+
> If step 1 warns the index is stale, ask user whether to rebuild index via \`npx -y gitnexus analyze\` first (it reuses previous analyze scope/options by default; add \`--no-reuse-options\` to reset). If user declines, explicitly warn that retrieval may not reflect current codebase. For build/analyze/test commands, use a 10-30 minute timeout; on failure/timeout, report exact tool output and do not auto-retry or silently fall back to glob/grep.
|
|
42
42
|
|
|
43
43
|
## Skills
|
|
44
44
|
|
|
@@ -20,3 +20,13 @@ test('runPipelineFromRepo deduplicates overlapping scope matches and reports dia
|
|
|
20
20
|
assert.equal(result.scopeDiagnostics?.dedupedMatchCount, 3);
|
|
21
21
|
assert.equal(result.scopeDiagnostics?.normalizedCollisions.length, 0);
|
|
22
22
|
});
|
|
23
|
+
test('pipeline forwards extension-filtered scoped paths to unity enrich', { timeout: 60_000 }, async () => {
|
|
24
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const fixtureRepo = path.resolve(here, '../../src/core/unity/__fixtures__/mini-unity');
|
|
26
|
+
const result = await runPipelineFromRepo(fixtureRepo, () => { }, {
|
|
27
|
+
includeExtensions: ['.cs'],
|
|
28
|
+
scopeRules: ['Assets'],
|
|
29
|
+
});
|
|
30
|
+
assert.ok((result.unityResult?.bindingCount || 0) > 0);
|
|
31
|
+
assert.ok(result.unityResult?.diagnostics.some((message) => message.includes('scanContext:')));
|
|
32
|
+
});
|