@veewo/gitnexus 1.5.0-rc.4 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/dist/benchmark/agent-context/runner.js +3 -0
  2. package/dist/benchmark/agent-context/runner.test.js +22 -0
  3. package/dist/benchmark/agent-context/tool-runner.d.ts +7 -6
  4. package/dist/benchmark/agent-safe-query-context/io.d.ts +2 -0
  5. package/dist/benchmark/agent-safe-query-context/io.js +86 -0
  6. package/dist/benchmark/agent-safe-query-context/io.test.d.ts +1 -0
  7. package/dist/benchmark/agent-safe-query-context/io.test.js +13 -0
  8. package/dist/benchmark/agent-safe-query-context/report.d.ts +57 -0
  9. package/dist/benchmark/agent-safe-query-context/report.js +159 -0
  10. package/dist/benchmark/agent-safe-query-context/report.test.d.ts +1 -0
  11. package/dist/benchmark/agent-safe-query-context/report.test.js +362 -0
  12. package/dist/benchmark/agent-safe-query-context/runner.d.ts +44 -0
  13. package/dist/benchmark/agent-safe-query-context/runner.js +406 -0
  14. package/dist/benchmark/agent-safe-query-context/runner.test.d.ts +1 -0
  15. package/dist/benchmark/agent-safe-query-context/runner.test.js +290 -0
  16. package/dist/benchmark/agent-safe-query-context/semantic-tuple.d.ts +20 -0
  17. package/dist/benchmark/agent-safe-query-context/semantic-tuple.js +225 -0
  18. package/dist/benchmark/agent-safe-query-context/semantic-tuple.test.d.ts +1 -0
  19. package/dist/benchmark/agent-safe-query-context/semantic-tuple.test.js +122 -0
  20. package/dist/benchmark/agent-safe-query-context/subagent-live.d.ts +47 -0
  21. package/dist/benchmark/agent-safe-query-context/subagent-live.js +128 -0
  22. package/dist/benchmark/agent-safe-query-context/subagent-live.test.d.ts +1 -0
  23. package/dist/benchmark/agent-safe-query-context/subagent-live.test.js +155 -0
  24. package/dist/benchmark/agent-safe-query-context/telemetry-tool.d.ts +9 -0
  25. package/dist/benchmark/agent-safe-query-context/telemetry-tool.js +77 -0
  26. package/dist/benchmark/agent-safe-query-context/types.d.ts +61 -0
  27. package/dist/benchmark/agent-safe-query-context/types.js +8 -0
  28. package/dist/benchmark/analyze-runner.d.ts +1 -1
  29. package/dist/benchmark/analyze-runner.js +4 -3
  30. package/dist/benchmark/analyze-runner.test.js +7 -0
  31. package/dist/benchmark/runtime-poc/provenance-artifact.d.ts +47 -0
  32. package/dist/benchmark/runtime-poc/provenance-artifact.js +89 -0
  33. package/dist/benchmark/runtime-poc/runner.d.ts +31 -0
  34. package/dist/benchmark/runtime-poc/runner.js +163 -0
  35. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.d.ts +8 -0
  36. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.js +21 -0
  37. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.d.ts +0 -1
  38. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.js +53 -51
  39. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.test.js +0 -1
  40. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.d.ts +1 -1
  41. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.js +82 -18
  42. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.js +1 -2
  43. package/dist/benchmark/u2-e2e/retrieval-runner.js +15 -7
  44. package/dist/benchmark/u2-e2e/retrieval-runner.test.js +46 -0
  45. package/dist/cli/ai-context.d.ts +0 -1
  46. package/dist/cli/ai-context.js +5 -6
  47. package/dist/cli/ai-context.test.js +8 -0
  48. package/dist/cli/analyze-options.js +58 -34
  49. package/dist/cli/analyze-options.test.js +57 -0
  50. package/dist/cli/analyze-runtime-summary.js +2 -0
  51. package/dist/cli/analyze-runtime-summary.test.js +12 -0
  52. package/dist/cli/analyze-summary.d.ts +4 -0
  53. package/dist/cli/analyze-summary.js +43 -0
  54. package/dist/cli/analyze-summary.test.js +65 -1
  55. package/dist/cli/analyze.d.ts +11 -0
  56. package/dist/cli/analyze.js +34 -5
  57. package/dist/cli/analyze.test.d.ts +1 -0
  58. package/dist/cli/analyze.test.js +25 -0
  59. package/dist/cli/benchmark-agent-context.js +1 -1
  60. package/dist/cli/benchmark-agent-safe-query-context.d.ts +20 -0
  61. package/dist/cli/benchmark-agent-safe-query-context.js +39 -0
  62. package/dist/cli/benchmark-agent-safe-query-context.test.d.ts +1 -0
  63. package/dist/cli/benchmark-agent-safe-query-context.test.js +271 -0
  64. package/dist/cli/benchmark-unity.js +1 -1
  65. package/dist/cli/benchmark-unity.test.js +5 -1
  66. package/dist/cli/benchmark.d.ts +29 -0
  67. package/dist/cli/benchmark.js +55 -0
  68. package/dist/cli/index.js +27 -2
  69. package/dist/cli/rule-lab.d.ts +3 -7
  70. package/dist/cli/rule-lab.js +13 -22
  71. package/dist/cli/rule-lab.test.js +23 -3
  72. package/dist/cli/scope-manifest-config.d.ts +9 -0
  73. package/dist/cli/scope-manifest-config.js +37 -0
  74. package/dist/cli/setup.js +40 -41
  75. package/dist/cli/setup.test.js +14 -14
  76. package/dist/cli/sync-manifest.d.ts +27 -0
  77. package/dist/cli/sync-manifest.js +200 -0
  78. package/dist/cli/sync-manifest.test.d.ts +1 -0
  79. package/dist/cli/sync-manifest.test.js +88 -0
  80. package/dist/cli/tool.d.ts +2 -0
  81. package/dist/cli/tool.js +2 -0
  82. package/dist/core/config/unity-config.d.ts +1 -1
  83. package/dist/core/config/unity-config.js +1 -1
  84. package/dist/core/ingestion/call-processor.d.ts +2 -1
  85. package/dist/core/ingestion/call-processor.js +28 -6
  86. package/dist/core/ingestion/heritage-processor.d.ts +2 -1
  87. package/dist/core/ingestion/heritage-processor.js +30 -7
  88. package/dist/core/ingestion/import-processor.d.ts +2 -1
  89. package/dist/core/ingestion/import-processor.js +28 -6
  90. package/dist/core/ingestion/parsing-processor.d.ts +5 -3
  91. package/dist/core/ingestion/parsing-processor.js +46 -13
  92. package/dist/core/ingestion/pipeline.js +100 -19
  93. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.test.js +18 -20
  94. package/dist/core/ingestion/unity-parity-seed.d.ts +2 -1
  95. package/dist/core/ingestion/unity-parity-seed.js +8 -0
  96. package/dist/core/ingestion/unity-resource-processor.d.ts +11 -0
  97. package/dist/core/ingestion/unity-resource-processor.js +102 -0
  98. package/dist/core/ingestion/unity-resource-processor.test.js +449 -0
  99. package/dist/core/ingestion/unity-runtime-binding-rules.d.ts +16 -1
  100. package/dist/core/ingestion/unity-runtime-binding-rules.js +193 -42
  101. package/dist/core/ingestion/workers/parse-worker.d.ts +2 -0
  102. package/dist/core/ingestion/workers/parse-worker.js +50 -6
  103. package/dist/core/lbug/csv-generator.test.js +2 -2
  104. package/dist/core/tree-sitter/csharp-define-profile.d.ts +6 -0
  105. package/dist/core/tree-sitter/csharp-define-profile.js +43 -0
  106. package/dist/core/tree-sitter/csharp-preproc-normalizer.d.ts +14 -0
  107. package/dist/core/tree-sitter/csharp-preproc-normalizer.js +261 -0
  108. package/dist/core/tree-sitter/parser-loader.d.ts +10 -0
  109. package/dist/core/tree-sitter/parser-loader.js +19 -0
  110. package/dist/core/unity/doc-contract.test.d.ts +1 -0
  111. package/dist/core/unity/doc-contract.test.js +30 -0
  112. package/dist/core/unity/prefab-source-scan.d.ts +25 -0
  113. package/dist/core/unity/prefab-source-scan.js +152 -0
  114. package/dist/core/unity/prefab-source-scan.test.d.ts +1 -0
  115. package/dist/core/unity/prefab-source-scan.test.js +70 -0
  116. package/dist/core/unity/scan-context.d.ts +12 -0
  117. package/dist/core/unity/scan-context.js +50 -2
  118. package/dist/core/unity/scan-context.test.js +74 -0
  119. package/dist/mcp/local/agent-safe-response.d.ts +10 -0
  120. package/dist/mcp/local/agent-safe-response.js +639 -0
  121. package/dist/mcp/local/derived-process-reader.js +1 -1
  122. package/dist/mcp/local/local-backend.d.ts +18 -1
  123. package/dist/mcp/local/local-backend.js +319 -125
  124. package/dist/mcp/local/process-confidence.d.ts +1 -2
  125. package/dist/mcp/local/process-confidence.js +0 -3
  126. package/dist/mcp/local/process-confidence.test.js +4 -2
  127. package/dist/mcp/local/process-evidence.d.ts +1 -8
  128. package/dist/mcp/local/process-evidence.js +1 -23
  129. package/dist/mcp/local/process-evidence.test.js +2 -16
  130. package/dist/mcp/local/process-ref.d.ts +1 -1
  131. package/dist/mcp/local/runtime-chain-closure-evaluator.d.ts +33 -0
  132. package/dist/mcp/local/runtime-chain-closure-evaluator.js +273 -0
  133. package/dist/mcp/local/runtime-chain-graph-candidates.d.ts +23 -0
  134. package/dist/mcp/local/runtime-chain-graph-candidates.js +131 -0
  135. package/dist/mcp/local/runtime-chain-verify.d.ts +1 -1
  136. package/dist/mcp/local/runtime-chain-verify.js +149 -138
  137. package/dist/mcp/local/runtime-chain-verify.test.js +126 -68
  138. package/dist/mcp/local/runtime-claim-rule-registry.d.ts +4 -0
  139. package/dist/mcp/local/runtime-claim-rule-registry.js +4 -0
  140. package/dist/mcp/local/runtime-claim-rule-registry.test.js +37 -4
  141. package/dist/mcp/local/runtime-claim.d.ts +11 -0
  142. package/dist/mcp/local/runtime-claim.js +28 -0
  143. package/dist/mcp/local/unity-evidence-view.d.ts +1 -1
  144. package/dist/mcp/local/unity-evidence-view.js +1 -1
  145. package/dist/mcp/local/unity-evidence-view.test.js +22 -0
  146. package/dist/mcp/tools.js +51 -21
  147. package/dist/rule-lab/analyze.d.ts +2 -1
  148. package/dist/rule-lab/analyze.js +94 -59
  149. package/dist/rule-lab/analyze.test.js +238 -20
  150. package/dist/rule-lab/curate.d.ts +2 -1
  151. package/dist/rule-lab/curate.js +24 -3
  152. package/dist/rule-lab/curate.test.js +65 -0
  153. package/dist/rule-lab/curation-input-builder.d.ts +45 -0
  154. package/dist/rule-lab/curation-input-builder.js +133 -0
  155. package/dist/rule-lab/promote.js +80 -7
  156. package/dist/rule-lab/promote.test.js +150 -0
  157. package/dist/rule-lab/review-pack.d.ts +3 -0
  158. package/dist/rule-lab/review-pack.js +41 -1
  159. package/dist/rule-lab/review-pack.test.js +67 -0
  160. package/dist/rule-lab/types.d.ts +29 -0
  161. package/dist/types/pipeline.d.ts +16 -0
  162. package/package.json +14 -13
  163. package/scripts/check-sync-manifest-traceability.mjs +203 -0
  164. package/scripts/run-node-tests.mjs +61 -0
  165. package/scripts/tree-sitter-audit-classify.mjs +172 -0
  166. package/skills/_shared/unity-rule-authoring-contract.md +64 -0
  167. package/skills/_shared/unity-runtime-process-contract.md +16 -0
  168. package/skills/gitnexus-cli.md +44 -4
  169. package/skills/gitnexus-debugging.md +9 -0
  170. package/skills/gitnexus-exploring.md +66 -18
  171. package/skills/gitnexus-guide.md +42 -3
  172. package/skills/gitnexus-impact-analysis.md +8 -0
  173. package/skills/gitnexus-pr-review.md +8 -0
  174. package/skills/gitnexus-refactoring.md +8 -0
  175. package/skills/gitnexus-unity-rule-gen.md +66 -312
