@veewo/gitnexus 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/benchmark/agent-context/runner.js +3 -0
- package/dist/benchmark/agent-context/runner.test.js +22 -0
- package/dist/benchmark/agent-context/tool-runner.d.ts +7 -6
- package/dist/benchmark/agent-safe-query-context/io.d.ts +2 -0
- package/dist/benchmark/agent-safe-query-context/io.js +86 -0
- package/dist/benchmark/agent-safe-query-context/io.test.d.ts +1 -0
- package/dist/benchmark/agent-safe-query-context/io.test.js +13 -0
- package/dist/benchmark/agent-safe-query-context/report.d.ts +57 -0
- package/dist/benchmark/agent-safe-query-context/report.js +159 -0
- package/dist/benchmark/agent-safe-query-context/report.test.d.ts +1 -0
- package/dist/benchmark/agent-safe-query-context/report.test.js +362 -0
- package/dist/benchmark/agent-safe-query-context/runner.d.ts +44 -0
- package/dist/benchmark/agent-safe-query-context/runner.js +406 -0
- package/dist/benchmark/agent-safe-query-context/runner.test.d.ts +1 -0
- package/dist/benchmark/agent-safe-query-context/runner.test.js +290 -0
- package/dist/benchmark/agent-safe-query-context/semantic-tuple.d.ts +20 -0
- package/dist/benchmark/agent-safe-query-context/semantic-tuple.js +225 -0
- package/dist/benchmark/agent-safe-query-context/semantic-tuple.test.d.ts +1 -0
- package/dist/benchmark/agent-safe-query-context/semantic-tuple.test.js +122 -0
- package/dist/benchmark/agent-safe-query-context/subagent-live.d.ts +47 -0
- package/dist/benchmark/agent-safe-query-context/subagent-live.js +128 -0
- package/dist/benchmark/agent-safe-query-context/subagent-live.test.d.ts +1 -0
- package/dist/benchmark/agent-safe-query-context/subagent-live.test.js +155 -0
- package/dist/benchmark/agent-safe-query-context/telemetry-tool.d.ts +9 -0
- package/dist/benchmark/agent-safe-query-context/telemetry-tool.js +77 -0
- package/dist/benchmark/agent-safe-query-context/types.d.ts +61 -0
- package/dist/benchmark/agent-safe-query-context/types.js +8 -0
- package/dist/benchmark/runtime-poc/provenance-artifact.d.ts +47 -0
- package/dist/benchmark/runtime-poc/provenance-artifact.js +89 -0
- package/dist/benchmark/runtime-poc/runner.d.ts +31 -0
- package/dist/benchmark/runtime-poc/runner.js +163 -0
- package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.d.ts +8 -0
- package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.js +21 -0
- package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.d.ts +0 -1
- package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.js +53 -51
- package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.test.js +0 -1
- package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.d.ts +1 -1
- package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.js +82 -18
- package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.js +1 -2
- package/dist/benchmark/u2-e2e/retrieval-runner.js +15 -7
- package/dist/benchmark/u2-e2e/retrieval-runner.test.js +46 -0
- package/dist/cli/ai-context.js +2 -12
- package/dist/cli/ai-context.test.js +8 -0
- package/dist/cli/analyze-runtime-summary.js +1 -0
- package/dist/cli/analyze-runtime-summary.test.js +2 -0
- package/dist/cli/analyze-summary.d.ts +2 -0
- package/dist/cli/analyze-summary.js +24 -0
- package/dist/cli/analyze-summary.test.js +65 -1
- package/dist/cli/analyze.js +5 -1
- package/dist/cli/benchmark-agent-safe-query-context.d.ts +20 -0
- package/dist/cli/benchmark-agent-safe-query-context.js +39 -0
- package/dist/cli/benchmark-agent-safe-query-context.test.d.ts +1 -0
- package/dist/cli/benchmark-agent-safe-query-context.test.js +271 -0
- package/dist/cli/benchmark.d.ts +29 -0
- package/dist/cli/benchmark.js +55 -0
- package/dist/cli/index.js +23 -0
- package/dist/cli/rule-lab.d.ts +3 -7
- package/dist/cli/rule-lab.js +13 -22
- package/dist/cli/rule-lab.test.js +23 -3
- package/dist/cli/tool.d.ts +2 -0
- package/dist/cli/tool.js +2 -0
- package/dist/core/config/unity-config.d.ts +0 -1
- package/dist/core/config/unity-config.js +0 -1
- package/dist/core/ingestion/pipeline.js +35 -6
- package/dist/core/ingestion/unity-lifecycle-synthetic-calls.test.js +18 -20
- package/dist/core/ingestion/unity-parity-seed.d.ts +2 -1
- package/dist/core/ingestion/unity-parity-seed.js +8 -0
- package/dist/core/ingestion/unity-resource-processor.d.ts +11 -0
- package/dist/core/ingestion/unity-resource-processor.js +102 -0
- package/dist/core/ingestion/unity-resource-processor.test.js +449 -0
- package/dist/core/ingestion/unity-runtime-binding-rules.d.ts +15 -0
- package/dist/core/ingestion/unity-runtime-binding-rules.js +178 -30
- package/dist/core/lbug/csv-generator.test.js +2 -2
- package/dist/core/unity/doc-contract.test.d.ts +1 -0
- package/dist/core/unity/doc-contract.test.js +30 -0
- package/dist/core/unity/prefab-source-scan.d.ts +25 -0
- package/dist/core/unity/prefab-source-scan.js +152 -0
- package/dist/core/unity/prefab-source-scan.test.d.ts +1 -0
- package/dist/core/unity/prefab-source-scan.test.js +70 -0
- package/dist/core/unity/scan-context.d.ts +12 -0
- package/dist/core/unity/scan-context.js +50 -2
- package/dist/core/unity/scan-context.test.js +74 -0
- package/dist/mcp/local/agent-safe-response.d.ts +10 -0
- package/dist/mcp/local/agent-safe-response.js +639 -0
- package/dist/mcp/local/derived-process-reader.js +1 -1
- package/dist/mcp/local/local-backend.d.ts +18 -1
- package/dist/mcp/local/local-backend.js +319 -125
- package/dist/mcp/local/process-confidence.d.ts +1 -2
- package/dist/mcp/local/process-confidence.js +0 -3
- package/dist/mcp/local/process-confidence.test.js +4 -2
- package/dist/mcp/local/process-evidence.d.ts +1 -8
- package/dist/mcp/local/process-evidence.js +1 -23
- package/dist/mcp/local/process-evidence.test.js +2 -16
- package/dist/mcp/local/process-ref.d.ts +1 -1
- package/dist/mcp/local/runtime-chain-closure-evaluator.d.ts +33 -0
- package/dist/mcp/local/runtime-chain-closure-evaluator.js +273 -0
- package/dist/mcp/local/runtime-chain-graph-candidates.d.ts +23 -0
- package/dist/mcp/local/runtime-chain-graph-candidates.js +131 -0
- package/dist/mcp/local/runtime-chain-verify.d.ts +1 -1
- package/dist/mcp/local/runtime-chain-verify.js +149 -138
- package/dist/mcp/local/runtime-chain-verify.test.js +126 -68
- package/dist/mcp/local/runtime-claim-rule-registry.d.ts +4 -0
- package/dist/mcp/local/runtime-claim-rule-registry.js +4 -0
- package/dist/mcp/local/runtime-claim-rule-registry.test.js +37 -4
- package/dist/mcp/local/runtime-claim.d.ts +11 -0
- package/dist/mcp/local/runtime-claim.js +28 -0
- package/dist/mcp/local/unity-evidence-view.d.ts +1 -1
- package/dist/mcp/local/unity-evidence-view.js +1 -1
- package/dist/mcp/local/unity-evidence-view.test.js +22 -0
- package/dist/mcp/tools.js +51 -21
- package/dist/rule-lab/analyze.d.ts +2 -1
- package/dist/rule-lab/analyze.js +94 -59
- package/dist/rule-lab/analyze.test.js +238 -20
- package/dist/rule-lab/curate.d.ts +2 -1
- package/dist/rule-lab/curate.js +24 -3
- package/dist/rule-lab/curate.test.js +65 -0
- package/dist/rule-lab/curation-input-builder.d.ts +45 -0
- package/dist/rule-lab/curation-input-builder.js +133 -0
- package/dist/rule-lab/promote.js +80 -7
- package/dist/rule-lab/promote.test.js +150 -0
- package/dist/rule-lab/review-pack.d.ts +3 -0
- package/dist/rule-lab/review-pack.js +41 -1
- package/dist/rule-lab/review-pack.test.js +67 -0
- package/dist/rule-lab/types.d.ts +29 -0
- package/dist/types/pipeline.d.ts +3 -0
- package/package.json +4 -3
- package/scripts/run-node-tests.mjs +61 -0
- package/skills/_shared/unity-rule-authoring-contract.md +64 -0
- package/skills/_shared/unity-runtime-process-contract.md +16 -0
- package/skills/gitnexus-cli.md +8 -0
- package/skills/gitnexus-debugging.md +9 -0
- package/skills/gitnexus-exploring.md +66 -18
- package/skills/gitnexus-guide.md +42 -3
- package/skills/gitnexus-impact-analysis.md +8 -0
- package/skills/gitnexus-pr-review.md +8 -0
- package/skills/gitnexus-refactoring.md +8 -0
- 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
|
-
|
|
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 {
|
|
103
|
+
return {
|
|
104
|
+
edgesInjected: totalEdges,
|
|
105
|
+
ruleResults,
|
|
106
|
+
diagnostics: finalizeRuleBindingDiagnostics(executionState.diagnostics, totalEdges),
|
|
107
|
+
};
|
|
82
108
|
}
|
|
83
|
-
function findMethodsOnResource(resourceFileId,
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
205
|
-
for (const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
5
|
-
const cache = new FileContentCache('/tmp/repo',
|
|
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>;
|