@veewo/gitnexus 1.3.7 → 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.
Files changed (132) hide show
  1. package/README.md +9 -4
  2. package/dist/benchmark/runner.test.js +1 -1
  3. package/dist/benchmark/u2-e2e/analyze-parser.d.ts +22 -0
  4. package/dist/benchmark/u2-e2e/analyze-parser.js +89 -0
  5. package/dist/benchmark/u2-e2e/analyze-parser.test.d.ts +1 -0
  6. package/dist/benchmark/u2-e2e/analyze-parser.test.js +13 -0
  7. package/dist/benchmark/u2-e2e/characterlist-assetref.d.ts +19 -0
  8. package/dist/benchmark/u2-e2e/characterlist-assetref.js +80 -0
  9. package/dist/benchmark/u2-e2e/characterlist-assetref.test.d.ts +1 -0
  10. package/dist/benchmark/u2-e2e/characterlist-assetref.test.js +108 -0
  11. package/dist/benchmark/u2-e2e/config.d.ts +25 -0
  12. package/dist/benchmark/u2-e2e/config.js +86 -0
  13. package/dist/benchmark/u2-e2e/config.test.d.ts +1 -0
  14. package/dist/benchmark/u2-e2e/config.test.js +29 -0
  15. package/dist/benchmark/u2-e2e/metrics.d.ts +20 -0
  16. package/dist/benchmark/u2-e2e/metrics.js +34 -0
  17. package/dist/benchmark/u2-e2e/metrics.test.d.ts +1 -0
  18. package/dist/benchmark/u2-e2e/metrics.test.js +13 -0
  19. package/dist/benchmark/u2-e2e/neonspark-full-e2e.d.ts +33 -0
  20. package/dist/benchmark/u2-e2e/neonspark-full-e2e.js +439 -0
  21. package/dist/benchmark/u2-e2e/neonspark-full-e2e.test.d.ts +1 -0
  22. package/dist/benchmark/u2-e2e/neonspark-full-e2e.test.js +40 -0
  23. package/dist/benchmark/u2-e2e/report.d.ts +58 -0
  24. package/dist/benchmark/u2-e2e/report.js +130 -0
  25. package/dist/benchmark/u2-e2e/report.test.d.ts +1 -0
  26. package/dist/benchmark/u2-e2e/report.test.js +58 -0
  27. package/dist/benchmark/u2-e2e/retrieval-runner.d.ts +21 -0
  28. package/dist/benchmark/u2-e2e/retrieval-runner.js +166 -0
  29. package/dist/benchmark/u2-e2e/retrieval-runner.test.d.ts +1 -0
  30. package/dist/benchmark/u2-e2e/retrieval-runner.test.js +145 -0
  31. package/dist/benchmark/u2-performance-sampler.d.ts +33 -0
  32. package/dist/benchmark/u2-performance-sampler.js +178 -0
  33. package/dist/benchmark/u2-performance-sampler.test.d.ts +1 -0
  34. package/dist/benchmark/u2-performance-sampler.test.js +34 -0
  35. package/dist/cli/analyze-custom-modules-regression.test.d.ts +1 -0
  36. package/dist/cli/analyze-custom-modules-regression.test.js +75 -0
  37. package/dist/cli/analyze-modules-diagnostics.test.d.ts +1 -0
  38. package/dist/cli/analyze-modules-diagnostics.test.js +36 -0
  39. package/dist/cli/analyze-multi-scope-regression.test.js +10 -0
  40. package/dist/cli/analyze-summary.d.ts +7 -0
  41. package/dist/cli/analyze-summary.js +37 -0
  42. package/dist/cli/analyze-summary.test.d.ts +1 -0
  43. package/dist/cli/analyze-summary.test.js +58 -0
  44. package/dist/cli/analyze.js +11 -6
  45. package/dist/cli/benchmark-u2-e2e.d.ts +9 -0
  46. package/dist/cli/benchmark-u2-e2e.js +35 -0
  47. package/dist/cli/benchmark-u2-e2e.test.d.ts +1 -0
  48. package/dist/cli/benchmark-u2-e2e.test.js +7 -0
  49. package/dist/cli/index.js +20 -0
  50. package/dist/cli/setup.js +24 -3
  51. package/dist/cli/setup.test.js +6 -4
  52. package/dist/cli/tool.d.ts +3 -0
  53. package/dist/cli/tool.js +2 -0
  54. package/dist/cli/unity-bindings.d.ts +8 -0
  55. package/dist/cli/unity-bindings.js +33 -0
  56. package/dist/cli/unity-bindings.test.d.ts +1 -0
  57. package/dist/cli/unity-bindings.test.js +24 -0
  58. package/dist/core/graph/types.d.ts +1 -1
  59. package/dist/core/ingestion/modules/assignment-engine.d.ts +33 -0
  60. package/dist/core/ingestion/modules/assignment-engine.js +179 -0
  61. package/dist/core/ingestion/modules/assignment-engine.test.d.ts +1 -0
  62. package/dist/core/ingestion/modules/assignment-engine.test.js +111 -0
  63. package/dist/core/ingestion/modules/config-loader.d.ts +2 -0
  64. package/dist/core/ingestion/modules/config-loader.js +186 -0
  65. package/dist/core/ingestion/modules/config-loader.test.d.ts +1 -0
  66. package/dist/core/ingestion/modules/config-loader.test.js +57 -0
  67. package/dist/core/ingestion/modules/rule-matcher.d.ts +12 -0
  68. package/dist/core/ingestion/modules/rule-matcher.js +63 -0
  69. package/dist/core/ingestion/modules/rule-matcher.test.d.ts +1 -0
  70. package/dist/core/ingestion/modules/rule-matcher.test.js +58 -0
  71. package/dist/core/ingestion/modules/types.d.ts +44 -0
  72. package/dist/core/ingestion/modules/types.js +2 -0
  73. package/dist/core/ingestion/pipeline.d.ts +2 -4
  74. package/dist/core/ingestion/pipeline.js +12 -0
  75. package/dist/core/ingestion/unity-resource-processor.d.ts +26 -0
  76. package/dist/core/ingestion/unity-resource-processor.js +363 -0
  77. package/dist/core/ingestion/unity-resource-processor.test.d.ts +1 -0
  78. package/dist/core/ingestion/unity-resource-processor.test.js +599 -0
  79. package/dist/core/kuzu/kuzu-adapter.d.ts +6 -0
  80. package/dist/core/kuzu/kuzu-adapter.js +18 -7
  81. package/dist/core/kuzu/schema.d.ts +2 -2
  82. package/dist/core/kuzu/schema.js +22 -1
  83. package/dist/core/kuzu/schema.test.d.ts +1 -0
  84. package/dist/core/kuzu/schema.test.js +17 -0
  85. package/dist/core/unity/meta-index.d.ts +5 -0
  86. package/dist/core/unity/meta-index.js +113 -0
  87. package/dist/core/unity/meta-index.test.d.ts +1 -0
  88. package/dist/core/unity/meta-index.test.js +11 -0
  89. package/dist/core/unity/options.d.ts +2 -0
  90. package/dist/core/unity/options.js +9 -0
  91. package/dist/core/unity/options.test.d.ts +1 -0
  92. package/dist/core/unity/options.test.js +10 -0
  93. package/dist/core/unity/override-merger.d.ts +27 -0
  94. package/dist/core/unity/override-merger.js +35 -0
  95. package/dist/core/unity/override-merger.test.d.ts +1 -0
  96. package/dist/core/unity/override-merger.test.js +47 -0
  97. package/dist/core/unity/resolver.d.ts +79 -0
  98. package/dist/core/unity/resolver.js +384 -0
  99. package/dist/core/unity/resolver.test.d.ts +1 -0
  100. package/dist/core/unity/resolver.test.js +244 -0
  101. package/dist/core/unity/resource-hit-scanner.d.ts +10 -0
  102. package/dist/core/unity/resource-hit-scanner.js +60 -0
  103. package/dist/core/unity/resource-hit-scanner.test.d.ts +1 -0
  104. package/dist/core/unity/resource-hit-scanner.test.js +20 -0
  105. package/dist/core/unity/scan-context.d.ts +23 -0
  106. package/dist/core/unity/scan-context.js +318 -0
  107. package/dist/core/unity/scan-context.test.d.ts +1 -0
  108. package/dist/core/unity/scan-context.test.js +118 -0
  109. package/dist/core/unity/serialized-type-index.d.ts +10 -0
  110. package/dist/core/unity/serialized-type-index.js +105 -0
  111. package/dist/core/unity/serialized-type-index.test.d.ts +1 -0
  112. package/dist/core/unity/serialized-type-index.test.js +34 -0
  113. package/dist/core/unity/u2-thresholds.test.d.ts +1 -0
  114. package/dist/core/unity/u2-thresholds.test.js +71 -0
  115. package/dist/core/unity/yaml-object-graph.d.ts +9 -0
  116. package/dist/core/unity/yaml-object-graph.js +92 -0
  117. package/dist/core/unity/yaml-object-graph.test.d.ts +1 -0
  118. package/dist/core/unity/yaml-object-graph.test.js +49 -0
  119. package/dist/mcp/local/cluster-aggregation.d.ts +20 -0
  120. package/dist/mcp/local/cluster-aggregation.js +48 -0
  121. package/dist/mcp/local/cluster-aggregation.test.d.ts +1 -0
  122. package/dist/mcp/local/cluster-aggregation.test.js +22 -0
  123. package/dist/mcp/local/local-backend.js +12 -1
  124. package/dist/mcp/local/unity-enrichment.d.ts +6 -0
  125. package/dist/mcp/local/unity-enrichment.js +91 -0
  126. package/dist/mcp/local/unity-enrichment.test.d.ts +1 -0
  127. package/dist/mcp/local/unity-enrichment.test.js +130 -0
  128. package/dist/mcp/tools.js +12 -0
  129. package/dist/types/pipeline.d.ts +7 -0
  130. package/dist/types/pipeline.js +2 -0
  131. package/hooks/check-release-path-hygiene.mjs +108 -0
  132. package/package.json +14 -7
