@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.
Files changed (133) hide show
  1. package/README.md +5 -0
  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.js +13 -0
  6. package/dist/benchmark/u2-e2e/characterlist-assetref.d.ts +19 -0
  7. package/dist/benchmark/u2-e2e/characterlist-assetref.js +80 -0
  8. package/dist/benchmark/u2-e2e/characterlist-assetref.test.js +108 -0
  9. package/dist/benchmark/u2-e2e/config.d.ts +25 -0
  10. package/dist/benchmark/u2-e2e/config.js +86 -0
  11. package/dist/benchmark/u2-e2e/config.test.js +29 -0
  12. package/dist/benchmark/u2-e2e/metrics.d.ts +20 -0
  13. package/dist/benchmark/u2-e2e/metrics.js +34 -0
  14. package/dist/benchmark/u2-e2e/metrics.test.js +13 -0
  15. package/dist/benchmark/u2-e2e/neonspark-full-e2e.d.ts +33 -0
  16. package/dist/benchmark/u2-e2e/neonspark-full-e2e.js +439 -0
  17. package/dist/benchmark/u2-e2e/neonspark-full-e2e.test.js +40 -0
  18. package/dist/benchmark/u2-e2e/report.d.ts +58 -0
  19. package/dist/benchmark/u2-e2e/report.js +130 -0
  20. package/dist/benchmark/u2-e2e/report.test.js +58 -0
  21. package/dist/benchmark/u2-e2e/retrieval-runner.d.ts +21 -0
  22. package/dist/benchmark/u2-e2e/retrieval-runner.js +166 -0
  23. package/dist/benchmark/u2-e2e/retrieval-runner.test.d.ts +1 -0
  24. package/dist/benchmark/u2-e2e/retrieval-runner.test.js +145 -0
  25. package/dist/benchmark/u2-performance-sampler.d.ts +33 -0
  26. package/dist/benchmark/u2-performance-sampler.js +178 -0
  27. package/dist/benchmark/u2-performance-sampler.test.d.ts +1 -0
  28. package/dist/benchmark/u2-performance-sampler.test.js +34 -0
  29. package/dist/cli/ai-context.js +1 -1
  30. package/dist/cli/analyze-multi-scope-regression.test.js +10 -0
  31. package/dist/cli/analyze-options.d.ts +19 -0
  32. package/dist/cli/analyze-options.js +35 -0
  33. package/dist/cli/analyze-options.test.js +42 -1
  34. package/dist/cli/analyze-summary.d.ts +7 -0
  35. package/dist/cli/analyze-summary.js +37 -0
  36. package/dist/cli/analyze-summary.test.d.ts +1 -0
  37. package/dist/cli/analyze-summary.test.js +58 -0
  38. package/dist/cli/analyze.d.ts +1 -0
  39. package/dist/cli/analyze.js +64 -32
  40. package/dist/cli/benchmark-u2-e2e.d.ts +9 -0
  41. package/dist/cli/benchmark-u2-e2e.js +35 -0
  42. package/dist/cli/benchmark-u2-e2e.test.d.ts +1 -0
  43. package/dist/cli/benchmark-u2-e2e.test.js +7 -0
  44. package/dist/cli/index.js +21 -0
  45. package/dist/cli/repo-manager-alias.test.js +24 -1
  46. package/dist/cli/tool.d.ts +3 -0
  47. package/dist/cli/tool.js +2 -0
  48. package/dist/cli/unity-bindings.d.ts +8 -0
  49. package/dist/cli/unity-bindings.js +33 -0
  50. package/dist/cli/unity-bindings.test.d.ts +1 -0
  51. package/dist/cli/unity-bindings.test.js +24 -0
  52. package/dist/core/graph/types.d.ts +1 -1
  53. package/dist/core/ingestion/pipeline.d.ts +2 -4
  54. package/dist/core/ingestion/pipeline.js +12 -0
  55. package/dist/core/ingestion/unity-resource-processor.d.ts +26 -0
  56. package/dist/core/ingestion/unity-resource-processor.js +363 -0
  57. package/dist/core/ingestion/unity-resource-processor.test.d.ts +1 -0
  58. package/dist/core/ingestion/unity-resource-processor.test.js +599 -0
  59. package/dist/core/kuzu/kuzu-adapter.d.ts +6 -0
  60. package/dist/core/kuzu/kuzu-adapter.js +18 -7
  61. package/dist/core/kuzu/schema.d.ts +2 -2
  62. package/dist/core/kuzu/schema.js +22 -1
  63. package/dist/core/kuzu/schema.test.d.ts +1 -0
  64. package/dist/core/kuzu/schema.test.js +17 -0
  65. package/dist/core/unity/meta-index.d.ts +5 -0
  66. package/dist/core/unity/meta-index.js +113 -0
  67. package/dist/core/unity/meta-index.test.d.ts +1 -0
  68. package/dist/core/unity/meta-index.test.js +11 -0
  69. package/dist/core/unity/options.d.ts +2 -0
  70. package/dist/core/unity/options.js +9 -0
  71. package/dist/core/unity/options.test.d.ts +1 -0
  72. package/dist/core/unity/options.test.js +10 -0
  73. package/dist/core/unity/override-merger.d.ts +27 -0
  74. package/dist/core/unity/override-merger.js +35 -0
  75. package/dist/core/unity/override-merger.test.d.ts +1 -0
  76. package/dist/core/unity/override-merger.test.js +47 -0
  77. package/dist/core/unity/resolver.d.ts +79 -0
  78. package/dist/core/unity/resolver.js +384 -0
  79. package/dist/core/unity/resolver.test.d.ts +1 -0
  80. package/dist/core/unity/resolver.test.js +244 -0
  81. package/dist/core/unity/resource-hit-scanner.d.ts +10 -0
  82. package/dist/core/unity/resource-hit-scanner.js +60 -0
  83. package/dist/core/unity/resource-hit-scanner.test.d.ts +1 -0
  84. package/dist/core/unity/resource-hit-scanner.test.js +20 -0
  85. package/dist/core/unity/scan-context.d.ts +23 -0
  86. package/dist/core/unity/scan-context.js +318 -0
  87. package/dist/core/unity/scan-context.test.d.ts +1 -0
  88. package/dist/core/unity/scan-context.test.js +118 -0
  89. package/dist/core/unity/serialized-type-index.d.ts +10 -0
  90. package/dist/core/unity/serialized-type-index.js +105 -0
  91. package/dist/core/unity/serialized-type-index.test.d.ts +1 -0
  92. package/dist/core/unity/serialized-type-index.test.js +34 -0
  93. package/dist/core/unity/u2-thresholds.test.d.ts +1 -0
  94. package/dist/core/unity/u2-thresholds.test.js +71 -0
  95. package/dist/core/unity/yaml-object-graph.d.ts +9 -0
  96. package/dist/core/unity/yaml-object-graph.js +92 -0
  97. package/dist/core/unity/yaml-object-graph.test.d.ts +1 -0
  98. package/dist/core/unity/yaml-object-graph.test.js +49 -0
  99. package/dist/mcp/local/local-backend.js +12 -1
  100. package/dist/mcp/local/unity-enrichment.d.ts +6 -0
  101. package/dist/mcp/local/unity-enrichment.js +91 -0
  102. package/dist/mcp/local/unity-enrichment.test.d.ts +1 -0
  103. package/dist/mcp/local/unity-enrichment.test.js +130 -0
  104. package/dist/mcp/resources.js +1 -1
  105. package/dist/mcp/staleness.js +1 -1
  106. package/dist/mcp/tools.js +12 -0
  107. package/dist/storage/repo-manager.d.ts +6 -0
  108. package/dist/types/pipeline.d.ts +7 -0
  109. package/dist/types/pipeline.js +2 -0
  110. package/hooks/check-release-path-hygiene.mjs +108 -0
  111. package/package.json +16 -9
  112. package/dist/cli/analyze-custom-modules-regression.test.js +0 -75
  113. package/dist/cli/analyze-modules-diagnostics.test.js +0 -36
  114. package/dist/core/ingestion/modules/assignment-engine.d.ts +0 -33
  115. package/dist/core/ingestion/modules/assignment-engine.js +0 -179
  116. package/dist/core/ingestion/modules/assignment-engine.test.js +0 -111
  117. package/dist/core/ingestion/modules/config-loader.d.ts +0 -2
  118. package/dist/core/ingestion/modules/config-loader.js +0 -186
  119. package/dist/core/ingestion/modules/config-loader.test.js +0 -57
  120. package/dist/core/ingestion/modules/rule-matcher.d.ts +0 -12
  121. package/dist/core/ingestion/modules/rule-matcher.js +0 -63
  122. package/dist/core/ingestion/modules/rule-matcher.test.js +0 -58
  123. package/dist/core/ingestion/modules/types.d.ts +0 -44
  124. package/dist/core/ingestion/modules/types.js +0 -2
  125. package/dist/mcp/local/cluster-aggregation.d.ts +0 -20
  126. package/dist/mcp/local/cluster-aggregation.js +0 -48
  127. package/dist/mcp/local/cluster-aggregation.test.js +0 -22
  128. /package/dist/{cli/analyze-custom-modules-regression.test.d.ts → benchmark/u2-e2e/analyze-parser.test.d.ts} +0 -0
  129. /package/dist/{cli/analyze-modules-diagnostics.test.d.ts → benchmark/u2-e2e/characterlist-assetref.test.d.ts} +0 -0
  130. /package/dist/{core/ingestion/modules/assignment-engine.test.d.ts → benchmark/u2-e2e/config.test.d.ts} +0 -0
  131. /package/dist/{core/ingestion/modules/config-loader.test.d.ts → benchmark/u2-e2e/metrics.test.d.ts} +0 -0
  132. /package/dist/{core/ingestion/modules/rule-matcher.test.d.ts → benchmark/u2-e2e/neonspark-full-e2e.test.d.ts} +0 -0
  133. /package/dist/{mcp/local/cluster-aggregation.test.d.ts → benchmark/u2-e2e/report.test.d.ts} +0 -0
