@veewo/gitnexus 1.3.8 → 1.3.9
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.d.ts +1 -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.d.ts +1 -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.d.ts +1 -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.d.ts +1 -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.d.ts +1 -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.d.ts +1 -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/analyze-multi-scope-regression.test.js +10 -0
- 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.js +11 -6
- 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 +20 -0
- 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/tools.js +12 -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 +14 -7
package/README.md
CHANGED
|
@@ -176,12 +176,17 @@ gitnexus clean # Delete index for current repo
|
|
|
176
176
|
gitnexus clean --all --force # Delete all indexes
|
|
177
177
|
gitnexus wiki [path] # Generate LLM-powered docs from knowledge graph
|
|
178
178
|
gitnexus wiki --model <model> # Wiki with custom LLM model (default: gpt-4o-mini)
|
|
179
|
+
gitnexus unity-bindings <symbol> --target-path <path> [--json] # Experimental Unity C# <-> prefab/scene/asset cross-reference
|
|
180
|
+
gitnexus context <symbol> --unity-resources on # Include graph-native Unity resource data (opt-in)
|
|
181
|
+
gitnexus query <symbol> --unity-resources on # Enrich query symbol hits with Unity resource data (opt-in)
|
|
179
182
|
gitnexus benchmark-unity ../benchmarks/unity-baseline/v1 --profile quick --target-path ../benchmarks/fixtures/unity-mini
|
|
180
183
|
gitnexus benchmark-unity ../benchmarks/unity-baseline/v1 --profile full --target-path ../benchmarks/fixtures/unity-mini
|
|
181
184
|
```
|
|
182
185
|
|
|
183
186
|
For scoped indexing, `analyze` logs scope overlap dedupe counts and any normalized path collisions to help diagnose multi-directory merge safety.
|
|
184
187
|
|
|
188
|
+
Unity resource retrieval is opt-in on `query/context` via `unity_resources: off|on|auto` (default: `off`). Use `--unity-resources on` when you need `resourceBindings`, `serializedFields`, `resolvedReferences`, and `unityDiagnostics` in output.
|
|
189
|
+
|
|
185
190
|
## Unity Benchmark
|
|
186
191
|
|
|
187
192
|
Run reproducible Unity/C# accuracy and regression checks:
|
|
@@ -18,7 +18,7 @@ test('resolveBenchmarkRepoName falls back to repo alias', () => {
|
|
|
18
18
|
});
|
|
19
19
|
test('resolveBenchmarkRepoName uses target basename when no repo input exists', () => {
|
|
20
20
|
const resolved = resolveBenchmarkRepoName({
|
|
21
|
-
targetPath: '/
|
|
21
|
+
targetPath: '/tmp/unity-projects/neonspark',
|
|
22
22
|
});
|
|
23
23
|
assert.equal(resolved, 'neonspark');
|
|
24
24
|
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface AnalyzeSummary {
|
|
2
|
+
totalSec: number;
|
|
3
|
+
kuzuSec: number;
|
|
4
|
+
ftsSec: number;
|
|
5
|
+
nodes?: number;
|
|
6
|
+
edges?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface EstimateVerdict {
|
|
9
|
+
actualSec: number;
|
|
10
|
+
lower: number;
|
|
11
|
+
upper: number;
|
|
12
|
+
inRange: boolean;
|
|
13
|
+
status: 'below-range' | 'in-range' | 'above-range';
|
|
14
|
+
deltaSec: number;
|
|
15
|
+
}
|
|
16
|
+
interface EstimateRange {
|
|
17
|
+
lower: number;
|
|
18
|
+
upper: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function parseAnalyzeSummary(logPath: string): Promise<AnalyzeSummary>;
|
|
21
|
+
export declare function compareEstimate(actualSec: number, range: EstimateRange): EstimateVerdict;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
function round1(value) {
|
|
4
|
+
return Number(value.toFixed(1));
|
|
5
|
+
}
|
|
6
|
+
function parseNumber(raw) {
|
|
7
|
+
if (!raw)
|
|
8
|
+
return 0;
|
|
9
|
+
const value = Number(raw);
|
|
10
|
+
return Number.isFinite(value) ? value : 0;
|
|
11
|
+
}
|
|
12
|
+
function candidatePaths(inputPath) {
|
|
13
|
+
if (path.isAbsolute(inputPath)) {
|
|
14
|
+
return [inputPath];
|
|
15
|
+
}
|
|
16
|
+
return [
|
|
17
|
+
path.resolve(process.cwd(), inputPath),
|
|
18
|
+
path.resolve(process.cwd(), 'src/benchmark/u2-e2e', inputPath),
|
|
19
|
+
path.resolve(process.cwd(), 'gitnexus/src/benchmark/u2-e2e', inputPath),
|
|
20
|
+
path.resolve(process.cwd(), '..', inputPath),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
export async function parseAnalyzeSummary(logPath) {
|
|
24
|
+
let raw = '';
|
|
25
|
+
const tried = [];
|
|
26
|
+
for (const candidate of candidatePaths(logPath)) {
|
|
27
|
+
tried.push(candidate);
|
|
28
|
+
try {
|
|
29
|
+
raw = await fs.readFile(candidate, 'utf-8');
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code !== 'ENOENT') {
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!raw) {
|
|
39
|
+
throw new Error(`Analyze log not found: ${logPath}. Tried: ${tried.join(', ')}`);
|
|
40
|
+
}
|
|
41
|
+
const totalMatch = raw.match(/Repository indexed successfully \(([\d.]+)s\)/i);
|
|
42
|
+
const kuzuFtsMatch = raw.match(/KuzuDB\s+([\d.]+)s\s*\|\s*FTS\s+([\d.]+)s/i);
|
|
43
|
+
const realMatch = raw.match(/^real\s+([\d.]+)$/m);
|
|
44
|
+
const nodesMatch = raw.match(/\bnodes?\D+(\d+)/i);
|
|
45
|
+
const edgesMatch = raw.match(/\bedges?\D+(\d+)/i);
|
|
46
|
+
const totalSec = parseNumber(totalMatch?.[1]) || parseNumber(realMatch?.[1]);
|
|
47
|
+
const kuzuSec = parseNumber(kuzuFtsMatch?.[1]);
|
|
48
|
+
const ftsSec = parseNumber(kuzuFtsMatch?.[2]);
|
|
49
|
+
if (!totalSec) {
|
|
50
|
+
throw new Error(`Failed to parse total duration from analyze log: ${logPath}`);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
totalSec: round1(totalSec),
|
|
54
|
+
kuzuSec: round1(kuzuSec),
|
|
55
|
+
ftsSec: round1(ftsSec),
|
|
56
|
+
nodes: nodesMatch ? parseNumber(nodesMatch[1]) : undefined,
|
|
57
|
+
edges: edgesMatch ? parseNumber(edgesMatch[1]) : undefined,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function compareEstimate(actualSec, range) {
|
|
61
|
+
if (actualSec < range.lower) {
|
|
62
|
+
return {
|
|
63
|
+
actualSec: round1(actualSec),
|
|
64
|
+
lower: range.lower,
|
|
65
|
+
upper: range.upper,
|
|
66
|
+
inRange: false,
|
|
67
|
+
status: 'below-range',
|
|
68
|
+
deltaSec: round1(actualSec - range.lower),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (actualSec > range.upper) {
|
|
72
|
+
return {
|
|
73
|
+
actualSec: round1(actualSec),
|
|
74
|
+
lower: range.lower,
|
|
75
|
+
upper: range.upper,
|
|
76
|
+
inRange: false,
|
|
77
|
+
status: 'above-range',
|
|
78
|
+
deltaSec: round1(actualSec - range.upper),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
actualSec: round1(actualSec),
|
|
83
|
+
lower: range.lower,
|
|
84
|
+
upper: range.upper,
|
|
85
|
+
inRange: true,
|
|
86
|
+
status: 'in-range',
|
|
87
|
+
deltaSec: 0,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { parseAnalyzeSummary, compareEstimate } from './analyze-parser.js';
|
|
4
|
+
test('parseAnalyzeSummary extracts totalSec and kuzu/fts sec', async () => {
|
|
5
|
+
const summary = await parseAnalyzeSummary('__fixtures__/analyze.log');
|
|
6
|
+
assert.equal(summary.totalSec, 114.8);
|
|
7
|
+
assert.equal(summary.kuzuSec, 73.5);
|
|
8
|
+
assert.equal(summary.ftsSec, 19.6);
|
|
9
|
+
});
|
|
10
|
+
test('compareEstimate marks in-range status', () => {
|
|
11
|
+
const verdict = compareEstimate(500, { lower: 322.6, upper: 540.1 });
|
|
12
|
+
assert.equal(verdict.status, 'in-range');
|
|
13
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface AssetRefPathInstance {
|
|
2
|
+
fieldName: string;
|
|
3
|
+
relativePath: string;
|
|
4
|
+
isEmpty: boolean;
|
|
5
|
+
isSprite: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface CharacterListAssetRefSpriteSummary {
|
|
8
|
+
extractedAssetRefInstances: number;
|
|
9
|
+
nonEmptyAssetRefInstances: number;
|
|
10
|
+
spriteAssetRefInstances: number;
|
|
11
|
+
spriteRatioInNonEmpty: number | null;
|
|
12
|
+
uniqueSpriteAssets: number;
|
|
13
|
+
byFieldAllNonEmpty: Record<string, number>;
|
|
14
|
+
byFieldSpriteOnly: Record<string, number>;
|
|
15
|
+
topSpritePaths: Record<string, number>;
|
|
16
|
+
}
|
|
17
|
+
export declare function isSpriteRelativePath(input: string): boolean;
|
|
18
|
+
export declare function extractAssetRefPathInstances(bindings: Array<Record<string, any>>): AssetRefPathInstance[];
|
|
19
|
+
export declare function summarizeCharacterListAssetRefSprite(bindings: Array<Record<string, any>>): CharacterListAssetRefSpriteSummary;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
function unquote(input) {
|
|
2
|
+
return input.replace(/^"|"$/g, '');
|
|
3
|
+
}
|
|
4
|
+
export function isSpriteRelativePath(input) {
|
|
5
|
+
const value = input.trim().toLowerCase();
|
|
6
|
+
if (!value)
|
|
7
|
+
return false;
|
|
8
|
+
if (value.includes('/sprites/'))
|
|
9
|
+
return true;
|
|
10
|
+
return /\.(png|jpg|jpeg|tga|psd|webp|spriteatlas|spriteatlasv2)$/.test(value);
|
|
11
|
+
}
|
|
12
|
+
function countBy(rows, keyFn) {
|
|
13
|
+
const counts = new Map();
|
|
14
|
+
for (const row of rows) {
|
|
15
|
+
const key = keyFn(row);
|
|
16
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
17
|
+
}
|
|
18
|
+
return Object.fromEntries([...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])));
|
|
19
|
+
}
|
|
20
|
+
export function extractAssetRefPathInstances(bindings) {
|
|
21
|
+
const rows = [];
|
|
22
|
+
for (const binding of bindings) {
|
|
23
|
+
const structuredRows = Array.isArray(binding?.assetRefPaths) ? binding.assetRefPaths : [];
|
|
24
|
+
if (structuredRows.length > 0) {
|
|
25
|
+
for (const structured of structuredRows) {
|
|
26
|
+
const relativePath = String(structured.relativePath || '');
|
|
27
|
+
rows.push({
|
|
28
|
+
fieldName: String(structured.fieldName || 'unknown'),
|
|
29
|
+
relativePath,
|
|
30
|
+
isEmpty: Boolean(structured.isEmpty ?? relativePath.length === 0),
|
|
31
|
+
isSprite: Boolean(structured.isSprite ?? isSpriteRelativePath(relativePath)),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const scalarFields = Array.isArray(binding?.serializedFields?.scalarFields)
|
|
37
|
+
? binding.serializedFields.scalarFields
|
|
38
|
+
: [];
|
|
39
|
+
for (const scalar of scalarFields) {
|
|
40
|
+
const text = String(scalar?.value || '');
|
|
41
|
+
if (!text)
|
|
42
|
+
continue;
|
|
43
|
+
const lines = text.split(/\r?\n/);
|
|
44
|
+
let currentFieldName = 'unknown';
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
const fieldMatch = line.match(/^\s*([A-Za-z0-9_]*Ref):\s*$/);
|
|
47
|
+
if (fieldMatch) {
|
|
48
|
+
currentFieldName = fieldMatch[1];
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const relativePathMatch = line.match(/^\s*_relativePath:\s*(.*)$/);
|
|
52
|
+
if (!relativePathMatch)
|
|
53
|
+
continue;
|
|
54
|
+
const relativePath = unquote((relativePathMatch[1] || '').trim());
|
|
55
|
+
rows.push({
|
|
56
|
+
fieldName: currentFieldName,
|
|
57
|
+
relativePath,
|
|
58
|
+
isEmpty: relativePath.length === 0,
|
|
59
|
+
isSprite: isSpriteRelativePath(relativePath),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return rows;
|
|
65
|
+
}
|
|
66
|
+
export function summarizeCharacterListAssetRefSprite(bindings) {
|
|
67
|
+
const extracted = extractAssetRefPathInstances(bindings);
|
|
68
|
+
const nonEmpty = extracted.filter((row) => !row.isEmpty);
|
|
69
|
+
const spriteOnly = nonEmpty.filter((row) => row.isSprite);
|
|
70
|
+
return {
|
|
71
|
+
extractedAssetRefInstances: extracted.length,
|
|
72
|
+
nonEmptyAssetRefInstances: nonEmpty.length,
|
|
73
|
+
spriteAssetRefInstances: spriteOnly.length,
|
|
74
|
+
spriteRatioInNonEmpty: nonEmpty.length === 0 ? null : Number((spriteOnly.length / nonEmpty.length).toFixed(4)),
|
|
75
|
+
uniqueSpriteAssets: new Set(spriteOnly.map((row) => row.relativePath)).size,
|
|
76
|
+
byFieldAllNonEmpty: countBy(nonEmpty, (row) => row.fieldName),
|
|
77
|
+
byFieldSpriteOnly: countBy(spriteOnly, (row) => row.fieldName),
|
|
78
|
+
topSpritePaths: countBy(spriteOnly, (row) => row.relativePath),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { extractAssetRefPathInstances, summarizeCharacterListAssetRefSprite, } from './characterlist-assetref.js';
|
|
4
|
+
const SAMPLE_BINDING = {
|
|
5
|
+
resourcePath: 'Assets/NEON/DataAssets/CharacterList.asset',
|
|
6
|
+
serializedFields: {
|
|
7
|
+
scalarFields: [
|
|
8
|
+
{
|
|
9
|
+
name: 'Values',
|
|
10
|
+
value: `
|
|
11
|
+
_Head_Ref:
|
|
12
|
+
_assetBundleName: char_head
|
|
13
|
+
_relativePath: Assets/NEON/Art/Sprites/UI/0_pixle/ui_character_head/hero_head_Nik.png
|
|
14
|
+
_actorPrefabRef:
|
|
15
|
+
_assetBundleName: char_nik_actor
|
|
16
|
+
_relativePath: Assets/ActorPrefab/Actor_Nik/V_Actor_Nik.prefab
|
|
17
|
+
_lockedSprite_Ref:
|
|
18
|
+
_assetBundleName: char_nik_portrait
|
|
19
|
+
_relativePath: Assets/NEON/Art/Sprites/UI/4K/new_UI_character_choose/heroes_pic/hero_pic_nik.png
|
|
20
|
+
_activeSkillPowerUp_Ref:
|
|
21
|
+
_assetBundleName:
|
|
22
|
+
_relativePath:
|
|
23
|
+
`,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
test('extractAssetRefPathInstances parses _relativePath rows and preserves field names', () => {
|
|
29
|
+
const rows = extractAssetRefPathInstances([SAMPLE_BINDING]);
|
|
30
|
+
assert.equal(rows.length, 4);
|
|
31
|
+
const byField = new Map(rows.map((row) => [row.fieldName, row]));
|
|
32
|
+
assert.equal(byField.get('_Head_Ref')?.relativePath, 'Assets/NEON/Art/Sprites/UI/0_pixle/ui_character_head/hero_head_Nik.png');
|
|
33
|
+
assert.equal(byField.get('_actorPrefabRef')?.relativePath, 'Assets/ActorPrefab/Actor_Nik/V_Actor_Nik.prefab');
|
|
34
|
+
assert.equal(byField.get('_lockedSprite_Ref')?.relativePath, 'Assets/NEON/Art/Sprites/UI/4K/new_UI_character_choose/heroes_pic/hero_pic_nik.png');
|
|
35
|
+
assert.equal(byField.get('_activeSkillPowerUp_Ref')?.relativePath, '');
|
|
36
|
+
});
|
|
37
|
+
test('summarizeCharacterListAssetRefSprite counts non-empty and sprite instances', () => {
|
|
38
|
+
const summary = summarizeCharacterListAssetRefSprite([SAMPLE_BINDING]);
|
|
39
|
+
assert.equal(summary.extractedAssetRefInstances, 4);
|
|
40
|
+
assert.equal(summary.nonEmptyAssetRefInstances, 3);
|
|
41
|
+
assert.equal(summary.spriteAssetRefInstances, 2);
|
|
42
|
+
assert.equal(summary.uniqueSpriteAssets, 2);
|
|
43
|
+
assert.equal(summary.spriteRatioInNonEmpty, 0.6667);
|
|
44
|
+
});
|
|
45
|
+
test('summarizeCharacterListAssetRefSprite field histogram keeps only sprite fields for sprite map', () => {
|
|
46
|
+
const summary = summarizeCharacterListAssetRefSprite([SAMPLE_BINDING]);
|
|
47
|
+
assert.equal(summary.byFieldAllNonEmpty._Head_Ref, 1);
|
|
48
|
+
assert.equal(summary.byFieldAllNonEmpty._actorPrefabRef, 1);
|
|
49
|
+
assert.equal(summary.byFieldAllNonEmpty._lockedSprite_Ref, 1);
|
|
50
|
+
assert.equal(summary.byFieldSpriteOnly._Head_Ref, 1);
|
|
51
|
+
assert.equal(summary.byFieldSpriteOnly._lockedSprite_Ref, 1);
|
|
52
|
+
assert.equal(summary.byFieldSpriteOnly._actorPrefabRef, undefined);
|
|
53
|
+
});
|
|
54
|
+
test('extractAssetRefPathInstances marks sprite with extension or /Sprites/ path', () => {
|
|
55
|
+
const rows = extractAssetRefPathInstances([
|
|
56
|
+
{
|
|
57
|
+
serializedFields: {
|
|
58
|
+
scalarFields: [
|
|
59
|
+
{
|
|
60
|
+
value: `
|
|
61
|
+
_icon_Ref:
|
|
62
|
+
_relativePath: Assets/Texture/icon.webp
|
|
63
|
+
_atlas_Ref:
|
|
64
|
+
_relativePath: Assets/Atlas/UI.spriteatlasv2
|
|
65
|
+
_folderSprite_Ref:
|
|
66
|
+
_relativePath: Assets/NEON/Art/Sprites/UI/hero_avatar
|
|
67
|
+
`,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
]);
|
|
73
|
+
const spriteOnly = rows.filter((row) => row.isSprite).map((row) => row.fieldName);
|
|
74
|
+
assert.deepEqual(spriteOnly, ['_icon_Ref', '_atlas_Ref', '_folderSprite_Ref']);
|
|
75
|
+
});
|
|
76
|
+
test('extractAssetRefPathInstances prefers structured assetRefPaths when provided', () => {
|
|
77
|
+
const rows = extractAssetRefPathInstances([
|
|
78
|
+
{
|
|
79
|
+
assetRefPaths: [
|
|
80
|
+
{
|
|
81
|
+
fieldName: '_Head_Ref',
|
|
82
|
+
relativePath: 'Assets/NEON/Art/Sprites/UI/head.png',
|
|
83
|
+
isEmpty: false,
|
|
84
|
+
isSprite: true,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
fieldName: '_actorPrefabRef',
|
|
88
|
+
relativePath: 'Assets/ActorPrefab/Actor_Nik/V_Actor_Nik.prefab',
|
|
89
|
+
isEmpty: false,
|
|
90
|
+
isSprite: false,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
serializedFields: {
|
|
94
|
+
scalarFields: [
|
|
95
|
+
{
|
|
96
|
+
value: `
|
|
97
|
+
_Head_Ref:
|
|
98
|
+
_relativePath: Assets/THIS/SHOULD/NOT/BE/USED.png
|
|
99
|
+
`,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
]);
|
|
105
|
+
assert.equal(rows.length, 2);
|
|
106
|
+
assert.equal(rows[0]?.relativePath, 'Assets/NEON/Art/Sprites/UI/head.png');
|
|
107
|
+
assert.equal(rows[1]?.relativePath, 'Assets/ActorPrefab/Actor_Nik/V_Actor_Nik.prefab');
|
|
108
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface E2EConfig {
|
|
2
|
+
runIdPrefix: string;
|
|
3
|
+
targetPath: string;
|
|
4
|
+
repoAliasPrefix: string;
|
|
5
|
+
scope: {
|
|
6
|
+
scriptPrefixes: string[];
|
|
7
|
+
resourcePrefixes: string[];
|
|
8
|
+
};
|
|
9
|
+
estimateRangeSec: {
|
|
10
|
+
lower: number;
|
|
11
|
+
upper: number;
|
|
12
|
+
};
|
|
13
|
+
symbolScenarios: SymbolScenario[];
|
|
14
|
+
}
|
|
15
|
+
export interface SymbolScenario {
|
|
16
|
+
symbol: string;
|
|
17
|
+
kind: 'component' | 'scriptableobject' | 'serializable-class' | 'partial-component';
|
|
18
|
+
objectives: string[];
|
|
19
|
+
contextFileHint?: string;
|
|
20
|
+
deepDivePlan: Array<{
|
|
21
|
+
tool: 'query' | 'context' | 'impact' | 'cypher';
|
|
22
|
+
input: Record<string, unknown>;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
export declare function loadE2EConfig(configPath: string, env?: NodeJS.ProcessEnv): Promise<E2EConfig>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
function candidatePaths(inputPath) {
|
|
4
|
+
if (path.isAbsolute(inputPath)) {
|
|
5
|
+
return [inputPath];
|
|
6
|
+
}
|
|
7
|
+
return [
|
|
8
|
+
path.resolve(process.cwd(), inputPath),
|
|
9
|
+
path.resolve(process.cwd(), '..', inputPath),
|
|
10
|
+
];
|
|
11
|
+
}
|
|
12
|
+
async function readJsonFile(inputPath) {
|
|
13
|
+
const tried = [];
|
|
14
|
+
for (const filePath of candidatePaths(inputPath)) {
|
|
15
|
+
tried.push(filePath);
|
|
16
|
+
try {
|
|
17
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
18
|
+
return JSON.parse(raw);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error.code !== 'ENOENT') {
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`File not found: ${inputPath}. Tried: ${tried.join(', ')}`);
|
|
27
|
+
}
|
|
28
|
+
function parseEnvNumber(raw, envName) {
|
|
29
|
+
if (typeof raw !== 'string')
|
|
30
|
+
return undefined;
|
|
31
|
+
const trimmed = raw.trim();
|
|
32
|
+
if (!trimmed)
|
|
33
|
+
return undefined;
|
|
34
|
+
const parsed = Number(trimmed);
|
|
35
|
+
if (!Number.isFinite(parsed)) {
|
|
36
|
+
throw new Error(`${envName} must be a finite number, got "${raw}"`);
|
|
37
|
+
}
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
function loadEnvOverrides(env) {
|
|
41
|
+
const runIdPrefix = env.GITNEXUS_U2_E2E_RUN_ID_PREFIX?.trim();
|
|
42
|
+
const targetPath = env.GITNEXUS_U2_E2E_TARGET_PATH?.trim();
|
|
43
|
+
const repoAliasPrefix = env.GITNEXUS_U2_E2E_REPO_ALIAS_PREFIX?.trim();
|
|
44
|
+
const symbolScenariosPath = env.GITNEXUS_U2_E2E_SYMBOL_SCENARIOS_PATH?.trim();
|
|
45
|
+
const estimateLowerSec = parseEnvNumber(env.GITNEXUS_U2_E2E_ESTIMATE_LOWER_SEC, 'GITNEXUS_U2_E2E_ESTIMATE_LOWER_SEC');
|
|
46
|
+
const estimateUpperSec = parseEnvNumber(env.GITNEXUS_U2_E2E_ESTIMATE_UPPER_SEC, 'GITNEXUS_U2_E2E_ESTIMATE_UPPER_SEC');
|
|
47
|
+
const hasLower = estimateLowerSec !== undefined;
|
|
48
|
+
const hasUpper = estimateUpperSec !== undefined;
|
|
49
|
+
if (hasLower !== hasUpper) {
|
|
50
|
+
throw new Error('GITNEXUS_U2_E2E_ESTIMATE_LOWER_SEC and GITNEXUS_U2_E2E_ESTIMATE_UPPER_SEC must be set together');
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
...(runIdPrefix ? { runIdPrefix } : {}),
|
|
54
|
+
...(targetPath ? { targetPath } : {}),
|
|
55
|
+
...(repoAliasPrefix ? { repoAliasPrefix } : {}),
|
|
56
|
+
...(symbolScenariosPath ? { symbolScenariosPath } : {}),
|
|
57
|
+
...(hasLower && hasUpper
|
|
58
|
+
? {
|
|
59
|
+
estimateLowerSec: estimateLowerSec,
|
|
60
|
+
estimateUpperSec: estimateUpperSec,
|
|
61
|
+
}
|
|
62
|
+
: {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export async function loadE2EConfig(configPath, env = process.env) {
|
|
66
|
+
const raw = await readJsonFile(configPath);
|
|
67
|
+
const overrides = loadEnvOverrides(env);
|
|
68
|
+
const symbolScenariosPath = overrides.symbolScenariosPath || raw.symbolScenariosPath;
|
|
69
|
+
let symbolScenarios = raw.symbolScenarios || [];
|
|
70
|
+
if (symbolScenariosPath) {
|
|
71
|
+
symbolScenarios = await readJsonFile(symbolScenariosPath);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
runIdPrefix: overrides.runIdPrefix || raw.runIdPrefix,
|
|
75
|
+
targetPath: overrides.targetPath || raw.targetPath,
|
|
76
|
+
repoAliasPrefix: overrides.repoAliasPrefix || raw.repoAliasPrefix,
|
|
77
|
+
scope: raw.scope,
|
|
78
|
+
estimateRangeSec: overrides.estimateLowerSec !== undefined && overrides.estimateUpperSec !== undefined
|
|
79
|
+
? {
|
|
80
|
+
lower: overrides.estimateLowerSec,
|
|
81
|
+
upper: overrides.estimateUpperSec,
|
|
82
|
+
}
|
|
83
|
+
: raw.estimateRangeSec,
|
|
84
|
+
symbolScenarios,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { loadE2EConfig } from './config.js';
|
|
4
|
+
test('loadE2EConfig reads estimate range and 5 symbol scenarios', async () => {
|
|
5
|
+
const config = await loadE2EConfig('benchmarks/u2-e2e/neonspark-full-u2-e2e.config.json');
|
|
6
|
+
assert.equal(config.estimateRangeSec.lower, 322.6);
|
|
7
|
+
assert.equal(config.estimateRangeSec.upper, 540.1);
|
|
8
|
+
assert.equal(config.symbolScenarios.length, 5);
|
|
9
|
+
assert.deepEqual(config.symbolScenarios.map((s) => s.symbol), ['MainUIManager', 'CoinPowerUp', 'GlobalDataAssets', 'AssetRef', 'PlayerActor']);
|
|
10
|
+
});
|
|
11
|
+
test('loadE2EConfig applies env overrides for real-repo gate', async () => {
|
|
12
|
+
const config = await loadE2EConfig('benchmarks/u2-e2e/neonspark-full-u2-e2e.config.json', {
|
|
13
|
+
GITNEXUS_U2_E2E_TARGET_PATH: '/tmp/unity-repo',
|
|
14
|
+
GITNEXUS_U2_E2E_RUN_ID_PREFIX: 'nightly-u3-real',
|
|
15
|
+
GITNEXUS_U2_E2E_REPO_ALIAS_PREFIX: 'neonspark-nightly',
|
|
16
|
+
GITNEXUS_U2_E2E_ESTIMATE_LOWER_SEC: '10.5',
|
|
17
|
+
GITNEXUS_U2_E2E_ESTIMATE_UPPER_SEC: '20.5',
|
|
18
|
+
});
|
|
19
|
+
assert.equal(config.targetPath, '/tmp/unity-repo');
|
|
20
|
+
assert.equal(config.runIdPrefix, 'nightly-u3-real');
|
|
21
|
+
assert.equal(config.repoAliasPrefix, 'neonspark-nightly');
|
|
22
|
+
assert.equal(config.estimateRangeSec.lower, 10.5);
|
|
23
|
+
assert.equal(config.estimateRangeSec.upper, 20.5);
|
|
24
|
+
});
|
|
25
|
+
test('loadE2EConfig rejects half-configured estimate override', async () => {
|
|
26
|
+
await assert.rejects(loadE2EConfig('benchmarks/u2-e2e/neonspark-full-u2-e2e.config.json', {
|
|
27
|
+
GITNEXUS_U2_E2E_ESTIMATE_LOWER_SEC: '10.5',
|
|
28
|
+
}), /must be set together/);
|
|
29
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface StepMetric {
|
|
2
|
+
stepId: string;
|
|
3
|
+
tool: string;
|
|
4
|
+
durationMs: number;
|
|
5
|
+
inputChars: number;
|
|
6
|
+
outputChars: number;
|
|
7
|
+
inputTokensEst: number;
|
|
8
|
+
outputTokensEst: number;
|
|
9
|
+
totalTokensEst: number;
|
|
10
|
+
}
|
|
11
|
+
export interface DurationSummary {
|
|
12
|
+
count: number;
|
|
13
|
+
minMs: number;
|
|
14
|
+
maxMs: number;
|
|
15
|
+
meanMs: number;
|
|
16
|
+
medianMs: number;
|
|
17
|
+
spreadMs: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function estimateTokens(text: string): number;
|
|
20
|
+
export declare function summarizeDurations(values: number[]): DurationSummary;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function round1(value) {
|
|
2
|
+
return Number(value.toFixed(1));
|
|
3
|
+
}
|
|
4
|
+
export function estimateTokens(text) {
|
|
5
|
+
return Math.ceil((text || '').length / 4);
|
|
6
|
+
}
|
|
7
|
+
export function summarizeDurations(values) {
|
|
8
|
+
if (values.length === 0) {
|
|
9
|
+
return {
|
|
10
|
+
count: 0,
|
|
11
|
+
minMs: 0,
|
|
12
|
+
maxMs: 0,
|
|
13
|
+
meanMs: 0,
|
|
14
|
+
medianMs: 0,
|
|
15
|
+
spreadMs: 0,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
19
|
+
const mid = Math.floor(sorted.length / 2);
|
|
20
|
+
const median = sorted.length % 2 === 0
|
|
21
|
+
? (sorted[mid - 1] + sorted[mid]) / 2
|
|
22
|
+
: sorted[mid];
|
|
23
|
+
const min = sorted[0];
|
|
24
|
+
const max = sorted[sorted.length - 1];
|
|
25
|
+
const mean = sorted.reduce((sum, value) => sum + value, 0) / sorted.length;
|
|
26
|
+
return {
|
|
27
|
+
count: sorted.length,
|
|
28
|
+
minMs: round1(min),
|
|
29
|
+
maxMs: round1(max),
|
|
30
|
+
meanMs: round1(mean),
|
|
31
|
+
medianMs: round1(median),
|
|
32
|
+
spreadMs: round1(max - min),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { estimateTokens, summarizeDurations } from './metrics.js';
|
|
4
|
+
test('estimateTokens uses chars-per-token heuristic', () => {
|
|
5
|
+
assert.equal(estimateTokens('1234'), 1);
|
|
6
|
+
assert.equal(estimateTokens('12345'), 2);
|
|
7
|
+
});
|
|
8
|
+
test('summarizeDurations computes median/min/max', () => {
|
|
9
|
+
const out = summarizeDurations([50, 100, 150]);
|
|
10
|
+
assert.equal(out.medianMs, 100);
|
|
11
|
+
assert.equal(out.minMs, 50);
|
|
12
|
+
assert.equal(out.maxMs, 150);
|
|
13
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type E2EGateName = 'preflight' | 'build' | 'pipeline-profile' | 'analyze' | 'estimate-compare' | 'retrieval' | 'final-report';
|
|
2
|
+
export interface E2ERunFailure {
|
|
3
|
+
status: 'failed';
|
|
4
|
+
runId: string;
|
|
5
|
+
reportDir: string;
|
|
6
|
+
completedGates: E2EGateName[];
|
|
7
|
+
failedGate: E2EGateName;
|
|
8
|
+
error: string;
|
|
9
|
+
gateOutputs: Partial<Record<E2EGateName, unknown>>;
|
|
10
|
+
}
|
|
11
|
+
export interface E2ERunSuccess {
|
|
12
|
+
status: 'passed';
|
|
13
|
+
runId: string;
|
|
14
|
+
reportDir: string;
|
|
15
|
+
completedGates: E2EGateName[];
|
|
16
|
+
gateOutputs: Partial<Record<E2EGateName, unknown>>;
|
|
17
|
+
}
|
|
18
|
+
export type E2ERunResult = E2ERunFailure | E2ERunSuccess;
|
|
19
|
+
export interface RunE2EOptions {
|
|
20
|
+
runId?: string;
|
|
21
|
+
reportDir?: string;
|
|
22
|
+
gates?: Partial<Record<E2EGateName, () => Promise<unknown>>>;
|
|
23
|
+
writeCheckpoint?: (reportDir: string, payload: Record<string, unknown>) => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
export interface RunNeonsparkU2E2EOptions {
|
|
26
|
+
configPath: string;
|
|
27
|
+
reportDir?: string;
|
|
28
|
+
runId?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function createRunId(prefix?: string): string;
|
|
31
|
+
export declare function extractSingleCountFromCypherResult(result: unknown): number | null;
|
|
32
|
+
export declare function runNeonsparkU2E2E(options: RunNeonsparkU2E2EOptions): Promise<E2ERunResult>;
|
|
33
|
+
export declare function runE2E(options?: RunE2EOptions): Promise<E2ERunResult>;
|