package/README.md CHANGED
@@ -70,7 +70,7 @@ If you prefer to configure manually instead of using `gitnexus setup`:
70
70
  ### Claude Code (full support — MCP + skills + hooks)
71
71
 
72
72
  ```bash
73
- claude mcp add gitnexus -- npx -y gitnexus@latest mcp
73
+ claude mcp add gitnexus -- npx -y @veewo/gitnexus@latest mcp
74
74
  ```
75
75
 
76
76
  ### Cursor / Windsurf
@@ -82,7 +82,7 @@ Add to `~/.cursor/mcp.json` (global — works for all projects):
82
82
  "mcpServers": {
83
83
  "gitnexus": {
84
84
  "command": "npx",
85
- "args": ["-y", "gitnexus@latest", "mcp"]
85
+ "args": ["-y", "@veewo/gitnexus@latest", "mcp"]
86
86
  }
87
87
  }
88
88
  }
@@ -97,7 +97,7 @@ Add to `~/.config/opencode/opencode.json`:
97
97
  "mcp": {
98
98
  "gitnexus": {
99
99
  "type": "local",
100
- "command": ["npx", "-y", "gitnexus@latest", "mcp"]
100
+ "command": ["npx", "-y", "@veewo/gitnexus@latest", "mcp"]
101
101
  }
102
102
  }
103
103
  }
@@ -106,7 +106,7 @@ Add to `~/.config/opencode/opencode.json`:
106
106
  ### Codex
107
107
 
108
108
  ```bash
109
- codex mcp add gitnexus -- npx -y gitnexus@latest mcp
109
+ codex mcp add gitnexus -- npx -y @veewo/gitnexus@latest mcp
110
110
  ```
111
111
 
112
112
  ## How It Works
@@ -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: '/Volumes/Shuttle/unity-projects/neonspark',
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,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>;