@@ -1,6 +1,7 @@
1
1
  import { generateId } from '../../lib/utils.js';
2
2
  const RULE_EDGE_CONFIDENCE = 0.75;
3
- export function applyUnityRuntimeBindingRules(graph, rules, _config) {
3
+ const RULE_ANOMALY_PREVIEW_LIMIT = 5;
4
+ export function applyUnityRuntimeBindingRules(graph, rules, config) {
4
5
  const ruleResults = [];
5
6
  let totalEdges = 0;
6
7
  const existingPairs = new Set();
@@ -27,7 +28,10 @@ export function applyUnityRuntimeBindingRules(graph, rules, _config) {
27
28
  };
28
29
  // Pre-build indexes
29
30
  const methodsByClassId = new Map();
30
- const classNodes = [];
31
+ const containerNodes = [];
32
+ const containerLabels = config.enableContainerNodes
33
+ ? new Set(['Class', 'Struct', 'Interface', 'Record'])
34
+ : new Set(['Class']);
31
35
  for (const rel of graph.iterRelationships()) {
32
36
  if (rel.type !== 'HAS_METHOD')
33
37
  continue;
@@ -39,8 +43,8 @@ export function applyUnityRuntimeBindingRules(graph, rules, _config) {
39
43
  methodsByClassId.set(rel.sourceId, list);
40
44
  }
41
45
  for (const node of graph.iterNodes()) {
42
- if (node.label === 'Class')
43
- classNodes.push(node);
46
+ if (containerLabels.has(node.label))
47
+ containerNodes.push(node);
44
48
  }
45
49
  // Collect UNITY_ASSET_GUID_REF and UNITY_COMPONENT_INSTANCE edges
46
50
  const assetGuidRefs = [];
@@ -51,6 +55,24 @@ export function applyUnityRuntimeBindingRules(graph, rules, _config) {
51
55
  else if (rel.type === 'UNITY_COMPONENT_INSTANCE')
52
56
  componentInstances.push(rel);
53
57
  }
58
+ const prefabSourceTargetsBySource = buildPrefabSourceTargetsBySource(assetGuidRefs);
59
+ const methodsByResourceId = buildMethodsByResourceId(componentInstances, methodsByClassId);
60
+ const executionState = {
61
+ methodsByResourceId,
62
+ resourceMethodLookupCache: new Map(),
63
+ sceneRuntimeResourceIdsCache: new Map(),
64
+ diagnostics: {
65
+ rulesEvaluated: rules.length,
66
+ bindingsEvaluated: 0,
67
+ bindingsByKind: {},
68
+ methodLookupCalls: 0,
69
+ methodLookupCacheHits: 0,
70
+ sceneRuntimeTraversalCalls: 0,
71
+ sceneRuntimeTraversalCacheHits: 0,
72
+ sceneRuntimeResourcesVisited: 0,
73
+ anomalySet: new Set(),
74
+ },
75
+ };
54
76
  // Pre-build scene file index: lowercase scene name → fileId[]
55
77
  const sceneFilesByName = new Map();
56
78
  for (const node of graph.iterNodes()) {
@@ -67,51 +89,69 @@ export function applyUnityRuntimeBindingRules(graph, rules, _config) {
67
89
  for (const rule of rules) {
68
90
  let ruleEdges = 0;
69
91
  for (const binding of rule.resource_bindings ?? []) {
70
- ruleEdges += processBinding(binding, rule.id, assetGuidRefs, componentInstances, methodsByClassId, classNodes, sceneFilesByName, addSyntheticEdge);
92
+ executionState.diagnostics.bindingsEvaluated += 1;
93
+ executionState.diagnostics.bindingsByKind[binding.kind] =
94
+ (executionState.diagnostics.bindingsByKind[binding.kind] || 0) + 1;
95
+ ruleEdges += processBinding(binding, rule.id, assetGuidRefs, componentInstances, methodsByClassId, containerNodes, sceneFilesByName, prefabSourceTargetsBySource, executionState, addSyntheticEdge);
71
96
  }
72
97
  if (rule.lifecycle_overrides?.additional_entry_points?.length) {
73
- ruleEdges += processLifecycleOverrides(rule, methodsByClassId, classNodes, addSyntheticEdge);
98
+ ruleEdges += processLifecycleOverrides(rule, methodsByClassId, containerNodes, addSyntheticEdge);
74
99
  }
75
100
  totalEdges += ruleEdges;
76
101
  ruleResults.push({ ruleId: rule.id, edgesInjected: ruleEdges });
77
102
  }
78
- return { edgesInjected: totalEdges, ruleResults };
103
+ return {
104
+ edgesInjected: totalEdges,
105
+ ruleResults,
106
+ diagnostics: finalizeRuleBindingDiagnostics(executionState.diagnostics, totalEdges),
107
+ };
79
108
  }
80
- function findMethodsOnResource(resourceFileId, componentInstances, methodsByClassId, entryPoints) {
81
- const results = [];
109
+ function findMethodsOnResource(resourceFileId, executionState, entryPoints) {
110
+ executionState.diagnostics.methodLookupCalls += 1;
111
+ const cacheKey = `${resourceFileId}::${entryPoints.join('|')}`;
112
+ const cached = executionState.resourceMethodLookupCache.get(cacheKey);
113
+ if (cached) {
114
+ executionState.diagnostics.methodLookupCacheHits += 1;
115
+ return cached;
116
+ }
117
+ const methods = executionState.methodsByResourceId.get(resourceFileId) ?? [];
118
+ if (methods.length === 0) {
119
+ executionState.resourceMethodLookupCache.set(cacheKey, []);
120
+ return [];
121
+ }
82
122
  const entrySet = new Set(entryPoints);
83
- for (const ci of componentInstances) {
84
- if (ci.targetId !== resourceFileId)
85
- continue;
86
- const classId = ci.sourceId;
87
- for (const method of methodsByClassId.get(classId) ?? []) {
88
- if (entrySet.has(method.properties.name))
89
- results.push(method);
90
- }
123
+ const results = [];
124
+ for (const method of methods) {
125
+ if (entrySet.has(method.properties.name))
126
+ results.push(method);
91
127
  }
128
+ executionState.resourceMethodLookupCache.set(cacheKey, results);
92
129
  return results;
93
130
  }
94
- function processBinding(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, classNodes, sceneFilesByName, addEdge) {
131
+ function processBinding(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, containerNodes, sceneFilesByName, prefabSourceTargetsBySource, executionState, addEdge) {
95
132
  if (binding.kind === 'asset_ref_loads_components') {
96
- return processAssetRefLoadsComponents(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, addEdge);
133
+ return processAssetRefLoadsComponents(binding, ruleId, assetGuidRefs, executionState, addEdge);
97
134
  }
98
135
  if (binding.kind === 'method_triggers_field_load') {
99
- return processMethodTriggersFieldLoad(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, classNodes, addEdge);
136
+ return processMethodTriggersFieldLoad(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, containerNodes, executionState, addEdge);
100
137
  }
101
138
  if (binding.kind === 'method_triggers_scene_load') {
102
- return processMethodTriggersSceneLoad(binding, ruleId, componentInstances, methodsByClassId, classNodes, sceneFilesByName, addEdge);
139
+ return processMethodTriggersSceneLoad(binding, ruleId, componentInstances, methodsByClassId, containerNodes, sceneFilesByName, prefabSourceTargetsBySource, executionState, addEdge);
103
140
  }
104
141
  if (binding.kind === 'method_triggers_method') {
105
- return processMethodTriggersMethod(binding, ruleId, methodsByClassId, classNodes, addEdge);
142
+ return processMethodTriggersMethod(binding, ruleId, methodsByClassId, containerNodes, addEdge);
106
143
  }
144
+ addAnomaly(executionState.diagnostics, `rule=${ruleId}: unsupported resource_binding kind "${binding.kind}"`);
107
145
  return 0;
108
146
  }
109
- function processAssetRefLoadsComponents(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, addEdge) {
147
+ function processAssetRefLoadsComponents(binding, ruleId, assetGuidRefs, executionState, addEdge) {
110
148
  let count = 0;
111
149
  const pattern = binding.ref_field_pattern ? new RegExp(binding.ref_field_pattern) : null;
112
150
  const entryPoints = binding.target_entry_points ?? [];
113
- if (!pattern || entryPoints.length === 0)
151
+ if (!pattern || entryPoints.length === 0) {
152
+ addAnomaly(executionState.diagnostics, `rule=${ruleId}: asset_ref_loads_components missing ref_field_pattern or target_entry_points`);
114
153
  return 0;
154
+ }
115
155
  const runtimeRootId = generateId('Method', 'unity-runtime-root');
116
156
  for (const ref of assetGuidRefs) {
117
157
  let fieldName = '';
@@ -124,7 +164,7 @@ function processAssetRefLoadsComponents(binding, ruleId, assetGuidRefs, componen
124
164
  }
125
165
  if (!pattern.test(fieldName))
126
166
  continue;
127
- const targetMethods = findMethodsOnResource(ref.targetId, componentInstances, methodsByClassId, entryPoints);
167
+ const targetMethods = findMethodsOnResource(ref.targetId, executionState, entryPoints);
128
168
  for (const method of targetMethods) {
129
169
  if (addEdge(runtimeRootId, method.id, `unity-rule-resource-load:${ruleId}`))
130
170
  count++;
@@ -132,15 +172,17 @@ function processAssetRefLoadsComponents(binding, ruleId, assetGuidRefs, componen
132
172
  }
133
173
  return count;
134
174
  }
135
- function processMethodTriggersFieldLoad(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, classNodes, addEdge) {
175
+ function processMethodTriggersFieldLoad(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, containerNodes, executionState, addEdge) {
136
176
  let count = 0;
137
177
  const classPattern = binding.host_class_pattern ? new RegExp(binding.host_class_pattern) : null;
138
178
  const loaderMethodNames = new Set(binding.loader_methods ?? []);
139
179
  const entryPoints = binding.target_entry_points ?? [];
140
180
  const defaultEntryPoints = ['OnEnable', 'Awake', 'Start'];
141
181
  const resolvedEntryPoints = entryPoints.length > 0 ? entryPoints : defaultEntryPoints;
142
- if (!classPattern || loaderMethodNames.size === 0)
182
+ if (!classPattern || loaderMethodNames.size === 0) {
183
+ addAnomaly(executionState.diagnostics, `rule=${ruleId}: method_triggers_field_load missing host_class_pattern or loader_methods`);
143
184
  return 0;
185
+ }
144
186
  // Build asset ref index by source file
145
187
  const refsBySource = new Map();
146
188
  for (const ref of assetGuidRefs) {
@@ -148,7 +190,7 @@ function processMethodTriggersFieldLoad(binding, ruleId, assetGuidRefs, componen
148
190
  list.push(ref);
149
191
  refsBySource.set(ref.sourceId, list);
150
192
  }
151
- for (const cls of classNodes) {
193
+ for (const cls of containerNodes) {
152
194
  if (!classPattern.test(cls.properties.name))
153
195
  continue;
154
196
  const methods = methodsByClassId.get(cls.id) ?? [];
@@ -164,7 +206,7 @@ function processMethodTriggersFieldLoad(binding, ruleId, assetGuidRefs, componen
164
206
  // Follow asset refs from those resource files
165
207
  for (const resourceFileId of resourceFileIds) {
166
208
  for (const ref of refsBySource.get(resourceFileId) ?? []) {
167
- const targetMethods = findMethodsOnResource(ref.targetId, componentInstances, methodsByClassId, resolvedEntryPoints);
209
+ const targetMethods = findMethodsOnResource(ref.targetId, executionState, resolvedEntryPoints);
168
210
  for (const loader of loaders) {
169
211
  for (const target of targetMethods) {
170
212
  if (addEdge(loader.id, target.id, `unity-rule-loader-bridge:${ruleId}`))
@@ -176,7 +218,7 @@ function processMethodTriggersFieldLoad(binding, ruleId, assetGuidRefs, componen
176
218
  }
177
219
  return count;
178
220
  }
179
- function processMethodTriggersSceneLoad(binding, ruleId, componentInstances, methodsByClassId, classNodes, sceneFilesByName, addEdge) {
221
+ function processMethodTriggersSceneLoad(binding, ruleId, componentInstances, methodsByClassId, containerNodes, sceneFilesByName, prefabSourceTargetsBySource, executionState, addEdge) {
180
222
  const classPattern = binding.host_class_pattern ? new RegExp(binding.host_class_pattern) : null;
181
223
  const loaderMethodNames = new Set(binding.loader_methods ?? []);
182
224
  const sceneName = binding.scene_name;
@@ -184,13 +226,17 @@ function processMethodTriggersSceneLoad(binding, ruleId, componentInstances, met
184
226
  const entryPoints = (binding.target_entry_points ?? []).length > 0
185
227
  ? binding.target_entry_points
186
228
  : defaultEntryPoints;
187
- if (!classPattern || loaderMethodNames.size === 0 || !sceneName)
229
+ if (!classPattern || loaderMethodNames.size === 0 || !sceneName) {
230
+ addAnomaly(executionState.diagnostics, `rule=${ruleId}: method_triggers_scene_load missing host_class_pattern, loader_methods, or scene_name`);
188
231
  return 0;
232
+ }
189
233
  const sceneFileIds = sceneFilesByName.get(sceneName.toLowerCase()) ?? [];
190
- if (sceneFileIds.length === 0)
234
+ if (sceneFileIds.length === 0) {
235
+ addAnomaly(executionState.diagnostics, `rule=${ruleId}: scene "${sceneName}" not found in File(.unity) index`);
191
236
  return 0;
237
+ }
192
238
  let count = 0;
193
- for (const cls of classNodes) {
239
+ for (const cls of containerNodes) {
194
240
  if (!classPattern.test(cls.properties.name))
195
241
  continue;
196
242
  const methods = methodsByClassId.get(cls.id) ?? [];
@@ -198,18 +244,82 @@ function processMethodTriggersSceneLoad(binding, ruleId, componentInstances, met
198
244
  if (loaders.length === 0)
199
245
  continue;
200
246
  for (const sceneFileId of sceneFileIds) {
201
- const targetMethods = findMethodsOnResource(sceneFileId, componentInstances, methodsByClassId, entryPoints);
202
- for (const loader of loaders) {
203
- for (const target of targetMethods) {
204
- if (addEdge(loader.id, target.id, `unity-rule-scene-load:${ruleId}`))
205
- count++;
247
+ const runtimeResourceIds = getSceneRuntimeResourceIds(sceneFileId, prefabSourceTargetsBySource, executionState);
248
+ for (const runtimeResourceId of runtimeResourceIds) {
249
+ const targetMethods = findMethodsOnResource(runtimeResourceId, executionState, entryPoints);
250
+ for (const loader of loaders) {
251
+ for (const target of targetMethods) {
252
+ if (addEdge(loader.id, target.id, `unity-rule-scene-load:${ruleId}`))
253
+ count++;
254
+ }
206
255
  }
207
256
  }
208
257
  }
209
258
  }
210
259
  return count;
211
260
  }
212
- function processMethodTriggersMethod(binding, ruleId, methodsByClassId, classNodes, addEdge) {
261
+ function buildMethodsByResourceId(componentInstances, methodsByClassId) {
262
+ const methodsByResourceId = new Map();
263
+ for (const componentRef of componentInstances) {
264
+ const methods = methodsByClassId.get(componentRef.sourceId) ?? [];
265
+ if (methods.length === 0)
266
+ continue;
267
+ const list = methodsByResourceId.get(componentRef.targetId) ?? [];
268
+ list.push(...methods);
269
+ methodsByResourceId.set(componentRef.targetId, list);
270
+ }
271
+ return methodsByResourceId;
272
+ }
273
+ function buildPrefabSourceTargetsBySource(assetGuidRefs) {
274
+ const targetsBySource = new Map();
275
+ for (const ref of assetGuidRefs) {
276
+ let fieldName = '';
277
+ try {
278
+ const parsed = JSON.parse(String(ref.reason || '{}'));
279
+ fieldName = String(parsed?.fieldName || '');
280
+ }
281
+ catch {
282
+ continue;
283
+ }
284
+ if (fieldName !== 'm_SourcePrefab')
285
+ continue;
286
+ const targets = targetsBySource.get(ref.sourceId) ?? new Set();
287
+ targets.add(ref.targetId);
288
+ targetsBySource.set(ref.sourceId, targets);
289
+ }
290
+ const out = new Map();
291
+ for (const [sourceId, targets] of targetsBySource.entries()) {
292
+ out.set(sourceId, [...targets]);
293
+ }
294
+ return out;
295
+ }
296
+ function collectSceneRuntimeResourceIds(sceneFileId, prefabSourceTargetsBySource) {
297
+ const visited = new Set([sceneFileId]);
298
+ const queue = [sceneFileId];
299
+ while (queue.length > 0) {
300
+ const currentId = queue.shift();
301
+ for (const targetId of prefabSourceTargetsBySource.get(currentId) ?? []) {
302
+ if (visited.has(targetId))
303
+ continue;
304
+ visited.add(targetId);
305
+ queue.push(targetId);
306
+ }
307
+ }
308
+ return [...visited];
309
+ }
310
+ function getSceneRuntimeResourceIds(sceneFileId, prefabSourceTargetsBySource, executionState) {
311
+ executionState.diagnostics.sceneRuntimeTraversalCalls += 1;
312
+ const cached = executionState.sceneRuntimeResourceIdsCache.get(sceneFileId);
313
+ if (cached) {
314
+ executionState.diagnostics.sceneRuntimeTraversalCacheHits += 1;
315
+ return cached;
316
+ }
317
+ const ids = collectSceneRuntimeResourceIds(sceneFileId, prefabSourceTargetsBySource);
318
+ executionState.sceneRuntimeResourceIdsCache.set(sceneFileId, ids);
319
+ executionState.diagnostics.sceneRuntimeResourcesVisited += ids.length;
320
+ return ids;
321
+ }
322
+ function processMethodTriggersMethod(binding, ruleId, methodsByClassId, containerNodes, addEdge) {
213
323
  const { source_class_pattern, source_method, target_class_pattern, target_method } = binding;
214
324
  if (!source_class_pattern || !source_method || !target_class_pattern || !target_method)
215
325
  return 0;
@@ -217,7 +327,7 @@ function processMethodTriggersMethod(binding, ruleId, methodsByClassId, classNod
217
327
  const tgtPattern = new RegExp(target_class_pattern);
218
328
  let sourceMethodId;
219
329
  let targetMethodId;
220
- for (const cls of classNodes) {
330
+ for (const cls of containerNodes) {
221
331
  if (!sourceMethodId && srcPattern.test(cls.properties.name)) {
222
332
  const match = (methodsByClassId.get(cls.id) ?? []).find(m => m.properties.name === source_method);
223
333
  if (match)
@@ -235,7 +345,7 @@ function processMethodTriggersMethod(binding, ruleId, methodsByClassId, classNod
235
345
  return 0;
236
346
  return addEdge(sourceMethodId, targetMethodId, `unity-rule-method-bridge:${ruleId}`) ? 1 : 0;
237
347
  }
238
- function processLifecycleOverrides(rule, methodsByClassId, classNodes, addEdge) {
348
+ function processLifecycleOverrides(rule, methodsByClassId, containerNodes, addEdge) {
239
349
  const overrides = rule.lifecycle_overrides;
240
350
  if (!overrides?.additional_entry_points?.length)
241
351
  return 0;
@@ -243,7 +353,7 @@ function processLifecycleOverrides(rule, methodsByClassId, classNodes, addEdge)
243
353
  const scopePattern = overrides.scope ? new RegExp(overrides.scope) : null;
244
354
  let count = 0;
245
355
  const runtimeRootId = generateId('Method', 'unity-runtime-root');
246
- for (const cls of classNodes) {
356
+ for (const cls of containerNodes) {
247
357
  if (scopePattern && !scopePattern.test(cls.properties.filePath ?? cls.properties.name))
248
358
  continue;
249
359
  for (const method of methodsByClassId.get(cls.id) ?? []) {
@@ -255,3 +365,44 @@ function processLifecycleOverrides(rule, methodsByClassId, classNodes, addEdge)
255
365
  }
256
366
  return count;
257
367
  }
368
+ function addAnomaly(diagnostics, message) {
369
+ diagnostics.anomalySet.add(message);
370
+ }
371
+ function finalizeRuleBindingDiagnostics(diagnostics, edgesInjected) {
372
+ const anomalies = [...diagnostics.anomalySet];
373
+ const shouldAgentReport = anomalies.length > 0;
374
+ const bindingsByKind = Object.fromEntries(Object.entries(diagnostics.bindingsByKind).sort(([a], [b]) => a.localeCompare(b)));
375
+ const kindSummary = Object.entries(bindingsByKind)
376
+ .map(([kind, count]) => `${kind}=${count}`)
377
+ .join(', ') || 'none';
378
+ const summary = [
379
+ `rule_binding.summary: rules=${diagnostics.rulesEvaluated}, bindings=${diagnostics.bindingsEvaluated}, edges=${edgesInjected}`,
380
+ `rule_binding.bindings_by_kind: ${kindSummary}`,
381
+ `rule_binding.lookup: method_calls=${diagnostics.methodLookupCalls}, cache_hits=${diagnostics.methodLookupCacheHits}`,
382
+ `rule_binding.scene_closure: traversals=${diagnostics.sceneRuntimeTraversalCalls}, cache_hits=${diagnostics.sceneRuntimeTraversalCacheHits}, visited_resources=${diagnostics.sceneRuntimeResourcesVisited}`,
383
+ `rule_binding.agent_report: should_report=${shouldAgentReport} reason="${shouldAgentReport ? 'rule-binding anomalies detected' : 'no anomalies detected'}"`,
384
+ ];
385
+ if (anomalies.length > 0) {
386
+ summary.push(`rule_binding.anomalies: count=${anomalies.length}`);
387
+ for (const anomaly of anomalies.slice(0, RULE_ANOMALY_PREVIEW_LIMIT)) {
388
+ summary.push(`rule_binding.anomaly: ${anomaly}`);
389
+ }
390
+ if (anomalies.length > RULE_ANOMALY_PREVIEW_LIMIT) {
391
+ summary.push(`rule_binding.anomaly: ... ${anomalies.length - RULE_ANOMALY_PREVIEW_LIMIT} more`);
392
+ }
393
+ }
394
+ return {
395
+ rulesEvaluated: diagnostics.rulesEvaluated,
396
+ bindingsEvaluated: diagnostics.bindingsEvaluated,
397
+ bindingsByKind,
398
+ methodLookupCalls: diagnostics.methodLookupCalls,
399
+ methodLookupCacheHits: diagnostics.methodLookupCacheHits,
400
+ sceneRuntimeTraversalCalls: diagnostics.sceneRuntimeTraversalCalls,
401
+ sceneRuntimeTraversalCacheHits: diagnostics.sceneRuntimeTraversalCacheHits,
402
+ sceneRuntimeResourcesVisited: diagnostics.sceneRuntimeResourcesVisited,
403
+ anomalies,
404
+ shouldAgentReport,
405
+ agentReportReason: shouldAgentReport ? 'rule-binding anomalies detected' : 'no anomalies detected',
406
+ summary,
407
+ };
408
+ }
@@ -97,10 +97,12 @@ export interface ParseWorkerResult {
97
97
  routes: ExtractedRoute[];
98
98
  constructorBindings: FileConstructorBindings[];
99
99
  skippedLanguages: Record<string, number>;
100
+ csharpPreprocFallbackFiles: number;
100
101
  fileCount: number;
101
102
  }
102
103
  export interface ParseWorkerInput {
103
104
  path: string;
104
105
  content: string;
106
+ rawContent?: string;
105
107
  }
106
108
  export {};
@@ -14,7 +14,7 @@ import Ruby from 'tree-sitter-ruby';
14
14
  import { createRequire } from 'node:module';
15
15
  import { SupportedLanguages } from '../../../config/supported-languages.js';
16
16
  import { LANGUAGE_QUERIES } from '../tree-sitter-queries.js';
17
- import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from '../constants.js';
17
+ import { TREE_SITTER_MAX_BUFFER } from '../constants.js';
18
18
  // tree-sitter-swift is an optionalDependency — may not be installed
19
19
  const _require = createRequire(import.meta.url);
20
20
  let Swift = null;
@@ -166,6 +166,7 @@ const processBatch = (files, onProgress) => {
166
166
  routes: [],
167
167
  constructorBindings: [],
168
168
  skippedLanguages: {},
169
+ csharpPreprocFallbackFiles: 0,
169
170
  fileCount: 0,
170
171
  };
171
172
  // Group by language to minimize setLanguage calls
@@ -716,14 +717,56 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
716
717
  if (file.content.length > TREE_SITTER_MAX_BUFFER)
717
718
  continue;
718
719
  let tree;
720
+ let usedRawContentFallback = false;
719
721
  try {
720
- tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
722
+ const MAX_CHUNK = 4096;
723
+ tree = parser.parse((index) => {
724
+ if (index >= file.content.length)
725
+ return null;
726
+ return file.content.slice(index, index + MAX_CHUNK);
727
+ });
721
728
  }
722
729
  catch (err) {
723
- console.warn(`Failed to parse file ${file.path}: ${err instanceof Error ? err.message : String(err)}`);
724
- continue;
730
+ if (file.rawContent && file.rawContent !== file.content) {
731
+ try {
732
+ const MAX_CHUNK = 4096;
733
+ tree = parser.parse((index) => {
734
+ if (index >= file.rawContent.length)
735
+ return null;
736
+ return file.rawContent.slice(index, index + MAX_CHUNK);
737
+ });
738
+ usedRawContentFallback = true;
739
+ }
740
+ catch {
741
+ console.warn(`Failed to parse file ${file.path}: ${err instanceof Error ? err.message : String(err)}`);
742
+ continue;
743
+ }
744
+ }
745
+ else {
746
+ console.warn(`Failed to parse file ${file.path}: ${err instanceof Error ? err.message : String(err)}`);
747
+ continue;
748
+ }
749
+ }
750
+ if (file.rawContent && file.rawContent !== file.content && tree.rootNode?.hasError) {
751
+ try {
752
+ const MAX_CHUNK = 4096;
753
+ const rawTree = parser.parse((index) => {
754
+ if (index >= file.rawContent.length)
755
+ return null;
756
+ return file.rawContent.slice(index, index + MAX_CHUNK);
757
+ });
758
+ if (!rawTree.rootNode?.hasError) {
759
+ tree = rawTree;
760
+ usedRawContentFallback = true;
761
+ }
762
+ }
763
+ catch {
764
+ // Keep normalized parse result when raw fallback parsing fails
765
+ }
725
766
  }
726
767
  result.fileCount++;
768
+ if (usedRawContentFallback)
769
+ result.csharpPreprocFallbackFiles += 1;
727
770
  onFileProcessed?.();
728
771
  // Build per-file type environment + constructor bindings in a single AST walk.
729
772
  // Constructor bindings are verified against the SymbolTable in processCallsFromExtracted.
@@ -1030,7 +1073,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1030
1073
  /** Accumulated result across sub-batches */
1031
1074
  let accumulated = {
1032
1075
  nodes: [], relationships: [], symbols: [],
1033
- imports: [], calls: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, fileCount: 0,
1076
+ imports: [], calls: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, csharpPreprocFallbackFiles: 0, fileCount: 0,
1034
1077
  };
1035
1078
  let cumulativeProcessed = 0;
1036
1079
  const mergeResult = (target, src) => {
@@ -1045,6 +1088,7 @@ const mergeResult = (target, src) => {
1045
1088
  for (const [lang, count] of Object.entries(src.skippedLanguages)) {
1046
1089
  target.skippedLanguages[lang] = (target.skippedLanguages[lang] || 0) + count;
1047
1090
  }
1091
+ target.csharpPreprocFallbackFiles += src.csharpPreprocFallbackFiles;
1048
1092
  target.fileCount += src.fileCount;
1049
1093
  };
1050
1094
  parentPort.on('message', (msg) => {
@@ -1064,7 +1108,7 @@ parentPort.on('message', (msg) => {
1064
1108
  if (msg && msg.type === 'flush') {
1065
1109
  parentPort.postMessage({ type: 'result', data: accumulated });
1066
1110
  // Reset for potential reuse
1067
- accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, fileCount: 0 };
1111
+ accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, csharpPreprocFallbackFiles: 0, fileCount: 0 };
1068
1112
  cumulativeProcessed = 0;
1069
1113
  return;
1070
1114
  }
@@ -1,8 +1,8 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { FileContentCache, toCodeElementCsvRow } from './csv-generator.js';
4
- test('FileContentCache evicts oldest entries when byte budget is exceeded', async () => {
5
- const cache = new FileContentCache('/tmp/repo', 10);
4
+ test('FileContentCache evicts oldest entries when max entry count is exceeded', async () => {
5
+ const cache = new FileContentCache('/tmp/repo', 1);
6
6
  cache.setForTest('a.cs', '123456');
7
7
  cache.setForTest('b.cs', '123456');
8
8
  assert.equal(cache.hasForTest('a.cs'), false);
@@ -0,0 +1,6 @@
1
+ export interface CSharpDefineProfile {
2
+ symbols: Set<string>;
3
+ sourcePath: string;
4
+ rawDefineConstants: string[];
5
+ }
6
+ export declare function loadCSharpDefineProfileFromCsproj(csprojPath: string): Promise<CSharpDefineProfile>;
@@ -0,0 +1,43 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ const DEFINE_CONSTANTS_RE = /<DefineConstants>\s*([\s\S]*?)\s*<\/DefineConstants>/gi;
4
+ const CSHARP_SYMBOL_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
5
+ function parseSymbols(rawDefineConstants) {
6
+ const symbols = new Set();
7
+ for (const raw of rawDefineConstants) {
8
+ for (const token of raw.split(';')) {
9
+ const symbol = token.trim();
10
+ if (!symbol)
11
+ continue;
12
+ // Drop MSBuild placeholders such as $(DefineConstants)
13
+ if (symbol.includes('$('))
14
+ continue;
15
+ if (!CSHARP_SYMBOL_RE.test(symbol))
16
+ continue;
17
+ symbols.add(symbol);
18
+ }
19
+ }
20
+ return symbols;
21
+ }
22
+ export async function loadCSharpDefineProfileFromCsproj(csprojPath) {
23
+ const sourcePath = path.resolve(csprojPath);
24
+ let content;
25
+ try {
26
+ content = await fs.readFile(sourcePath, 'utf-8');
27
+ }
28
+ catch (error) {
29
+ const message = error instanceof Error ? error.message : String(error);
30
+ throw new Error(`Failed to read C# csproj: ${sourcePath} (${message})`);
31
+ }
32
+ const rawDefineConstants = [];
33
+ for (const match of content.matchAll(DEFINE_CONSTANTS_RE)) {
34
+ const raw = (match[1] || '').trim();
35
+ if (raw)
36
+ rawDefineConstants.push(raw);
37
+ }
38
+ return {
39
+ symbols: parseSymbols(rawDefineConstants),
40
+ sourcePath,
41
+ rawDefineConstants,
42
+ };
43
+ }
@@ -0,0 +1,14 @@
1
+ export interface CSharpPreprocNormalizationDiagnostics {
2
+ directivesSeen: number;
3
+ inactiveLines: number;
4
+ undefinedSymbols: string[];
5
+ expressionErrors: number;
6
+ unmatchedEndif: number;
7
+ unterminatedIfBlocks: number;
8
+ }
9
+ export interface CSharpPreprocNormalizationResult {
10
+ normalizedText: string;
11
+ changed: boolean;
12
+ diagnostics: CSharpPreprocNormalizationDiagnostics;
13
+ }
14
+ export declare function normalizeCSharpPreprocessorBranches(source: string, defines: Set<string>): CSharpPreprocNormalizationResult;