@veewo/gitnexus 1.5.0 → 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 (137) 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/runtime-poc/provenance-artifact.d.ts +47 -0
  29. package/dist/benchmark/runtime-poc/provenance-artifact.js +89 -0
  30. package/dist/benchmark/runtime-poc/runner.d.ts +31 -0
  31. package/dist/benchmark/runtime-poc/runner.js +163 -0
  32. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.d.ts +8 -0
  33. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.js +21 -0
  34. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.d.ts +0 -1
  35. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.js +53 -51
  36. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.test.js +0 -1
  37. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.d.ts +1 -1
  38. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.js +82 -18
  39. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.js +1 -2
  40. package/dist/benchmark/u2-e2e/retrieval-runner.js +15 -7
  41. package/dist/benchmark/u2-e2e/retrieval-runner.test.js +46 -0
  42. package/dist/cli/ai-context.js +2 -12
  43. package/dist/cli/ai-context.test.js +8 -0
  44. package/dist/cli/analyze-runtime-summary.js +1 -0
  45. package/dist/cli/analyze-runtime-summary.test.js +2 -0
  46. package/dist/cli/analyze-summary.d.ts +2 -0
  47. package/dist/cli/analyze-summary.js +24 -0
  48. package/dist/cli/analyze-summary.test.js +65 -1
  49. package/dist/cli/analyze.js +5 -1
  50. package/dist/cli/benchmark-agent-safe-query-context.d.ts +20 -0
  51. package/dist/cli/benchmark-agent-safe-query-context.js +39 -0
  52. package/dist/cli/benchmark-agent-safe-query-context.test.d.ts +1 -0
  53. package/dist/cli/benchmark-agent-safe-query-context.test.js +271 -0
  54. package/dist/cli/benchmark.d.ts +29 -0
  55. package/dist/cli/benchmark.js +55 -0
  56. package/dist/cli/index.js +23 -0
  57. package/dist/cli/rule-lab.d.ts +3 -7
  58. package/dist/cli/rule-lab.js +13 -22
  59. package/dist/cli/rule-lab.test.js +23 -3
  60. package/dist/cli/tool.d.ts +2 -0
  61. package/dist/cli/tool.js +2 -0
  62. package/dist/core/config/unity-config.d.ts +0 -1
  63. package/dist/core/config/unity-config.js +0 -1
  64. package/dist/core/ingestion/pipeline.js +35 -6
  65. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.test.js +18 -20
  66. package/dist/core/ingestion/unity-parity-seed.d.ts +2 -1
  67. package/dist/core/ingestion/unity-parity-seed.js +8 -0
  68. package/dist/core/ingestion/unity-resource-processor.d.ts +11 -0
  69. package/dist/core/ingestion/unity-resource-processor.js +102 -0
  70. package/dist/core/ingestion/unity-resource-processor.test.js +449 -0
  71. package/dist/core/ingestion/unity-runtime-binding-rules.d.ts +15 -0
  72. package/dist/core/ingestion/unity-runtime-binding-rules.js +178 -30
  73. package/dist/core/lbug/csv-generator.test.js +2 -2
  74. package/dist/core/unity/doc-contract.test.d.ts +1 -0
  75. package/dist/core/unity/doc-contract.test.js +30 -0
  76. package/dist/core/unity/prefab-source-scan.d.ts +25 -0
  77. package/dist/core/unity/prefab-source-scan.js +152 -0
  78. package/dist/core/unity/prefab-source-scan.test.d.ts +1 -0
  79. package/dist/core/unity/prefab-source-scan.test.js +70 -0
  80. package/dist/core/unity/scan-context.d.ts +12 -0
  81. package/dist/core/unity/scan-context.js +50 -2
  82. package/dist/core/unity/scan-context.test.js +74 -0
  83. package/dist/mcp/local/agent-safe-response.d.ts +10 -0
  84. package/dist/mcp/local/agent-safe-response.js +639 -0
  85. package/dist/mcp/local/derived-process-reader.js +1 -1
  86. package/dist/mcp/local/local-backend.d.ts +18 -1
  87. package/dist/mcp/local/local-backend.js +319 -125
  88. package/dist/mcp/local/process-confidence.d.ts +1 -2
  89. package/dist/mcp/local/process-confidence.js +0 -3
  90. package/dist/mcp/local/process-confidence.test.js +4 -2
  91. package/dist/mcp/local/process-evidence.d.ts +1 -8
  92. package/dist/mcp/local/process-evidence.js +1 -23
  93. package/dist/mcp/local/process-evidence.test.js +2 -16
  94. package/dist/mcp/local/process-ref.d.ts +1 -1
  95. package/dist/mcp/local/runtime-chain-closure-evaluator.d.ts +33 -0
  96. package/dist/mcp/local/runtime-chain-closure-evaluator.js +273 -0
  97. package/dist/mcp/local/runtime-chain-graph-candidates.d.ts +23 -0
  98. package/dist/mcp/local/runtime-chain-graph-candidates.js +131 -0
  99. package/dist/mcp/local/runtime-chain-verify.d.ts +1 -1
  100. package/dist/mcp/local/runtime-chain-verify.js +149 -138
  101. package/dist/mcp/local/runtime-chain-verify.test.js +126 -68
  102. package/dist/mcp/local/runtime-claim-rule-registry.d.ts +4 -0
  103. package/dist/mcp/local/runtime-claim-rule-registry.js +4 -0
  104. package/dist/mcp/local/runtime-claim-rule-registry.test.js +37 -4
  105. package/dist/mcp/local/runtime-claim.d.ts +11 -0
  106. package/dist/mcp/local/runtime-claim.js +28 -0
  107. package/dist/mcp/local/unity-evidence-view.d.ts +1 -1
  108. package/dist/mcp/local/unity-evidence-view.js +1 -1
  109. package/dist/mcp/local/unity-evidence-view.test.js +22 -0
  110. package/dist/mcp/tools.js +51 -21
  111. package/dist/rule-lab/analyze.d.ts +2 -1
  112. package/dist/rule-lab/analyze.js +94 -59
  113. package/dist/rule-lab/analyze.test.js +238 -20
  114. package/dist/rule-lab/curate.d.ts +2 -1
  115. package/dist/rule-lab/curate.js +24 -3
  116. package/dist/rule-lab/curate.test.js +65 -0
  117. package/dist/rule-lab/curation-input-builder.d.ts +45 -0
  118. package/dist/rule-lab/curation-input-builder.js +133 -0
  119. package/dist/rule-lab/promote.js +80 -7
  120. package/dist/rule-lab/promote.test.js +150 -0
  121. package/dist/rule-lab/review-pack.d.ts +3 -0
  122. package/dist/rule-lab/review-pack.js +41 -1
  123. package/dist/rule-lab/review-pack.test.js +67 -0
  124. package/dist/rule-lab/types.d.ts +29 -0
  125. package/dist/types/pipeline.d.ts +3 -0
  126. package/package.json +3 -2
  127. package/scripts/run-node-tests.mjs +61 -0
  128. package/skills/_shared/unity-rule-authoring-contract.md +64 -0
  129. package/skills/_shared/unity-runtime-process-contract.md +16 -0
  130. package/skills/gitnexus-cli.md +8 -0
  131. package/skills/gitnexus-debugging.md +9 -0
  132. package/skills/gitnexus-exploring.md +66 -18
  133. package/skills/gitnexus-guide.md +42 -3
  134. package/skills/gitnexus-impact-analysis.md +8 -0
  135. package/skills/gitnexus-pr-review.md +8 -0
  136. package/skills/gitnexus-refactoring.md +8 -0
  137. package/skills/gitnexus-unity-rule-gen.md +66 -312