@@ -0,0 +1,34 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { buildSerializableTypeIndexFromSources } from './serialized-type-index.js';
4
+ test('buildSerializableTypeIndex extracts serializable symbols and host field declared types', () => {
5
+ const index = buildSerializableTypeIndexFromSources([
6
+ {
7
+ filePath: 'Assets/Scripts/AssetRef.cs',
8
+ content: `
9
+ [System.Serializable]
10
+ public class AssetRef { public string guid; }
11
+ `,
12
+ },
13
+ {
14
+ filePath: 'Assets/Scripts/InventoryConfig.cs',
15
+ content: `
16
+ using UnityEngine;
17
+ using System.Collections.Generic;
18
+ public class InventoryConfig : ScriptableObject {
19
+ public AssetRef icon;
20
+ public AssetRef<GameObject> iconPrefab;
21
+ [SerializeField] private List<AssetRef> drops;
22
+ [SerializeField] private List<AssetRef<Sprite>> iconVariants;
23
+ [SerializeField] private int ignored;
24
+ }
25
+ `,
26
+ },
27
+ ]);
28
+ assert.equal(index.serializableSymbols.has('AssetRef'), true);
29
+ assert.equal(index.hostFieldTypeHints.get('InventoryConfig')?.get('icon'), 'AssetRef');
30
+ assert.equal(index.hostFieldTypeHints.get('InventoryConfig')?.get('iconPrefab'), 'AssetRef');
31
+ assert.equal(index.hostFieldTypeHints.get('InventoryConfig')?.get('drops'), 'AssetRef');
32
+ assert.equal(index.hostFieldTypeHints.get('InventoryConfig')?.get('iconVariants'), 'AssetRef');
33
+ assert.equal(index.hostFieldTypeHints.get('InventoryConfig')?.has('ignored'), false);
34
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { resolveUnityBindings } from './resolver.js';
6
+ import { buildUnityScanContext } from './scan-context.js';
7
+ const here = path.dirname(fileURLToPath(import.meta.url));
8
+ const fixtureRoot = path.resolve(here, '../../../src/core/unity/__fixtures__/mini-unity');
9
+ const U2_REFERENCE_RESOLUTION_RATE_THRESHOLD = 0.8;
10
+ const U2_LIST_REFERENCE_PARSE_RATE_THRESHOLD = 1.0;
11
+ function countListCandidatesFromScalarFields(scalarFields) {
12
+ let total = 0;
13
+ for (const field of scalarFields) {
14
+ const lines = field.value.split(/\r?\n/);
15
+ for (const line of lines) {
16
+ if (/^\s*-\s*\{[^}]*fileID\s*:/.test(line)) {
17
+ total += 1;
18
+ }
19
+ }
20
+ }
21
+ return total;
22
+ }
23
+ function countResolvedSuccesses(references) {
24
+ return references.filter((ref) => ref.resolution === 'local-object' || ref.resolution === 'external-asset').length;
25
+ }
26
+ function collectU2ReferenceStats(results) {
27
+ let candidateRefs = 0;
28
+ let resolvedRefs = 0;
29
+ let listCandidates = 0;
30
+ let parsedListRefs = 0;
31
+ for (const result of results) {
32
+ for (const binding of result.resourceBindings) {
33
+ const listCandidatesForBinding = countListCandidatesFromScalarFields(binding.serializedFields.scalarFields);
34
+ const directCandidatesForBinding = binding.serializedFields.referenceFields.length;
35
+ candidateRefs += directCandidatesForBinding + listCandidatesForBinding;
36
+ listCandidates += listCandidatesForBinding;
37
+ resolvedRefs += countResolvedSuccesses(binding.resolvedReferences);
38
+ parsedListRefs += binding.resolvedReferences.filter((ref) => ref.fromList).length;
39
+ }
40
+ }
41
+ return { candidateRefs, resolvedRefs, listCandidates, parsedListRefs };
42
+ }
43
+ test('U2 threshold: reference_resolution_rate stays above baseline', async () => {
44
+ const scanContext = await buildUnityScanContext({
45
+ repoRoot: fixtureRoot,
46
+ scopedPaths: [
47
+ 'Assets/Scripts/MainUIManager.cs',
48
+ 'Assets/Scripts/MainUIManager.cs.meta',
49
+ 'Assets/Scene/MainUIManager.unity',
50
+ 'Assets/Config/MainUIDocument.asset.meta',
51
+ 'Assets/Scripts/MenuScreenCarrier.cs',
52
+ 'Assets/Scripts/MenuScreenCarrier.cs.meta',
53
+ 'Assets/Prefabs/MenuScreenCarrier.prefab',
54
+ ],
55
+ });
56
+ const [mainUIManager, menuScreenCarrier] = await Promise.all([
57
+ resolveUnityBindings({ repoRoot: fixtureRoot, symbol: 'MainUIManager', scanContext }),
58
+ resolveUnityBindings({ repoRoot: fixtureRoot, symbol: 'MenuScreenCarrier', scanContext }),
59
+ ]);
60
+ const stats = collectU2ReferenceStats([mainUIManager, menuScreenCarrier]);
61
+ assert.ok(stats.candidateRefs > 0, 'candidate reference count must be > 0');
62
+ const referenceResolutionRate = stats.resolvedRefs / stats.candidateRefs;
63
+ assert.ok(referenceResolutionRate >= U2_REFERENCE_RESOLUTION_RATE_THRESHOLD, `reference_resolution_rate=${referenceResolutionRate.toFixed(3)} below threshold=${U2_REFERENCE_RESOLUTION_RATE_THRESHOLD.toFixed(3)} (resolved=${stats.resolvedRefs}, candidates=${stats.candidateRefs})`);
64
+ });
65
+ test('U2 threshold: list_reference_parse_rate stays above baseline', async () => {
66
+ const result = await resolveUnityBindings({ repoRoot: fixtureRoot, symbol: 'MenuScreenCarrier' });
67
+ const stats = collectU2ReferenceStats([result]);
68
+ assert.ok(stats.listCandidates > 0, 'list reference candidate count must be > 0');
69
+ const listReferenceParseRate = stats.parsedListRefs / stats.listCandidates;
70
+ assert.ok(listReferenceParseRate >= U2_LIST_REFERENCE_PARSE_RATE_THRESHOLD, `list_reference_parse_rate=${listReferenceParseRate.toFixed(3)} below threshold=${U2_LIST_REFERENCE_PARSE_RATE_THRESHOLD.toFixed(3)} (parsed=${stats.parsedListRefs}, candidates=${stats.listCandidates})`);
71
+ });
@@ -0,0 +1,9 @@
1
+ export type UnityObjectType = 'MonoBehaviour' | 'PrefabInstance' | 'GameObject';
2
+ export interface UnityObjectBlock {
3
+ objectId: string;
4
+ objectType: UnityObjectType;
5
+ stripped: boolean;
6
+ fields: Record<string, string>;
7
+ rawBody: string;
8
+ }
9
+ export declare function parseUnityYamlObjects(text: string): UnityObjectBlock[];
@@ -0,0 +1,92 @@
1
+ const SUPPORTED_TYPES = new Set(['MonoBehaviour', 'PrefabInstance', 'GameObject']);
2
+ export function parseUnityYamlObjects(text) {
3
+ const lines = text.replace(/\r\n/g, '\n').split('\n');
4
+ const blocks = [];
5
+ let currentHeader = null;
6
+ let currentBody = [];
7
+ const flush = () => {
8
+ if (!currentHeader)
9
+ return;
10
+ const objectType = findObjectType(currentBody);
11
+ if (!objectType) {
12
+ currentHeader = null;
13
+ currentBody = [];
14
+ return;
15
+ }
16
+ blocks.push({
17
+ objectId: currentHeader.objectId,
18
+ objectType,
19
+ stripped: currentHeader.stripped,
20
+ fields: parseFields(currentBody),
21
+ rawBody: currentBody.join('\n'),
22
+ });
23
+ currentHeader = null;
24
+ currentBody = [];
25
+ };
26
+ for (const line of lines) {
27
+ const header = line.match(/^--- !u!\d+ &(\d+)(?:\s+(\w+))?\s*$/);
28
+ if (header) {
29
+ flush();
30
+ currentHeader = {
31
+ objectId: header[1],
32
+ stripped: header[2] === 'stripped',
33
+ };
34
+ continue;
35
+ }
36
+ if (currentHeader) {
37
+ currentBody.push(line);
38
+ }
39
+ }
40
+ flush();
41
+ return blocks;
42
+ }
43
+ function findObjectType(lines) {
44
+ for (const line of lines) {
45
+ const match = line.match(/^(MonoBehaviour|PrefabInstance|GameObject):\s*$/);
46
+ if (match && SUPPORTED_TYPES.has(match[1])) {
47
+ return match[1];
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+ function parseFields(lines) {
53
+ const fields = {};
54
+ const rootIndex = lines.findIndex((line) => /^(MonoBehaviour|PrefabInstance|GameObject):\s*$/.test(line));
55
+ if (rootIndex === -1)
56
+ return fields;
57
+ let currentKey = null;
58
+ let currentValue = [];
59
+ const flush = () => {
60
+ if (!currentKey)
61
+ return;
62
+ fields[currentKey] = currentValue.join('\n').trim();
63
+ currentKey = null;
64
+ currentValue = [];
65
+ };
66
+ for (const line of lines.slice(rootIndex + 1)) {
67
+ if (!line.trim()) {
68
+ if (currentKey)
69
+ currentValue.push('');
70
+ continue;
71
+ }
72
+ const topLevelMatch = line.match(/^ {2}(?!-)([^ ][^:]*):(.*)$/);
73
+ if (topLevelMatch) {
74
+ flush();
75
+ currentKey = topLevelMatch[1].trim();
76
+ const inlineValue = topLevelMatch[2].trim();
77
+ if (inlineValue)
78
+ currentValue.push(inlineValue);
79
+ continue;
80
+ }
81
+ if (currentKey) {
82
+ currentValue.push(stripCommonIndent(line));
83
+ }
84
+ }
85
+ flush();
86
+ return fields;
87
+ }
88
+ function stripCommonIndent(line) {
89
+ if (line.startsWith(' '))
90
+ return line.slice(4);
91
+ return line.trimStart();
92
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { parseUnityYamlObjects } from './yaml-object-graph.js';
4
+ const sampleYaml = `--- !u!1 &1000
5
+ GameObject:
6
+ m_Name: MainUIRoot
7
+ --- !u!114 &11400000 stripped
8
+ MonoBehaviour:
9
+ m_CorrespondingSourceObject: {fileID: 11400000, guid: abcdef0123456789abcdef0123456789, type: 3}
10
+ m_PrefabInstance: {fileID: 2000}
11
+ needPause: 0
12
+ mainUIDocument: {fileID: 11400000, guid: fedcba9876543210fedcba9876543210, type: 2}
13
+ --- !u!1001 &2000
14
+ PrefabInstance:
15
+ m_Modification:
16
+ m_Modifications:
17
+ - target: {fileID: 11400000}
18
+ propertyPath: needPause
19
+ value: 1
20
+ objectReference: {fileID: 0}
21
+ `;
22
+ test('parseUnityYamlObjects parses stripped MonoBehaviour and PrefabInstance blocks', () => {
23
+ const blocks = parseUnityYamlObjects(sampleYaml);
24
+ assert.equal(blocks.length, 3);
25
+ assert.deepEqual(blocks.map((block) => ({ id: block.objectId, type: block.objectType, stripped: block.stripped })), [
26
+ { id: '1000', type: 'GameObject', stripped: false },
27
+ { id: '11400000', type: 'MonoBehaviour', stripped: true },
28
+ { id: '2000', type: 'PrefabInstance', stripped: false },
29
+ ]);
30
+ assert.equal(blocks[1].fields.needPause, '0');
31
+ assert.match(blocks[1].fields.mainUIDocument, /fileID: 11400000/);
32
+ assert.match(blocks[2].fields.m_Modification, /propertyPath: needPause/);
33
+ });
34
+ test('parseUnityYamlObjects keeps inline list entries under their parent field', () => {
35
+ const yamlWithInlineList = `--- !u!114 &11400001
36
+ MonoBehaviour:
37
+ buttonMappings:
38
+ - {fileID: 11400000, guid: fedcba9876543210fedcba9876543210, type: 2}
39
+ - {fileID: 0}
40
+ needPause: 0
41
+ `;
42
+ const blocks = parseUnityYamlObjects(yamlWithInlineList);
43
+ assert.equal(blocks.length, 1);
44
+ const mono = blocks[0];
45
+ assert.equal(mono.objectType, 'MonoBehaviour');
46
+ assert.ok(mono.fields.buttonMappings.includes('- {fileID: 11400000'));
47
+ assert.equal(mono.fields['- {fileID'], undefined);
48
+ assert.equal(mono.fields.needPause, '0');
49
+ });
@@ -8,6 +8,8 @@
8
8
  import fs from 'fs/promises';
9
9
  import path from 'path';
10
10
  import { initKuzu, executeQuery, closeKuzu, isKuzuReady } from '../core/kuzu-adapter.js';
11
+ import { parseUnityResourcesMode } from '../../core/unity/options.js';
12
+ import { loadUnityContext } from './unity-enrichment.js';
11
13
  // Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
12
14
  // at MCP server startup — crashes on unsupported Node ABI versions (#89)
13
15
  // git utilities available if needed
@@ -285,6 +287,7 @@ export class LocalBackend {
285
287
  const processLimit = params.limit || 5;
286
288
  const maxSymbolsPerProcess = params.max_symbols || 10;
287
289
  const includeContent = params.include_content ?? false;
290
+ const unityResourcesMode = parseUnityResourcesMode(params.unity_resources);
288
291
  const searchQuery = params.query.trim();
289
292
  // Step 1: Run hybrid search to get matching symbols
290
293
  const searchLimit = processLimit * maxSymbolsPerProcess; // fetch enough raw results
@@ -383,6 +386,9 @@ export class LocalBackend {
383
386
  endLine: sym.endLine,
384
387
  ...(module ? { module } : {}),
385
388
  ...(includeContent && content ? { content } : {}),
389
+ ...((unityResourcesMode !== 'off' && sym.nodeId && sym.type === 'Class')
390
+ ? await loadUnityContext(repo.id, sym.nodeId, (query) => executeQuery(repo.id, query))
391
+ : {}),
386
392
  };
387
393
  if (processRows.length === 0) {
388
394
  // Symbol not in any process — goes to definitions
@@ -796,6 +802,7 @@ export class LocalBackend {
796
802
  async context(repo, params) {
797
803
  await this.ensureInitialized(repo.id);
798
804
  const { name, uid, file_path, include_content } = params;
805
+ const unityResourcesMode = parseUnityResourcesMode(params.unity_resources);
799
806
  if (!name && !uid) {
800
807
  return { error: 'Either "name" or "uid" parameter is required.' };
801
808
  }
@@ -948,7 +955,7 @@ export class LocalBackend {
948
955
  }
949
956
  return cats;
950
957
  };
951
- return {
958
+ const result = {
952
959
  status: 'found',
953
960
  symbol: {
954
961
  uid: getRowValue(sym, 'id', 0),
@@ -968,6 +975,10 @@ export class LocalBackend {
968
975
  step_count: getRowValue(r, 'stepCount', 3),
969
976
  })),
970
977
  };
978
+ if (unityResourcesMode !== 'off' && symNodeId && symKind === 'Class') {
979
+ Object.assign(result, await loadUnityContext(repo.id, symNodeId, (query) => executeQuery(repo.id, query)));
980
+ }
981
+ return result;
971
982
  }
972
983
  /**
973
984
  * Legacy explore — kept for backwards compatibility with resources.ts.
@@ -0,0 +1,6 @@
1
+ import type { ResolveOutput } from '../../core/unity/resolver.js';
2
+ export interface UnityContextPayload extends Pick<ResolveOutput, 'resourceBindings' | 'serializedFields' | 'unityDiagnostics'> {
3
+ }
4
+ export type ExecuteQuery = (query: string) => Promise<any[]>;
5
+ export declare function loadUnityContext(_repoId: string, symbolId: string, execute: ExecuteQuery): Promise<UnityContextPayload>;
6
+ export declare function projectUnityBindings(rows: any[]): UnityContextPayload;
@@ -0,0 +1,91 @@
1
+ import { extractAssetRefPathReferences } from '../../core/unity/resolver.js';
2
+ export async function loadUnityContext(_repoId, symbolId, execute) {
3
+ const escapedSymbolId = symbolId.replace(/'/g, "''");
4
+ const rows = await execute(`
5
+ MATCH (symbol {id: '${escapedSymbolId}'})-[r:CodeRelation]->(component:CodeElement)
6
+ WHERE r.type IN ['UNITY_COMPONENT_INSTANCE', 'UNITY_SERIALIZED_TYPE_IN']
7
+ RETURN component.filePath AS resourcePath, component.description AS payload, r.type AS relationType, r.reason AS relationReason
8
+ ORDER BY component.filePath, component.id
9
+ `);
10
+ return projectUnityBindings(rows);
11
+ }
12
+ export function projectUnityBindings(rows) {
13
+ const resourceBindings = [];
14
+ const scalarFields = [];
15
+ const referenceFields = [];
16
+ const unityDiagnostics = [];
17
+ for (const row of rows) {
18
+ const rawPayload = row?.payload ?? row?.description ?? row?.[1];
19
+ if (typeof rawPayload !== 'string' || rawPayload.length === 0) {
20
+ continue;
21
+ }
22
+ try {
23
+ const parsed = JSON.parse(rawPayload);
24
+ const binding = {
25
+ resourcePath: parsed.resourcePath || row?.resourcePath || row?.[0] || '',
26
+ resourceType: parsed.resourceType || inferResourceType(parsed.resourcePath || row?.resourcePath || row?.[0] || ''),
27
+ bindingKind: parsed.bindingKind || 'direct',
28
+ componentObjectId: parsed.componentObjectId || '',
29
+ evidence: parsed.evidence || buildSyntheticEvidence(row),
30
+ serializedFields: parsed.serializedFields || { scalarFields: [], referenceFields: [] },
31
+ resolvedReferences: parsed.resolvedReferences || [],
32
+ assetRefPaths: normalizeAssetRefPaths(parsed.assetRefPaths) || extractAssetRefPathReferences(parsed.serializedFields || { scalarFields: [], referenceFields: [] }),
33
+ };
34
+ resourceBindings.push(binding);
35
+ scalarFields.push(...binding.serializedFields.scalarFields);
36
+ referenceFields.push(...binding.serializedFields.referenceFields);
37
+ }
38
+ catch (error) {
39
+ unityDiagnostics.push(error instanceof Error ? error.message : String(error));
40
+ }
41
+ }
42
+ return {
43
+ resourceBindings,
44
+ serializedFields: {
45
+ scalarFields,
46
+ referenceFields,
47
+ },
48
+ unityDiagnostics,
49
+ };
50
+ }
51
+ function inferResourceType(resourcePath) {
52
+ if (resourcePath.endsWith('.prefab'))
53
+ return 'prefab';
54
+ if (resourcePath.endsWith('.asset'))
55
+ return 'asset';
56
+ return 'scene';
57
+ }
58
+ function buildSyntheticEvidence(row) {
59
+ const relationType = String(row?.relationType || '').trim();
60
+ const relationReason = String(row?.relationReason || '').trim();
61
+ if (!relationType && !relationReason) {
62
+ return { line: 0, lineText: '' };
63
+ }
64
+ return {
65
+ line: 0,
66
+ lineText: relationReason ? `${relationType}:${relationReason}` : relationType,
67
+ };
68
+ }
69
+ function normalizeAssetRefPaths(input) {
70
+ if (!Array.isArray(input) || input.length === 0) {
71
+ return undefined;
72
+ }
73
+ const rows = [];
74
+ for (const row of input) {
75
+ if (!row || typeof row !== 'object')
76
+ continue;
77
+ const parentFieldName = String(row.parentFieldName || '').trim();
78
+ const fieldName = String(row.fieldName || '').trim();
79
+ const relativePath = String(row.relativePath || '');
80
+ const sourceLayer = String(row.sourceLayer || 'unknown');
81
+ rows.push({
82
+ parentFieldName: parentFieldName || fieldName || 'unknown',
83
+ fieldName: fieldName || parentFieldName || 'unknown',
84
+ relativePath,
85
+ sourceLayer,
86
+ isEmpty: Boolean(row.isEmpty ?? relativePath.length === 0),
87
+ isSprite: Boolean(row.isSprite),
88
+ });
89
+ }
90
+ return rows.length > 0 ? rows : undefined;
91
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,130 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { loadUnityContext, projectUnityBindings } from './unity-enrichment.js';
4
+ test('projectUnityBindings restores graph-native Unity payload rows', () => {
5
+ const out = projectUnityBindings([
6
+ {
7
+ resourcePath: 'Assets/Scene/MainUIManager.unity',
8
+ payload: JSON.stringify({
9
+ resourcePath: 'Assets/Scene/MainUIManager.unity',
10
+ resourceType: 'scene',
11
+ bindingKind: 'nested',
12
+ componentObjectId: '11400000',
13
+ evidence: { line: 9, lineText: ' m_Script: {...}' },
14
+ serializedFields: {
15
+ scalarFields: [{ name: 'needPause', value: '1', valueType: 'number', sourceLayer: 'scene' }],
16
+ referenceFields: [{ name: 'mainUIDocument', guid: 'abc', sourceLayer: 'scene' }],
17
+ },
18
+ }),
19
+ },
20
+ ]);
21
+ assert.equal(out.resourceBindings[0].bindingKind, 'nested');
22
+ assert.ok(out.serializedFields.scalarFields.length >= 1);
23
+ assert.ok(out.serializedFields.referenceFields.length >= 1);
24
+ });
25
+ test('loadUnityContext queries component payload rows and projects stable output', async () => {
26
+ const out = await loadUnityContext('repo-id', 'Class:Assets/Scripts/MainUIManager.cs:MainUIManager', async (query) => {
27
+ assert.match(query, /UNITY_COMPONENT_INSTANCE/);
28
+ assert.match(query, /UNITY_SERIALIZED_TYPE_IN/);
29
+ return [
30
+ {
31
+ resourcePath: 'Assets/Scene/MainUIManager.unity',
32
+ payload: JSON.stringify({
33
+ resourcePath: 'Assets/Scene/MainUIManager.unity',
34
+ resourceType: 'scene',
35
+ bindingKind: 'scene-override',
36
+ componentObjectId: '11400000',
37
+ evidence: { line: 9, lineText: ' m_Script: {...}' },
38
+ serializedFields: {
39
+ scalarFields: [{ name: 'needPause', value: '1', valueType: 'number', sourceLayer: 'scene' }],
40
+ referenceFields: [],
41
+ },
42
+ }),
43
+ },
44
+ ];
45
+ });
46
+ assert.equal(out.resourceBindings[0]?.bindingKind, 'scene-override');
47
+ assert.equal(out.serializedFields.scalarFields[0]?.name, 'needPause');
48
+ assert.deepEqual(out.unityDiagnostics, []);
49
+ });
50
+ test('loadUnityContext returns resourceBindings for UNITY_SERIALIZED_TYPE_IN relations', async () => {
51
+ const out = await loadUnityContext('repo-id', 'Class:Assets/Scripts/AssetRef.cs:AssetRef', async () => [
52
+ {
53
+ relationType: 'UNITY_SERIALIZED_TYPE_IN',
54
+ relationReason: '{"hostSymbol":"InventoryConfig","fieldName":"icon","declaredType":"AssetRef"}',
55
+ resourcePath: 'Assets/Config/Inventory.asset',
56
+ payload: JSON.stringify({
57
+ resourceType: 'asset',
58
+ serializedFields: { scalarFields: [], referenceFields: [] },
59
+ }),
60
+ },
61
+ ]);
62
+ assert.equal(out.resourceBindings.length, 1);
63
+ assert.equal(out.resourceBindings[0]?.resourcePath, 'Assets/Config/Inventory.asset');
64
+ assert.equal(out.resourceBindings[0]?.resourceType, 'asset');
65
+ });
66
+ test('projectUnityBindings preserves structured assetRefPaths from payload', () => {
67
+ const out = projectUnityBindings([
68
+ {
69
+ resourcePath: 'Assets/NEON/DataAssets/CharacterList.asset',
70
+ payload: JSON.stringify({
71
+ resourcePath: 'Assets/NEON/DataAssets/CharacterList.asset',
72
+ resourceType: 'asset',
73
+ bindingKind: 'prefab-instance',
74
+ componentObjectId: '11400000',
75
+ assetRefPaths: [
76
+ {
77
+ parentFieldName: 'Values',
78
+ fieldName: '_Head_Ref',
79
+ relativePath: 'Assets/NEON/Art/Sprites/UI/0_pixle/ui_character_head/hero_head_Nik.png',
80
+ sourceLayer: 'asset',
81
+ isEmpty: false,
82
+ isSprite: true,
83
+ },
84
+ ],
85
+ serializedFields: {
86
+ scalarFields: [],
87
+ referenceFields: [],
88
+ },
89
+ }),
90
+ },
91
+ ]);
92
+ assert.equal(out.resourceBindings.length, 1);
93
+ assert.equal(out.resourceBindings[0]?.assetRefPaths?.length, 1);
94
+ assert.equal(out.resourceBindings[0]?.assetRefPaths?.[0]?.fieldName, '_Head_Ref');
95
+ assert.equal(out.resourceBindings[0]?.assetRefPaths?.[0]?.isSprite, true);
96
+ });
97
+ test('projectUnityBindings derives assetRefPaths from serialized scalar fields when payload lacks structured rows', () => {
98
+ const out = projectUnityBindings([
99
+ {
100
+ resourcePath: 'Assets/NEON/DataAssets/CharacterList.asset',
101
+ payload: JSON.stringify({
102
+ resourcePath: 'Assets/NEON/DataAssets/CharacterList.asset',
103
+ resourceType: 'asset',
104
+ bindingKind: 'prefab-instance',
105
+ componentObjectId: '11400000',
106
+ serializedFields: {
107
+ scalarFields: [
108
+ {
109
+ name: 'Values',
110
+ sourceLayer: 'asset',
111
+ value: `
112
+ _Head_Ref:
113
+ _relativePath: Assets/NEON/Art/Sprites/UI/0_pixle/ui_character_head/hero_head_Nik.png
114
+ _actorPrefabRef:
115
+ _relativePath: Assets/ActorPrefab/Actor_Nik/V_Actor_Nik.prefab
116
+ `,
117
+ },
118
+ ],
119
+ referenceFields: [],
120
+ },
121
+ }),
122
+ },
123
+ ]);
124
+ const refs = out.resourceBindings[0]?.assetRefPaths || [];
125
+ assert.equal(refs.length, 2);
126
+ assert.equal(refs[0]?.fieldName, '_Head_Ref');
127
+ assert.equal(refs[0]?.isSprite, true);
128
+ assert.equal(refs[1]?.fieldName, '_actorPrefabRef');
129
+ assert.equal(refs[1]?.isSprite, false);
130
+ });
@@ -186,7 +186,7 @@ async function getContextResource(backend, repoName) {
186
186
  lines.push(' - cypher: Raw graph queries');
187
187
  lines.push(' - list_repos: Discover all indexed repositories');
188
188
  lines.push('');
189
- lines.push('re_index: Run `npx gitnexus analyze` in terminal if data is stale');
189
+ lines.push('re_index: If data is stale, ask user whether to run `npx -y gitnexus analyze` (reuses previous analyze scope/options unless `--no-reuse-options` is passed). If user declines, clearly state retrieval may not reflect current code. For build/analyze/test commands, use 10-30 minute timeout; on failure/timeout, report exact tool output and do not auto-retry or silently switch to glob/grep fallback.');
190
190
  lines.push('');
191
191
  lines.push('resources_available:');
192
192
  lines.push(' - gitnexus://repos: All indexed repositories');
@@ -17,7 +17,7 @@ export function checkStaleness(repoPath, lastCommit) {
17
17
  return {
18
18
  isStale: true,
19
19
  commitsBehind,
20
- hint: `⚠️ Index is ${commitsBehind} commit${commitsBehind > 1 ? 's' : ''} behind HEAD. Run analyze tool to update.`,
20
+ hint: `⚠️ Index is ${commitsBehind} commit${commitsBehind > 1 ? 's' : ''} behind HEAD. Ask user whether to run analyze to rebuild index; if not rebuilt, retrieval may not match current code.`,
21
21
  };
22
22
  }
23
23
  return { isStale: false, commitsBehind: 0 };
package/dist/mcp/tools.js CHANGED
@@ -45,6 +45,12 @@ Hybrid ranking: BM25 keyword + semantic vector search, ranked by Reciprocal Rank
45
45
  limit: { type: 'number', description: 'Max processes to return (default: 5)', default: 5 },
46
46
  max_symbols: { type: 'number', description: 'Max symbols per process (default: 10)', default: 10 },
47
47
  include_content: { type: 'boolean', description: 'Include full symbol source code (default: false)', default: false },
48
+ unity_resources: {
49
+ type: 'string',
50
+ enum: ['off', 'on', 'auto'],
51
+ description: 'Unity resource retrieval mode (default: off)',
52
+ default: 'off',
53
+ },
48
54
  repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
49
55
  },
50
56
  required: ['query'],
@@ -106,6 +112,12 @@ Handles disambiguation: if multiple symbols share the same name, returns candida
106
112
  uid: { type: 'string', description: 'Direct symbol UID from prior tool results (zero-ambiguity lookup)' },
107
113
  file_path: { type: 'string', description: 'File path to disambiguate common names' },
108
114
  include_content: { type: 'boolean', description: 'Include full symbol source code (default: false)', default: false },
115
+ unity_resources: {
116
+ type: 'string',
117
+ enum: ['off', 'on', 'auto'],
118
+ description: 'Unity resource retrieval mode (default: off)',
119
+ default: 'off',
120
+ },
109
121
  repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
110
122
  },
111
123
  required: [],
@@ -9,6 +9,12 @@ export interface RepoMeta {
9
9
  repoPath: string;
10
10
  lastCommit: string;
11
11
  indexedAt: string;
12
+ analyzeOptions?: {
13
+ includeExtensions?: string[];
14
+ scopeRules?: string[];
15
+ repoAlias?: string;
16
+ embeddings?: boolean;
17
+ };
12
18
  stats?: {
13
19
  files?: number;
14
20
  nodes?: number;
@@ -1,6 +1,7 @@
1
1
  import { GraphNode, GraphRelationship, KnowledgeGraph } from '../core/graph/types.js';
2
2
  import { CommunityDetectionResult } from '../core/ingestion/community-processor.js';
3
3
  import { ProcessDetectionResult } from '../core/ingestion/process-processor.js';
4
+ import type { UnityResourceProcessingResult } from '../core/ingestion/unity-resource-processor.js';
4
5
  import type { ScopeSelectionDiagnostics } from '../core/ingestion/scope-filter.js';
5
6
  export type PipelinePhase = 'idle' | 'extracting' | 'structure' | 'parsing' | 'imports' | 'calls' | 'heritage' | 'communities' | 'processes' | 'enriching' | 'complete' | 'error';
6
7
  export interface PipelineProgress {
@@ -14,6 +15,10 @@ export interface PipelineProgress {
14
15
  nodesCreated: number;
15
16
  };
16
17
  }
18
+ export interface PipelineRunOptions {
19
+ includeExtensions?: string[];
20
+ scopeRules?: string[];
21
+ }
17
22
  export interface PipelineResult {
18
23
  graph: KnowledgeGraph;
19
24
  /** Absolute path to the repo root — used for lazy file reads during KuzuDB loading */
@@ -22,6 +27,7 @@ export interface PipelineResult {
22
27
  totalFileCount: number;
23
28
  communityResult?: CommunityDetectionResult;
24
29
  processResult?: ProcessDetectionResult;
30
+ unityResult?: UnityResourceProcessingResult;
25
31
  scopeDiagnostics?: ScopeSelectionDiagnostics;
26
32
  }
27
33
  export interface SerializablePipelineResult {
@@ -29,6 +35,7 @@ export interface SerializablePipelineResult {
29
35
  relationships: GraphRelationship[];
30
36
  repoPath: string;
31
37
  totalFileCount: number;
38
+ unityResult?: UnityResourceProcessingResult;
32
39
  }
33
40
  export declare const serializePipelineResult: (result: PipelineResult) => SerializablePipelineResult;
34
41
  export declare const deserializePipelineResult: (serialized: SerializablePipelineResult, createGraph: () => KnowledgeGraph) => PipelineResult;
@@ -4,6 +4,7 @@ export const serializePipelineResult = (result) => ({
4
4
  relationships: [...result.graph.iterRelationships()],
5
5
  repoPath: result.repoPath,
6
6
  totalFileCount: result.totalFileCount,
7
+ unityResult: result.unityResult,
7
8
  });
8
9
  // Helper to reconstruct from serializable format (used in main thread)
9
10
  export const deserializePipelineResult = (serialized, createGraph) => {
@@ -14,5 +15,6 @@ export const deserializePipelineResult = (serialized, createGraph) => {
14
15
  graph,
15
16
  repoPath: serialized.repoPath,
16
17
  totalFileCount: serialized.totalFileCount,
18
+ unityResult: serialized.unityResult,
17
19
  };
18
20
  };