@@ -1,5 +1,6 @@
1
1
  import { generateId } from '../../lib/utils.js';
2
2
  const RULE_EDGE_CONFIDENCE = 0.75;
3
+ const RULE_ANOMALY_PREVIEW_LIMIT = 5;
3
4
  export function applyUnityRuntimeBindingRules(graph, rules, config) {
4
5
  const ruleResults = [];
5
6
  let totalEdges = 0;
@@ -54,6 +55,24 @@ export function applyUnityRuntimeBindingRules(graph, rules, config) {
54
55
  else if (rel.type === 'UNITY_COMPONENT_INSTANCE')
55
56
  componentInstances.push(rel);
56
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
+ };
57
76
  // Pre-build scene file index: lowercase scene name → fileId[]
58
77
  const sceneFilesByName = new Map();
59
78
  for (const node of graph.iterNodes()) {
@@ -70,7 +89,10 @@ export function applyUnityRuntimeBindingRules(graph, rules, config) {
70
89
  for (const rule of rules) {
71
90
  let ruleEdges = 0;
72
91
  for (const binding of rule.resource_bindings ?? []) {
73
- ruleEdges += processBinding(binding, rule.id, assetGuidRefs, componentInstances, methodsByClassId, containerNodes, 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);
74
96
  }
75
97
  if (rule.lifecycle_overrides?.additional_entry_points?.length) {
76
98
  ruleEdges += processLifecycleOverrides(rule, methodsByClassId, containerNodes, addSyntheticEdge);
@@ -78,43 +100,58 @@ export function applyUnityRuntimeBindingRules(graph, rules, config) {
78
100
  totalEdges += ruleEdges;
79
101
  ruleResults.push({ ruleId: rule.id, edgesInjected: ruleEdges });
80
102
  }
81
- return { edgesInjected: totalEdges, ruleResults };
103
+ return {
104
+ edgesInjected: totalEdges,
105
+ ruleResults,
106
+ diagnostics: finalizeRuleBindingDiagnostics(executionState.diagnostics, totalEdges),
107
+ };
82
108
  }
83
- function findMethodsOnResource(resourceFileId, componentInstances, methodsByClassId, entryPoints) {
84
- 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
+ }
85
122
  const entrySet = new Set(entryPoints);
86
- for (const ci of componentInstances) {
87
- if (ci.targetId !== resourceFileId)
88
- continue;
89
- const classId = ci.sourceId;
90
- for (const method of methodsByClassId.get(classId) ?? []) {
91
- if (entrySet.has(method.properties.name))
92
- results.push(method);
93
- }
123
+ const results = [];
124
+ for (const method of methods) {
125
+ if (entrySet.has(method.properties.name))
126
+ results.push(method);
94
127
  }
128
+ executionState.resourceMethodLookupCache.set(cacheKey, results);
95
129
  return results;
96
130
  }
97
- function processBinding(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, containerNodes, sceneFilesByName, addEdge) {
131
+ function processBinding(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, containerNodes, sceneFilesByName, prefabSourceTargetsBySource, executionState, addEdge) {
98
132
  if (binding.kind === 'asset_ref_loads_components') {
99
- return processAssetRefLoadsComponents(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, addEdge);
133
+ return processAssetRefLoadsComponents(binding, ruleId, assetGuidRefs, executionState, addEdge);
100
134
  }
101
135
  if (binding.kind === 'method_triggers_field_load') {
102
- return processMethodTriggersFieldLoad(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, containerNodes, addEdge);
136
+ return processMethodTriggersFieldLoad(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, containerNodes, executionState, addEdge);
103
137
  }
104
138
  if (binding.kind === 'method_triggers_scene_load') {
105
- return processMethodTriggersSceneLoad(binding, ruleId, componentInstances, methodsByClassId, containerNodes, sceneFilesByName, addEdge);
139
+ return processMethodTriggersSceneLoad(binding, ruleId, componentInstances, methodsByClassId, containerNodes, sceneFilesByName, prefabSourceTargetsBySource, executionState, addEdge);
106
140
  }
107
141
  if (binding.kind === 'method_triggers_method') {
108
142
  return processMethodTriggersMethod(binding, ruleId, methodsByClassId, containerNodes, addEdge);
109
143
  }
144
+ addAnomaly(executionState.diagnostics, `rule=${ruleId}: unsupported resource_binding kind "${binding.kind}"`);
110
145
  return 0;
111
146
  }
112
- function processAssetRefLoadsComponents(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, addEdge) {
147
+ function processAssetRefLoadsComponents(binding, ruleId, assetGuidRefs, executionState, addEdge) {
113
148
  let count = 0;
114
149
  const pattern = binding.ref_field_pattern ? new RegExp(binding.ref_field_pattern) : null;
115
150
  const entryPoints = binding.target_entry_points ?? [];
116
- 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`);
117
153
  return 0;
154
+ }
118
155
  const runtimeRootId = generateId('Method', 'unity-runtime-root');
119
156
  for (const ref of assetGuidRefs) {
120
157
  let fieldName = '';
@@ -127,7 +164,7 @@ function processAssetRefLoadsComponents(binding, ruleId, assetGuidRefs, componen
127
164
  }
128
165
  if (!pattern.test(fieldName))
129
166
  continue;
130
- const targetMethods = findMethodsOnResource(ref.targetId, componentInstances, methodsByClassId, entryPoints);
167
+ const targetMethods = findMethodsOnResource(ref.targetId, executionState, entryPoints);
131
168
  for (const method of targetMethods) {
132
169
  if (addEdge(runtimeRootId, method.id, `unity-rule-resource-load:${ruleId}`))
133
170
  count++;
@@ -135,15 +172,17 @@ function processAssetRefLoadsComponents(binding, ruleId, assetGuidRefs, componen
135
172
  }
136
173
  return count;
137
174
  }
138
- function processMethodTriggersFieldLoad(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, containerNodes, addEdge) {
175
+ function processMethodTriggersFieldLoad(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, containerNodes, executionState, addEdge) {
139
176
  let count = 0;
140
177
  const classPattern = binding.host_class_pattern ? new RegExp(binding.host_class_pattern) : null;
141
178
  const loaderMethodNames = new Set(binding.loader_methods ?? []);
142
179
  const entryPoints = binding.target_entry_points ?? [];
143
180
  const defaultEntryPoints = ['OnEnable', 'Awake', 'Start'];
144
181
  const resolvedEntryPoints = entryPoints.length > 0 ? entryPoints : defaultEntryPoints;
145
- 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`);
146
184
  return 0;
185
+ }
147
186
  // Build asset ref index by source file
148
187
  const refsBySource = new Map();
149
188
  for (const ref of assetGuidRefs) {
@@ -167,7 +206,7 @@ function processMethodTriggersFieldLoad(binding, ruleId, assetGuidRefs, componen
167
206
  // Follow asset refs from those resource files
168
207
  for (const resourceFileId of resourceFileIds) {
169
208
  for (const ref of refsBySource.get(resourceFileId) ?? []) {
170
- const targetMethods = findMethodsOnResource(ref.targetId, componentInstances, methodsByClassId, resolvedEntryPoints);
209
+ const targetMethods = findMethodsOnResource(ref.targetId, executionState, resolvedEntryPoints);
171
210
  for (const loader of loaders) {
172
211
  for (const target of targetMethods) {
173
212
  if (addEdge(loader.id, target.id, `unity-rule-loader-bridge:${ruleId}`))
@@ -179,7 +218,7 @@ function processMethodTriggersFieldLoad(binding, ruleId, assetGuidRefs, componen
179
218
  }
180
219
  return count;
181
220
  }
182
- function processMethodTriggersSceneLoad(binding, ruleId, componentInstances, methodsByClassId, containerNodes, sceneFilesByName, addEdge) {
221
+ function processMethodTriggersSceneLoad(binding, ruleId, componentInstances, methodsByClassId, containerNodes, sceneFilesByName, prefabSourceTargetsBySource, executionState, addEdge) {
183
222
  const classPattern = binding.host_class_pattern ? new RegExp(binding.host_class_pattern) : null;
184
223
  const loaderMethodNames = new Set(binding.loader_methods ?? []);
185
224
  const sceneName = binding.scene_name;
@@ -187,11 +226,15 @@ function processMethodTriggersSceneLoad(binding, ruleId, componentInstances, met
187
226
  const entryPoints = (binding.target_entry_points ?? []).length > 0
188
227
  ? binding.target_entry_points
189
228
  : defaultEntryPoints;
190
- 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`);
191
231
  return 0;
232
+ }
192
233
  const sceneFileIds = sceneFilesByName.get(sceneName.toLowerCase()) ?? [];
193
- if (sceneFileIds.length === 0)
234
+ if (sceneFileIds.length === 0) {
235
+ addAnomaly(executionState.diagnostics, `rule=${ruleId}: scene "${sceneName}" not found in File(.unity) index`);
194
236
  return 0;
237
+ }
195
238
  let count = 0;
196
239
  for (const cls of containerNodes) {
197
240
  if (!classPattern.test(cls.properties.name))
@@ -201,17 +244,81 @@ function processMethodTriggersSceneLoad(binding, ruleId, componentInstances, met
201
244
  if (loaders.length === 0)
202
245
  continue;
203
246
  for (const sceneFileId of sceneFileIds) {
204
- const targetMethods = findMethodsOnResource(sceneFileId, componentInstances, methodsByClassId, entryPoints);
205
- for (const loader of loaders) {
206
- for (const target of targetMethods) {
207
- if (addEdge(loader.id, target.id, `unity-rule-scene-load:${ruleId}`))
208
- 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
+ }
209
255
  }
210
256
  }
211
257
  }
212
258
  }
213
259
  return count;
214
260
  }
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
+ }
215
322
  function processMethodTriggersMethod(binding, ruleId, methodsByClassId, containerNodes, addEdge) {
216
323
  const { source_class_pattern, source_method, target_class_pattern, target_method } = binding;
217
324
  if (!source_class_pattern || !source_method || !target_class_pattern || !target_method)
@@ -258,3 +365,44 @@ function processLifecycleOverrides(rule, methodsByClassId, containerNodes, addEd
258
365
  }
259
366
  return count;
260
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
+ }
@@ -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 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ const here = path.dirname(fileURLToPath(import.meta.url));
7
+ const gitnexusRoot = path.resolve(here, '../../..');
8
+ const repoRoot = path.resolve(gitnexusRoot, '..');
9
+ test('scan-context carrier contract matches code and docs', async () => {
10
+ const bindingDoc = await fs.readFile(path.join(repoRoot, 'UNITY_RESOURCE_BINDING.md'), 'utf-8');
11
+ const ssot = await fs.readFile(path.join(repoRoot, 'docs/unity-runtime-process-source-of-truth.md'), 'utf-8');
12
+ const design = await fs.readFile(path.join(repoRoot, 'docs/plans/2026-04-10-prefab-source-streaming-consumption-memory-optimization-design.md'), 'utf-8');
13
+ const scanContextCode = await fs.readFile(path.join(gitnexusRoot, 'src/core/unity/scan-context.ts'), 'utf-8');
14
+ const processorCode = await fs.readFile(path.join(gitnexusRoot, 'src/core/ingestion/unity-resource-processor.ts'), 'utf-8');
15
+ const pipelineCode = await fs.readFile(path.join(gitnexusRoot, 'src/core/ingestion/pipeline.ts'), 'utf-8');
16
+ assert.match(bindingDoc, /scan-context.*承载器|resource signal carrier|scan-context.*carrier/i);
17
+ assert.match(bindingDoc, /streaming delivery|incremental consumption/i);
18
+ assert.match(ssot, /As-Built[\s\S]*Design Direction/i);
19
+ assert.match(ssot, /scan-context[\s\S]*does not write graph/i);
20
+ assert.match(ssot, /统一消费点契约/i);
21
+ assert.match(design, /scan-context[\s\S]*(统一消费|unified consumer)/i);
22
+ assert.match(scanContextCode, /streamPrefabSourceRefs/);
23
+ assert.match(processorCode, /streamPrefabSourceRefs\(/);
24
+ assert.match(processorCode, /emitPrefabSourceGuidRefsFromScanContext/);
25
+ assert.doesNotMatch(processorCode, /emitPrefabSourceGuidRefs\(/);
26
+ assert.doesNotMatch(scanContextCode, /addRelationship\(/);
27
+ assert.ok(pipelineCode.indexOf('processUnityResources(') >= 0
28
+ && pipelineCode.indexOf('applyUnityLifecycleSyntheticCalls(') >= 0
29
+ && pipelineCode.indexOf('processUnityResources(') < pipelineCode.indexOf('applyUnityLifecycleSyntheticCalls('));
30
+ });
@@ -0,0 +1,25 @@
1
+ export interface PrefabSourceScanRow {
2
+ sourceResourcePath: string;
3
+ targetGuid: string;
4
+ targetResourcePath?: string;
5
+ fileId?: string;
6
+ fieldName: 'm_SourcePrefab';
7
+ sourceLayer: 'scene' | 'prefab';
8
+ }
9
+ export interface StreamPrefabSourceRefsInput {
10
+ repoRoot: string;
11
+ resourceFiles: string[];
12
+ assetGuidToPath: Map<string, string>;
13
+ queue?: {
14
+ enabled?: boolean;
15
+ maxDepth?: number;
16
+ };
17
+ hooks?: {
18
+ onFileOpen?: (resourcePath: string) => void;
19
+ onYield?: (row: PrefabSourceScanRow) => void;
20
+ onQueueDepth?: (depth: number) => void;
21
+ onFileError?: (resourcePath: string, error: unknown) => void;
22
+ };
23
+ }
24
+ export declare function collectPrefabSourceRefs(args: StreamPrefabSourceRefsInput): Promise<PrefabSourceScanRow[]>;
25
+ export declare function streamPrefabSourceRefs(args: StreamPrefabSourceRefsInput): AsyncGenerator<PrefabSourceScanRow>;
@@ -0,0 +1,152 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createInterface } from 'node:readline';
4
+ const PREFAB_INSTANCE_PATTERN = /^\s*PrefabInstance:\s*$/;
5
+ const YAML_OBJECT_BOUNDARY_PATTERN = /^\s*---\s*!u!\d+\s+&/;
6
+ const SOURCE_PREFAB_LINE_PATTERN = /m_SourcePrefab\s*:\s*\{[^}]*fileID\s*:\s*([^,\s}]+)[^}]*guid\s*:\s*([0-9a-fA-F]{32})[^}]*\}/;
7
+ const ZERO_GUID = '00000000000000000000000000000000';
8
+ export async function collectPrefabSourceRefs(args) {
9
+ const rows = [];
10
+ for await (const row of streamPrefabSourceRefs(args)) {
11
+ rows.push(row);
12
+ }
13
+ return rows;
14
+ }
15
+ export async function* streamPrefabSourceRefs(args) {
16
+ const resources = [...new Set((args.resourceFiles || []).map((value) => normalizePath(value)))]
17
+ .filter((value) => value.endsWith('.unity') || value.endsWith('.prefab'));
18
+ if (!args.queue?.enabled) {
19
+ for await (const row of streamPrefabSourceRefsSequential(resources, args)) {
20
+ yield row;
21
+ }
22
+ return;
23
+ }
24
+ const maxDepth = Math.max(1, Number(args.queue.maxDepth || 64));
25
+ const queue = [];
26
+ const waitingReaders = [];
27
+ const waitingWriters = [];
28
+ let producerDone = false;
29
+ let producerError;
30
+ const notifyDepth = () => args.hooks?.onQueueDepth?.(queue.length);
31
+ const push = async (row) => {
32
+ while (queue.length >= maxDepth) {
33
+ await new Promise((resolve) => waitingWriters.push(resolve));
34
+ }
35
+ if (waitingReaders.length > 0) {
36
+ const resolveReader = waitingReaders.shift();
37
+ resolveReader(row);
38
+ return;
39
+ }
40
+ queue.push(row);
41
+ notifyDepth();
42
+ };
43
+ const shift = async () => {
44
+ if (queue.length > 0) {
45
+ const value = queue.shift();
46
+ notifyDepth();
47
+ const resolveWriter = waitingWriters.shift();
48
+ if (resolveWriter)
49
+ resolveWriter();
50
+ return value;
51
+ }
52
+ if (producerDone) {
53
+ return null;
54
+ }
55
+ return await new Promise((resolve) => waitingReaders.push(resolve));
56
+ };
57
+ const finishReaders = () => {
58
+ while (waitingReaders.length > 0) {
59
+ const resolveReader = waitingReaders.shift();
60
+ resolveReader(null);
61
+ }
62
+ };
63
+ const finishWriters = () => {
64
+ while (waitingWriters.length > 0) {
65
+ const resolveWriter = waitingWriters.shift();
66
+ resolveWriter();
67
+ }
68
+ };
69
+ const producer = (async () => {
70
+ try {
71
+ for await (const row of streamPrefabSourceRefsSequential(resources, args)) {
72
+ await push(row);
73
+ }
74
+ }
75
+ catch (error) {
76
+ producerError = error;
77
+ }
78
+ finally {
79
+ producerDone = true;
80
+ finishReaders();
81
+ finishWriters();
82
+ }
83
+ })();
84
+ while (true) {
85
+ const row = await shift();
86
+ if (!row)
87
+ break;
88
+ yield row;
89
+ }
90
+ await producer;
91
+ if (producerError) {
92
+ throw producerError;
93
+ }
94
+ }
95
+ async function* streamPrefabSourceRefsSequential(resources, args) {
96
+ const hooks = args.hooks;
97
+ for (const resourcePath of resources) {
98
+ hooks?.onFileOpen?.(resourcePath);
99
+ const absolutePath = path.join(args.repoRoot, resourcePath);
100
+ const stream = createReadStream(absolutePath, { encoding: 'utf-8' });
101
+ const reader = createInterface({
102
+ input: stream,
103
+ crlfDelay: Infinity,
104
+ });
105
+ try {
106
+ let inPrefabInstance = false;
107
+ for await (const line of reader) {
108
+ if (YAML_OBJECT_BOUNDARY_PATTERN.test(line)) {
109
+ inPrefabInstance = false;
110
+ }
111
+ if (PREFAB_INSTANCE_PATTERN.test(line)) {
112
+ inPrefabInstance = true;
113
+ continue;
114
+ }
115
+ if (!inPrefabInstance)
116
+ continue;
117
+ const match = line.match(SOURCE_PREFAB_LINE_PATTERN);
118
+ if (!match)
119
+ continue;
120
+ const fileId = String(match[1] || '').trim();
121
+ const guid = String(match[2] || '').trim().toLowerCase();
122
+ if (!guid || guid === ZERO_GUID)
123
+ continue;
124
+ const targetResourcePath = normalizePath(args.assetGuidToPath.get(guid) || args.assetGuidToPath.get(guid.toLowerCase()) || '');
125
+ if (!targetResourcePath || !targetResourcePath.endsWith('.prefab'))
126
+ continue;
127
+ const row = {
128
+ sourceResourcePath: resourcePath,
129
+ targetGuid: guid,
130
+ targetResourcePath,
131
+ fileId: fileId || undefined,
132
+ fieldName: 'm_SourcePrefab',
133
+ sourceLayer: resourcePath.endsWith('.unity') ? 'scene' : 'prefab',
134
+ };
135
+ hooks?.onYield?.(row);
136
+ yield row;
137
+ inPrefabInstance = false;
138
+ }
139
+ }
140
+ catch (error) {
141
+ hooks?.onFileError?.(resourcePath, error);
142
+ continue;
143
+ }
144
+ finally {
145
+ reader.close();
146
+ stream.destroy();
147
+ }
148
+ }
149
+ }
150
+ function normalizePath(value) {
151
+ return String(value || '').replace(/\\/g, '/').trim();
152
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
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 { streamPrefabSourceRefs } from './prefab-source-scan.js';
6
+ const here = path.dirname(fileURLToPath(import.meta.url));
7
+ const fixtureRoot = path.resolve(here, '../../../src/core/unity/__fixtures__/mini-unity');
8
+ const assetGuidToPath = new Map([['99999999999999999999999999999999', 'Assets/Prefabs/BattleMode.prefab']]);
9
+ const scopedFiles = ['Assets/Scene/MainUIManager.unity', 'Assets/Prefabs/BattleMode.prefab'];
10
+ test('same source can yield prefab-source rows while script-guid flow remains independent', async () => {
11
+ const rows = [];
12
+ for await (const row of streamPrefabSourceRefs({
13
+ repoRoot: fixtureRoot,
14
+ resourceFiles: ['Assets/Scene/MainUIManager.unity'],
15
+ assetGuidToPath,
16
+ })) {
17
+ rows.push(row);
18
+ }
19
+ assert.ok(rows.length > 0);
20
+ assert.equal(rows.every((r) => r.fieldName === 'm_SourcePrefab'), true);
21
+ });
22
+ test('streamPrefabSourceRefs does not open second file before first row is yielded', async () => {
23
+ const probe = [];
24
+ const iterator = streamPrefabSourceRefs({
25
+ repoRoot: fixtureRoot,
26
+ resourceFiles: scopedFiles,
27
+ assetGuidToPath,
28
+ hooks: {
29
+ onFileOpen: (filePath) => probe.push(`open:${filePath}`),
30
+ onYield: () => probe.push('yield'),
31
+ },
32
+ })[Symbol.asyncIterator]();
33
+ const first = await iterator.next();
34
+ assert.equal(first.done, false);
35
+ assert.equal(first.value.fieldName, 'm_SourcePrefab');
36
+ assert.equal(probe.includes('open:Assets/Prefabs/BattleMode.prefab'), false);
37
+ await iterator.return?.(undefined);
38
+ });
39
+ test('producer rows are immutable snapshots (consumer mutation does not backflow)', async () => {
40
+ for await (const row of streamPrefabSourceRefs({
41
+ repoRoot: fixtureRoot,
42
+ resourceFiles: scopedFiles,
43
+ assetGuidToPath,
44
+ })) {
45
+ const copy = { ...row };
46
+ copy.targetResourcePath = '__PLACEHOLDER__';
47
+ }
48
+ const again = [];
49
+ for await (const row of streamPrefabSourceRefs({
50
+ repoRoot: fixtureRoot,
51
+ resourceFiles: scopedFiles,
52
+ assetGuidToPath,
53
+ })) {
54
+ again.push(row);
55
+ }
56
+ assert.equal(again.some((r) => r.targetResourcePath === '__PLACEHOLDER__'), false);
57
+ });
58
+ test('bounded queue backpressure never exceeds configured depth when decoupled mode is enabled', async () => {
59
+ const depthSamples = [];
60
+ for await (const _row of streamPrefabSourceRefs({
61
+ repoRoot: fixtureRoot,
62
+ resourceFiles: scopedFiles,
63
+ assetGuidToPath,
64
+ queue: { enabled: true, maxDepth: 64 },
65
+ hooks: { onQueueDepth: (depth) => depthSamples.push(depth) },
66
+ })) {
67
+ await new Promise((resolve) => setTimeout(resolve, 1));
68
+ }
69
+ assert.equal(depthSamples.every((depth) => depth <= 64), true);
70
+ });
@@ -1,3 +1,4 @@
1
+ import type { StreamPrefabSourceRefsInput } from './prefab-source-scan.js';
1
2
  import type { UnityResourceGuidHit } from './resource-hit-scanner.js';
2
3
  import type { UnityObjectBlock } from './yaml-object-graph.js';
3
4
  import type { UnityParitySeed } from '../ingestion/unity-parity-seed.js';
@@ -10,6 +11,14 @@ export interface UnitySymbolDeclaration {
10
11
  symbol: string;
11
12
  scriptPath: string;
12
13
  }
14
+ export interface UnityPrefabSourceRef {
15
+ sourceResourcePath: string;
16
+ targetGuid: string;
17
+ targetResourcePath?: string;
18
+ fileId?: string;
19
+ fieldName: 'm_SourcePrefab';
20
+ sourceLayer: 'scene' | 'prefab';
21
+ }
13
22
  export interface UnityScanContext {
14
23
  symbolToScriptPaths: Map<string, string[]>;
15
24
  symbolToCanonicalScriptPath: Map<string, string>;
@@ -21,6 +30,9 @@ export interface UnityScanContext {
21
30
  assetGuidToPath?: Map<string, string>;
22
31
  uxmlGuidToPath?: Map<string, string>;
23
32
  ussGuidToPath?: Map<string, string>;
33
+ prefabSourceRefs: UnityPrefabSourceRef[];
34
+ streamPrefabSourceRefs: (options?: Pick<StreamPrefabSourceRefsInput, 'queue' | 'hooks'>) => AsyncIterable<UnityPrefabSourceRef>;
35
+ resourceFiles: string[];
24
36
  resourceDocCache: Map<string, UnityObjectBlock[]>;
25
37
  }
26
38
  export declare function buildUnityScanContext(input: BuildScanContextInput): Promise<UnityScanContext>;