@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.
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 +4 -3
  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
@@ -0,0 +1,55 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { writeRuntimeProvenanceArtifact, } from '../benchmark/runtime-poc/provenance-artifact.js';
4
+ import { runRuntimePocBenchmark } from '../benchmark/runtime-poc/runner.js';
5
+ async function loadRuntimePocRecords(recordsPath) {
6
+ if (!recordsPath)
7
+ return [];
8
+ const raw = await fs.readFile(path.resolve(recordsPath), 'utf-8');
9
+ const parsed = JSON.parse(raw);
10
+ if (!Array.isArray(parsed)) {
11
+ throw new Error('runtime-poc records file must be a JSON array');
12
+ }
13
+ return parsed;
14
+ }
15
+ export async function benchmarkCommand(suite, options = {}) {
16
+ const normalizedSuite = String(suite || '').trim().toLowerCase();
17
+ if (normalizedSuite !== 'runtime-poc') {
18
+ throw new Error(`unsupported benchmark suite: ${suite}`);
19
+ }
20
+ const repo = String(options.repo || 'unknown-repo').trim();
21
+ const reportDir = path.resolve(options.reportDir || 'docs/reports/runtime-poc');
22
+ if (options.recordsPath) {
23
+ const records = await loadRuntimePocRecords(options.recordsPath);
24
+ const out = await writeRuntimeProvenanceArtifact({
25
+ reportDir,
26
+ repo,
27
+ records,
28
+ });
29
+ process.stdout.write(`runtime-poc provenance artifact written: ${out.artifactPath}\n`);
30
+ process.stdout.write(`runtime-poc provenance index updated: ${out.indexPath}\n`);
31
+ return {
32
+ artifactPath: out.artifactPath,
33
+ indexPath: out.indexPath,
34
+ sha256: out.sha256,
35
+ };
36
+ }
37
+ const run = await runRuntimePocBenchmark({
38
+ repo,
39
+ reportDir,
40
+ casesPath: options.casesPath,
41
+ });
42
+ process.stdout.write(`runtime-poc comparison report written: ${run.comparisonPath}\n`);
43
+ process.stdout.write(`runtime-poc markdown summary written: ${run.summaryPath}\n`);
44
+ process.stdout.write(`runtime-poc provenance artifact written: ${run.provenanceArtifactPath}\n`);
45
+ process.stdout.write(`runtime-poc provenance index updated: ${run.provenanceIndexPath}\n`);
46
+ return {
47
+ comparisonPath: run.comparisonPath,
48
+ summaryPath: run.summaryPath,
49
+ provenanceArtifactPath: run.provenanceArtifactPath,
50
+ provenanceIndexPath: run.provenanceIndexPath,
51
+ };
52
+ }
53
+ export async function benchmarkSuiteCommand(suite, options) {
54
+ return await benchmarkCommand(suite, options);
55
+ }
package/dist/cli/index.js CHANGED
@@ -86,6 +86,7 @@ program
86
86
  .option('-g, --goal <text>', 'What you want to find')
87
87
  .option('-l, --limit <n>', 'Max processes to return (default: 5)')
88
88
  .option('--content', 'Include full symbol source code')
89
+ .option('--response-profile <profile>', 'Response payload profile: slim|full', 'slim')
89
90
  .option('--scope-preset <preset>', 'Scope preset for retrieval: unity-gameplay|unity-all')
90
91
  .option('--unity-resources <mode>', 'Unity resource retrieval mode: off|on|auto', 'off')
91
92
  .option('--unity-hydration <mode>', 'Unity hydration mode when resources are enabled: parity|compact', 'compact')
@@ -101,6 +102,7 @@ program
101
102
  .option('-u, --uid <uid>', 'Direct symbol UID (zero-ambiguity lookup)')
102
103
  .option('-f, --file <path>', 'File path to disambiguate common names')
103
104
  .option('--content', 'Include full symbol source code')
105
+ .option('--response-profile <profile>', 'Response payload profile: slim|full', 'slim')
104
106
  .option('--unity-resources <mode>', 'Unity resource retrieval mode: off|on|auto', 'off')
105
107
  .option('--unity-hydration <mode>', 'Unity hydration mode when resources are enabled: parity|compact', 'compact')
106
108
  .option('--unity-evidence <mode>', 'Unity evidence payload mode: summary|focused|full', 'summary')
@@ -170,10 +172,31 @@ program
170
172
  .option('--scope-prefix <pathPrefix>', 'Analyze scope path prefix (repeatable)', collectValues, [])
171
173
  .option('--skip-analyze', 'Skip analyze stage and evaluate current index only')
172
174
  .action(createLazyAction(() => import('./benchmark-agent-context.js'), 'benchmarkAgentContextCommand'));
175
+ program
176
+ .command('benchmark-agent-safe-query-context <dataset>')
177
+ .description('Run the agent-safe Unity query/context benchmark')
178
+ .option('-r, --repo <name>', 'Target indexed repo')
179
+ .option('--repo-alias <name>', 'Analyze-time repo alias and default evaluation repo when --repo is omitted')
180
+ .option('--target-path <path>', 'Path to analyze before evaluation (required unless --skip-analyze)')
181
+ .option('--subagent-runs-dir <path>', 'Directory containing session-generated subagent run artifacts')
182
+ .option('--report-dir <path>', 'Output directory for benchmark-report.json and benchmark-summary.md', '.gitnexus/benchmark-agent-safe-query-context')
183
+ .option('--extensions <list>', 'Analyze extension filter (comma-separated, optional)')
184
+ .option('--scope-manifest <path>', 'Analyze scope manifest file')
185
+ .option('--scope-prefix <pathPrefix>', 'Analyze scope path prefix (repeatable)', collectValues, [])
186
+ .option('--skip-analyze', 'Skip analyze stage and evaluate current index only')
187
+ .action(createLazyAction(() => import('./benchmark-agent-safe-query-context.js'), 'benchmarkAgentSafeQueryContextCommand'));
173
188
  program
174
189
  .command('benchmark-u2-e2e')
175
190
  .description('Run fail-fast full neonspark U2 E2E benchmark and emit evidence reports')
176
191
  .option('--config <path>', 'Path to E2E config JSON')
177
192
  .option('--report-dir <path>', 'Output directory for reports')
178
193
  .action(createLazyAction(() => import('./benchmark-u2-e2e.js'), 'benchmarkU2E2ECommand'));
194
+ program
195
+ .command('benchmark <suite>')
196
+ .description('Run benchmark suite (currently supports: runtime-poc)')
197
+ .option('-r, --repo <name>', 'Target indexed repo')
198
+ .option('--report-dir <path>', 'Output directory for runtime-poc reports', 'docs/reports/runtime-poc')
199
+ .option('--cases-path <path>', 'Optional JSON cases file for runtime-poc comparison run')
200
+ .option('--records-path <path>', 'Optional JSON records file to emit provenance artifact only')
201
+ .action(createLazyAction(() => import('./benchmark.js'), 'benchmarkSuiteCommand'));
179
202
  program.parse(process.argv);
@@ -1,14 +1,9 @@
1
1
  import type { Command } from 'commander';
2
- declare const RULE_LAB_COMMANDS: readonly ["discover", "analyze", "review-pack", "curate", "promote", "regress"];
3
- type RuleLabHandlerName = 'ruleLabDiscoverCommand' | 'ruleLabAnalyzeCommand' | 'ruleLabReviewPackCommand' | 'ruleLabCurateCommand' | 'ruleLabPromoteCommand' | 'ruleLabRegressCommand';
2
+ declare const RULE_LAB_COMMANDS: readonly ["analyze", "review-pack", "curate", "promote", "regress"];
3
+ type RuleLabHandlerName = 'ruleLabAnalyzeCommand' | 'ruleLabReviewPackCommand' | 'ruleLabCurateCommand' | 'ruleLabPromoteCommand' | 'ruleLabRegressCommand';
4
4
  type LazyFactory = (handlerName: RuleLabHandlerName) => (...args: any[]) => void | Promise<void>;
5
5
  export declare function getRuleLabCommandNames(program: Command): string[];
6
6
  export declare function attachRuleLabCommands(program: Command, lazyFactory?: LazyFactory): void;
7
- export declare function ruleLabDiscoverCommand(options: {
8
- repoPath?: string;
9
- scope?: 'full' | 'diff';
10
- seed?: string;
11
- }): Promise<void>;
12
7
  export declare function ruleLabAnalyzeCommand(options: {
13
8
  repoPath?: string;
14
9
  runId: string;
@@ -30,6 +25,7 @@ export declare function ruleLabPromoteCommand(options: {
30
25
  repoPath?: string;
31
26
  runId: string;
32
27
  sliceId: string;
28
+ ruleVersion?: string;
33
29
  version?: string;
34
30
  }): Promise<void>;
35
31
  export declare function ruleLabRegressCommand(options: {
@@ -1,14 +1,13 @@
1
1
  import { writeSync } from 'node:fs';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
- import { discoverRuleLabRun } from '../rule-lab/discover.js';
5
4
  import { analyzeRuleLabSlice } from '../rule-lab/analyze.js';
6
5
  import { buildReviewPack } from '../rule-lab/review-pack.js';
7
6
  import { curateRuleLabSlice } from '../rule-lab/curate.js';
8
7
  import { promoteCuratedRules } from '../rule-lab/promote.js';
9
8
  import { runRuleLabRegress } from '../rule-lab/regress.js';
10
9
  import { compileRules } from '../rule-lab/compile.js';
11
- const RULE_LAB_COMMANDS = ['discover', 'analyze', 'review-pack', 'curate', 'promote', 'regress'];
10
+ const RULE_LAB_COMMANDS = ['analyze', 'review-pack', 'curate', 'promote', 'regress'];
12
11
  function output(data) {
13
12
  const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
14
13
  writeSync(1, `${text}\n`);
@@ -16,6 +15,11 @@ function output(data) {
16
15
  function resolveRepoPath(repoPath) {
17
16
  return path.resolve(repoPath || process.cwd());
18
17
  }
18
+ function assertConcreteRunSliceIds(runId, sliceId) {
19
+ if (/[<>]/.test(runId) || /[<>]/.test(sliceId)) {
20
+ throw new Error('Invalid run/slice id: placeholder values are not allowed');
21
+ }
22
+ }
19
23
  export function getRuleLabCommandNames(program) {
20
24
  const root = program.commands.find((command) => command.name() === 'rule-lab');
21
25
  if (!root)
@@ -27,8 +31,6 @@ export function attachRuleLabCommands(program, lazyFactory) {
27
31
  if (lazyFactory)
28
32
  return lazyFactory(handlerName);
29
33
  switch (handlerName) {
30
- case 'ruleLabDiscoverCommand':
31
- return (options) => ruleLabDiscoverCommand(options);
32
34
  case 'ruleLabAnalyzeCommand':
33
35
  return (options) => ruleLabAnalyzeCommand(options);
34
36
  case 'ruleLabReviewPackCommand':
@@ -47,13 +49,7 @@ export function attachRuleLabCommands(program, lazyFactory) {
47
49
  };
48
50
  const root = program
49
51
  .command('rule-lab')
50
- .description('Offline rule-lab workflow for discover/analyze/review-pack/curate/promote/regress');
51
- root
52
- .command('discover')
53
- .option('--repo-path <path>', 'Repository path (default: cwd)')
54
- .option('--scope <scope>', 'Discovery scope: full|diff', 'full')
55
- .option('--seed <seed>', 'Optional deterministic seed')
56
- .action(action('ruleLabDiscoverCommand'));
52
+ .description('Offline rule-lab workflow for analyze/review-pack/curate/promote/regress');
57
53
  root
58
54
  .command('analyze')
59
55
  .requiredOption('--run-id <id>', 'Rule-lab run id')
@@ -79,7 +75,7 @@ export function attachRuleLabCommands(program, lazyFactory) {
79
75
  .requiredOption('--run-id <id>', 'Rule-lab run id')
80
76
  .requiredOption('--slice-id <id>', 'Slice id')
81
77
  .option('--repo-path <path>', 'Repository path (default: cwd)')
82
- .option('--version <version>', 'Promoted rule version', '1.0.0')
78
+ .option('--rule-version <version>', 'Promoted rule version', '1.0.0')
83
79
  .action(action('ruleLabPromoteCommand'));
84
80
  root
85
81
  .command('regress')
@@ -96,17 +92,11 @@ export function attachRuleLabCommands(program, lazyFactory) {
96
92
  .option('--family <family>', 'Rule family to compile', 'analyze_rules')
97
93
  .action((options) => compileRules({ repoPath: options.repoPath, family: options.family }));
98
94
  }
99
- export async function ruleLabDiscoverCommand(options) {
100
- const result = await discoverRuleLabRun({
101
- repoPath: resolveRepoPath(options?.repoPath),
102
- scope: options?.scope || 'full',
103
- seed: options?.seed,
104
- });
105
- output(result);
106
- }
107
95
  export async function ruleLabAnalyzeCommand(options) {
96
+ assertConcreteRunSliceIds(options.runId, options.sliceId);
97
+ const repoPath = resolveRepoPath(options?.repoPath);
108
98
  const result = await analyzeRuleLabSlice({
109
- repoPath: resolveRepoPath(options?.repoPath),
99
+ repoPath,
110
100
  runId: options.runId,
111
101
  sliceId: options.sliceId,
112
102
  });
@@ -131,11 +121,12 @@ export async function ruleLabCurateCommand(options) {
131
121
  output(result);
132
122
  }
133
123
  export async function ruleLabPromoteCommand(options) {
124
+ const version = options.ruleVersion ?? options.version;
134
125
  const result = await promoteCuratedRules({
135
126
  repoPath: resolveRepoPath(options?.repoPath),
136
127
  runId: options.runId,
137
128
  sliceId: options.sliceId,
138
- version: options.version,
129
+ version,
139
130
  });
140
131
  output(result);
141
132
  }
@@ -1,11 +1,31 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { Command } from 'commander';
3
- import { attachRuleLabCommands, getRuleLabCommandNames } from './rule-lab.js';
3
+ import { attachRuleLabCommands, getRuleLabCommandNames, ruleLabAnalyzeCommand } from './rule-lab.js';
4
4
  describe('rule-lab cli', () => {
5
- it('registers all six rule-lab subcommands', async () => {
5
+ it('registers all rule-lab subcommands', async () => {
6
6
  const program = new Command();
7
+ program.version('test-version');
7
8
  attachRuleLabCommands(program);
8
9
  const cmds = getRuleLabCommandNames(program);
9
- expect(cmds).toEqual(['discover', 'analyze', 'review-pack', 'curate', 'promote', 'regress']);
10
+ expect(cmds).toEqual(['analyze', 'review-pack', 'curate', 'promote', 'regress', 'compile']);
11
+ });
12
+ it('uses --rule-version for promote to avoid root --version collision', () => {
13
+ const program = new Command();
14
+ program.version('test-version');
15
+ attachRuleLabCommands(program);
16
+ const ruleLab = program.commands.find((command) => command.name() === 'rule-lab');
17
+ expect(ruleLab).toBeTruthy();
18
+ const promote = ruleLab?.commands.find((command) => command.name() === 'promote');
19
+ expect(promote).toBeTruthy();
20
+ const optionNames = (promote?.options || []).map((option) => option.long);
21
+ expect(optionNames).toContain('--rule-version');
22
+ expect(optionNames).not.toContain('--version');
23
+ });
24
+ it('rejects placeholder run/slice ids at analyze command boundary', async () => {
25
+ await expect(ruleLabAnalyzeCommand({
26
+ repoPath: process.cwd(),
27
+ runId: '<run_id>',
28
+ sliceId: '<slice_id>',
29
+ })).rejects.toThrow(/placeholder/i);
10
30
  });
11
31
  });
@@ -21,6 +21,7 @@ export declare function queryCommand(queryText: string, options?: {
21
21
  goal?: string;
22
22
  limit?: string;
23
23
  content?: boolean;
24
+ responseProfile?: 'slim' | 'full';
24
25
  scopePreset?: 'unity-gameplay' | 'unity-all';
25
26
  unityResources?: UnityResourcesMode;
26
27
  unityHydration?: UnityHydrationMode;
@@ -34,6 +35,7 @@ export declare function contextCommand(name: string, options?: {
34
35
  file?: string;
35
36
  uid?: string;
36
37
  content?: boolean;
38
+ responseProfile?: 'slim' | 'full';
37
39
  unityResources?: UnityResourcesMode;
38
40
  unityHydration?: UnityHydrationMode;
39
41
  unityEvidence?: UnityEvidenceMode;
package/dist/cli/tool.js CHANGED
@@ -91,6 +91,7 @@ export async function queryCommand(queryText, options) {
91
91
  goal: options?.goal,
92
92
  limit: options?.limit ? parseInt(options.limit) : undefined,
93
93
  include_content: options?.content ?? false,
94
+ response_profile: options?.responseProfile,
94
95
  scope_preset: options?.scopePreset,
95
96
  unity_resources: options?.unityResources,
96
97
  unity_hydration_mode: options?.unityHydration,
@@ -114,6 +115,7 @@ export async function contextCommand(name, options) {
114
115
  uid: options?.uid,
115
116
  file_path: options?.file,
116
117
  include_content: options?.content ?? false,
118
+ response_profile: options?.responseProfile,
117
119
  unity_resources: options?.unityResources,
118
120
  unity_hydration_mode: options?.unityHydration,
119
121
  unity_evidence_mode: options?.unityEvidence,
@@ -6,7 +6,6 @@ export interface UnityConfig {
6
6
  lazyBatchSize: number;
7
7
  lazyMaxMs: number;
8
8
  payloadMode: 'compact' | 'full';
9
- persistLifecycleProcessMetadata: boolean;
10
9
  parityWarmup: boolean;
11
10
  parityWarmupMaxParallel: number;
12
11
  paritySeedCacheIdleMs: number;
@@ -8,7 +8,6 @@ const DEFAULTS = {
8
8
  lazyBatchSize: 30,
9
9
  lazyMaxMs: 5000,
10
10
  payloadMode: 'compact',
11
- persistLifecycleProcessMetadata: false,
12
11
  parityWarmup: false,
13
12
  parityWarmupMaxParallel: 4,
14
13
  paritySeedCacheIdleMs: 60000,
@@ -341,6 +341,7 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
341
341
  let communityResult;
342
342
  let processResult;
343
343
  let unityResult;
344
+ let unityRuleBindingResult;
344
345
  if (!options?.skipGraphPhases) {
345
346
  // ── Phase 4.5: Method Resolution Order ──────────────────────────────
346
347
  onProgress({
@@ -406,7 +407,9 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
406
407
  // ── Phase 5.6: Unity lifecycle synthetic calls (auto-detect) ────────
407
408
  const isUnityProject = allPaths.some(p => p.startsWith('Assets/') && p.endsWith('.cs'));
408
409
  const unityConfig = resolveUnityConfig();
409
- const persistLifecycleProcessMetadata = unityConfig.config.persistLifecycleProcessMetadata ?? false;
410
+ // Persistence is coupled to the Unity resource-binding indexing flow:
411
+ // if Unity project auto-detection is active, persist lifecycle metadata.
412
+ const persistLifecycleProcessMetadata = isUnityProject;
410
413
  const unityLifecycleSyntheticResult = isUnityProject
411
414
  ? applyUnityLifecycleSyntheticCalls(graph, {
412
415
  maxSyntheticEdgesPerClass: unityConfig.config.maxSyntheticEdgesPerClass,
@@ -422,16 +425,41 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
422
425
  // Phase 5.7: rule-driven binding injection (Phase 3)
423
426
  try {
424
427
  const analyzeRules = await loadAnalyzeRules(repoPath);
425
- if (analyzeRules.length > 0) {
426
- const bindingResult = applyUnityRuntimeBindingRules(graph, analyzeRules, unityConfig.config);
427
- if (isDev && bindingResult.edgesInjected > 0) {
428
- console.log(`[UnityRuleBinding] injected ${bindingResult.edgesInjected} edges from ${analyzeRules.length} rule(s)`);
429
- }
428
+ const bindingResult = applyUnityRuntimeBindingRules(graph, analyzeRules, unityConfig.config);
429
+ unityRuleBindingResult = bindingResult;
430
+ if (isDev && bindingResult.edgesInjected > 0) {
431
+ console.log(`[UnityRuleBinding] injected ${bindingResult.edgesInjected} edges from ${analyzeRules.length} rule(s)`);
430
432
  }
431
433
  }
432
434
  catch (err) {
433
435
  // rule catalog missing or invalid — skip silently
434
436
  console.warn(`[UnityRuleBinding] failed to load or apply analyze rules: ${err instanceof Error ? err.message : String(err)}`);
437
+ const reason = err instanceof Error ? err.message : String(err);
438
+ unityRuleBindingResult = {
439
+ edgesInjected: 0,
440
+ ruleResults: [],
441
+ diagnostics: {
442
+ rulesEvaluated: 0,
443
+ bindingsEvaluated: 0,
444
+ bindingsByKind: {},
445
+ methodLookupCalls: 0,
446
+ methodLookupCacheHits: 0,
447
+ sceneRuntimeTraversalCalls: 0,
448
+ sceneRuntimeTraversalCacheHits: 0,
449
+ sceneRuntimeResourcesVisited: 0,
450
+ anomalies: [`failed to load/apply analyze rules: ${reason}`],
451
+ shouldAgentReport: true,
452
+ agentReportReason: 'failed to load/apply analyze rules',
453
+ summary: [
454
+ 'rule_binding.summary: rules=0, bindings=0, edges=0',
455
+ 'rule_binding.bindings_by_kind: none',
456
+ 'rule_binding.lookup: method_calls=0, cache_hits=0',
457
+ 'rule_binding.scene_closure: traversals=0, cache_hits=0, visited_resources=0',
458
+ 'rule_binding.agent_report: should_report=true reason="failed to load/apply analyze rules"',
459
+ `rule_binding.anomaly: failed to load/apply analyze rules: ${reason}`,
460
+ ],
461
+ },
462
+ };
435
463
  }
436
464
  // ── Phase 6: Processes ─────────────────────────────────────────────
437
465
  onProgress({
@@ -517,6 +545,7 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
517
545
  communityResult,
518
546
  processResult,
519
547
  unityResult,
548
+ unityRuleBindingResult,
520
549
  scopeDiagnostics: scopeSelection.diagnostics,
521
550
  csharpPreprocDiagnostics,
522
551
  };
@@ -245,7 +245,7 @@ test('detects Unity hosts through transitive inheritance chains', () => {
245
245
  assert.equal(gunGraphHost.baseType, 'ScriptableObject');
246
246
  assert.ok(gunGraphHost.methods.length > 0);
247
247
  });
248
- test('prioritizes gameplay lifecycle hosts when synthetic edge budget is tight', () => {
248
+ test('respects global lifecycle edge budget without emitting loader bridges', () => {
249
249
  const graph = createKnowledgeGraph();
250
250
  for (let i = 0; i < 6; i += 1) {
251
251
  addClass(graph, {
@@ -275,12 +275,13 @@ test('prioritizes gameplay lifecycle hosts when synthetic edge budget is tight',
275
275
  maxSyntheticEdgesPerClass: 4,
276
276
  maxSyntheticEdgesTotal: 8,
277
277
  });
278
- const runtimeEdges = [...graph.iterRelationships()].filter((edge) => edge.type === 'CALLS' &&
279
- (edge.reason === 'unity-lifecycle-synthetic' || edge.reason === 'unity-runtime-loader-synthetic'));
280
- const targets = new Set(runtimeEdges.map((edge) => edge.targetId));
278
+ const lifecycleEdges = [...graph.iterRelationships()].filter((edge) => edge.type === 'CALLS' && edge.reason === 'unity-lifecycle-synthetic');
279
+ const loaderEdges = [...graph.iterRelationships()].filter((edge) => edge.type === 'CALLS' && edge.reason === 'unity-runtime-loader-synthetic');
281
280
  assert.equal(result.syntheticEdgeCount, 8);
282
- assert.equal([...targets].some((id) => id.includes('Assets/NEON/Code/Game/Core/GunGraphMB.cs')), true);
283
- assert.equal([...targets].some((id) => id.includes('Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadConfig.cs')), true);
281
+ assert.equal(result.lifecycleEdgeCount, 8);
282
+ assert.equal(result.loaderEdgeCount, 0);
283
+ assert.equal(lifecycleEdges.length, 8);
284
+ assert.equal(loaderEdges.length, 0);
284
285
  });
285
286
  test('resolves named class inheritance targets when direct target node lookup is ambiguous', () => {
286
287
  const graph = createKnowledgeGraph();
@@ -336,7 +337,7 @@ test('resolves named class inheritance targets when direct target node lookup is
336
337
  assert.ok(gunGraphHost);
337
338
  assert.equal(gunGraphHost.baseType, 'ScriptableObject');
338
339
  });
339
- test('emits deterministic runtime loader bridge chain after pre-bridge budget is exhausted', () => {
340
+ test('lifecycle phase does not emit runtime-loader bridge edges', () => {
340
341
  const graph = createKnowledgeGraph();
341
342
  addClass(graph, {
342
343
  className: 'GunGraphMB',
@@ -429,16 +430,15 @@ test('emits deterministic runtime loader bridge chain after pre-bridge budget is
429
430
  maxSyntheticEdgesPerClass: 12,
430
431
  maxSyntheticEdgesTotal: 10,
431
432
  });
432
- const syntheticEdges = [...graph.iterRelationships()].filter((edge) => edge.type === 'CALLS' && edge.reason === 'unity-runtime-loader-synthetic');
433
- const syntheticPairs = new Set(syntheticEdges.map((edge) => `${edge.sourceId}->${edge.targetId}`));
434
- assert.equal(result.syntheticEdgeCount, 10);
435
- assert.equal(syntheticPairs.has(`${generateId('Method', 'Assets/NEON/Code/Game/Core/GunGraphMB.cs:GunGraphMB.RegisterGraphEvents')}->${generateId('Method', 'Assets/NEON/Code/Game/Graph/Graphs/GunGraph.cs:GunGraph.RegisterEvents')}`), true);
436
- assert.equal(syntheticPairs.has(`${generateId('Method', 'Assets/NEON/Code/Game/Graph/Graphs/GunGraph.cs:GunGraph.RegisterEvents')}->${generateId('Method', 'Assets/NEON/Code/Game/Graph/Graphs/GunGraph.cs:GunGraph.StartRoutineWithEvents')}`), true);
437
- assert.equal(syntheticPairs.has(`${generateId('Method', 'Assets/NEON/Code/Game/Graph/Graphs/GunGraph.cs:GunGraph.StartRoutineWithEvents')}->${generateId('Method', 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadBase.cs:ReloadBase.GetValue')}`), true);
438
- assert.equal(syntheticPairs.has(`${generateId('Method', 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadBase.cs:ReloadBase.GetValue')}->${generateId('Method', 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadBase.cs:ReloadBase.CheckReload')}`), true);
439
- assert.equal(syntheticPairs.has(`${generateId('Method', 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadBase.cs:ReloadBase.CheckReload')}->${generateId('Method', 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/Reload.cs:Reload.ReloadRoutine')}`), true);
433
+ const syntheticLoaderEdges = [...graph.iterRelationships()].filter((edge) => edge.type === 'CALLS' && edge.reason === 'unity-runtime-loader-synthetic');
434
+ const lifecycleEdges = [...graph.iterRelationships()].filter((edge) => edge.type === 'CALLS' && edge.reason === 'unity-lifecycle-synthetic');
435
+ assert.equal(result.syntheticEdgeCount, 3);
436
+ assert.equal(result.lifecycleEdgeCount, 3);
437
+ assert.equal(result.loaderEdgeCount, 0);
438
+ assert.equal(lifecycleEdges.length, 3);
439
+ assert.equal(syntheticLoaderEdges.length, 0);
440
440
  });
441
- test('preserves bridge budget when accepted host count exceeds synthetic edge cap', () => {
441
+ test('lifecycle edge cap does not rely on runtime-loader bridge injection', () => {
442
442
  const graph = createKnowledgeGraph();
443
443
  for (let i = 0; i < 20; i += 1) {
444
444
  addClass(graph, {
@@ -534,8 +534,6 @@ test('preserves bridge budget when accepted host count exceeds synthetic edge ca
534
534
  maxSyntheticEdgesPerClass: 12,
535
535
  maxSyntheticEdgesTotal: 12,
536
536
  });
537
- const syntheticPairs = new Set([...graph.iterRelationships()]
538
- .filter((edge) => edge.type === 'CALLS' && edge.reason === 'unity-runtime-loader-synthetic')
539
- .map((edge) => `${edge.sourceId}->${edge.targetId}`));
540
- assert.equal(syntheticPairs.has(`${generateId('Method', 'Assets/NEON/Code/Game/Core/GunGraphMB.cs:GunGraphMB.RegisterGraphEvents')}->${generateId('Method', 'Assets/NEON/Code/Game/Graph/Graphs/GunGraph.cs:GunGraph.RegisterEvents')}`), true);
537
+ const loaderEdges = [...graph.iterRelationships()].filter((edge) => edge.type === 'CALLS' && edge.reason === 'unity-runtime-loader-synthetic');
538
+ assert.equal(loaderEdges.length, 0);
541
539
  });
@@ -1,9 +1,10 @@
1
- import type { UnityScanContext } from '../unity/scan-context.js';
1
+ import type { UnityPrefabSourceRef, UnityScanContext } from '../unity/scan-context.js';
2
2
  export interface UnityParitySeed {
3
3
  version: 1;
4
4
  symbolToScriptPath: Record<string, string>;
5
5
  scriptPathToGuid: Record<string, string>;
6
6
  guidToResourcePaths: Record<string, string[]>;
7
7
  assetGuidToPath?: Record<string, string>;
8
+ prefabSourceRefs?: UnityPrefabSourceRef[];
8
9
  }
9
10
  export declare function buildUnityParitySeed(scanContext: UnityScanContext): UnityParitySeed;
@@ -59,6 +59,14 @@ export function buildUnityParitySeed(scanContext) {
59
59
  scriptPathToGuid: sortRecord(scriptPathToGuid),
60
60
  guidToResourcePaths: sortRecord(guidToResourcePaths),
61
61
  assetGuidToPath: Object.keys(assetGuidToPath).length > 0 ? sortRecord(assetGuidToPath) : undefined,
62
+ prefabSourceRefs: scanContext.prefabSourceRefs?.map((row) => ({
63
+ sourceResourcePath: normalizePath(row.sourceResourcePath),
64
+ targetGuid: String(row.targetGuid || '').trim().toLowerCase(),
65
+ targetResourcePath: normalizePath(row.targetResourcePath || '') || undefined,
66
+ fileId: String(row.fileId || '').trim() || undefined,
67
+ fieldName: 'm_SourcePrefab',
68
+ sourceLayer: row.sourceLayer === 'scene' ? 'scene' : 'prefab',
69
+ })),
62
70
  };
63
71
  }
64
72
  function normalizePath(input) {
@@ -7,6 +7,7 @@ export interface UnityResourceProcessingResult {
7
7
  bindingCount: number;
8
8
  componentCount: number;
9
9
  diagnostics: string[];
10
+ prefabSourceStats: PrefabSourcePassStats;
10
11
  paritySeed?: UnityParitySeed;
11
12
  timingsMs: {
12
13
  scanContext: number;
@@ -25,4 +26,14 @@ export interface UnityResourceProcessingDeps {
25
26
  buildScanContext?: typeof buildUnityScanContext;
26
27
  resolveBindings?: typeof resolveUnityBindings;
27
28
  }
29
+ interface PrefabSourcePassStats {
30
+ rowsParsed: number;
31
+ rowsFilteredZeroGuid: number;
32
+ rowsFilteredPlaceholder: number;
33
+ rowsFilteredUnresolved: number;
34
+ rowsDeduped: number;
35
+ rowsEmitted: number;
36
+ fileErrors: number;
37
+ }
28
38
  export declare function processUnityResources(graph: KnowledgeGraph, options: UnityResourceProcessingOptions, deps?: UnityResourceProcessingDeps): Promise<UnityResourceProcessingResult>;
39
+ export {};
@@ -5,6 +5,7 @@ import { resolveUnityBindings } from '../unity/resolver.js';
5
5
  import { buildUnityParitySeed } from './unity-parity-seed.js';
6
6
  import { resolveUnityConfig } from '../config/unity-config.js';
7
7
  const UNITY_DIAGNOSTIC_SAMPLE_LIMIT = 3;
8
+ const PREFAB_SOURCE_PASS_DISABLE_ENV = 'GITNEXUS_DISABLE_PREFAB_SOURCE_PASS';
8
9
  export async function processUnityResources(graph, options, deps) {
9
10
  const tStart = performance.now();
10
11
  const buildScanContextFn = deps?.buildScanContext || buildUnityScanContext;
@@ -36,6 +37,7 @@ export async function processUnityResources(graph, options, deps) {
36
37
  let scanContextMs = 0;
37
38
  let resolveMs = 0;
38
39
  let graphWriteMs = 0;
40
+ let prefabSourceStats = initPrefabSourcePassStats();
39
41
  try {
40
42
  const tScanContextStart = performance.now();
41
43
  scanContext = await buildScanContextFn({
@@ -51,6 +53,22 @@ export async function processUnityResources(graph, options, deps) {
51
53
  }
52
54
  }
53
55
  diagnostics.push(`scanContext: scripts=${scanContext.symbolToScriptPath.size}, guids=${scanContext.scriptPathToGuid.size}, resources=${uniqueResourcePaths.size}`);
56
+ if (isPrefabSourcePassDisabledByEnv()) {
57
+ diagnostics.push(`prefab-source: skipped (env ${PREFAB_SOURCE_PASS_DISABLE_ENV}=1)`);
58
+ }
59
+ else {
60
+ const tPrefabSourceStart = performance.now();
61
+ prefabSourceStats = await emitPrefabSourceGuidRefsFromScanContext(graph, scanContext);
62
+ graphWriteMs += performance.now() - tPrefabSourceStart;
63
+ diagnostics.push(`prefab-source: emitted=${prefabSourceStats.rowsEmitted}`);
64
+ diagnostics.push(`prefab_source.rows_parsed=${prefabSourceStats.rowsParsed}`);
65
+ diagnostics.push(`prefab_source.rows_filtered_zero_guid=${prefabSourceStats.rowsFilteredZeroGuid}`);
66
+ diagnostics.push(`prefab_source.rows_filtered_placeholder=${prefabSourceStats.rowsFilteredPlaceholder}`);
67
+ diagnostics.push(`prefab_source.rows_filtered_unresolved=${prefabSourceStats.rowsFilteredUnresolved}`);
68
+ diagnostics.push(`prefab_source.rows_deduped=${prefabSourceStats.rowsDeduped}`);
69
+ diagnostics.push(`prefab_source.rows_emitted=${prefabSourceStats.rowsEmitted}`);
70
+ diagnostics.push(`prefab_source.file_errors=${prefabSourceStats.fileErrors}`);
71
+ }
54
72
  symbolsWithResourceHits = collectSymbolsWithResourceHits(scanContext);
55
73
  }
56
74
  catch (error) {
@@ -228,6 +246,7 @@ export async function processUnityResources(graph, options, deps) {
228
246
  bindingCount,
229
247
  componentCount,
230
248
  diagnostics,
249
+ prefabSourceStats,
231
250
  paritySeed: scanContext ? buildUnityParitySeed(scanContext) : undefined,
232
251
  timingsMs: {
233
252
  scanContext: roundMs(scanContextMs),
@@ -460,3 +479,86 @@ function classifyUnityDiagnostic(message) {
460
479
  }
461
480
  return 'other';
462
481
  }
482
+ async function emitPrefabSourceGuidRefsFromScanContext(graph, scanContext) {
483
+ const stats = initPrefabSourcePassStats();
484
+ const dedupeBySource = new Map();
485
+ const iterable = typeof scanContext.streamPrefabSourceRefs === 'function'
486
+ ? scanContext.streamPrefabSourceRefs({
487
+ hooks: {
488
+ onFileError: () => {
489
+ stats.fileErrors += 1;
490
+ },
491
+ },
492
+ })
493
+ : (async function* () {
494
+ for (const row of scanContext.prefabSourceRefs || []) {
495
+ yield row;
496
+ }
497
+ })();
498
+ for await (const row of iterable) {
499
+ stats.rowsParsed += 1;
500
+ const source = normalizePath(String(row.sourceResourcePath || '').trim());
501
+ const guid = String(row.targetGuid || '').trim().toLowerCase();
502
+ if (!guid || guid === '00000000000000000000000000000000') {
503
+ stats.rowsFilteredZeroGuid += 1;
504
+ continue;
505
+ }
506
+ const hintedTarget = normalizePath(String(row.targetResourcePath || '').trim());
507
+ if (hintedTarget === '__PLACEHOLDER__') {
508
+ stats.rowsFilteredPlaceholder += 1;
509
+ continue;
510
+ }
511
+ const resolvedTarget = hintedTarget
512
+ || normalizePath(scanContext.assetGuidToPath?.get(guid) || scanContext.assetGuidToPath?.get(guid.toLowerCase()) || '');
513
+ if (!resolvedTarget || !resolvedTarget.endsWith('.prefab')) {
514
+ stats.rowsFilteredUnresolved += 1;
515
+ continue;
516
+ }
517
+ if (!source) {
518
+ stats.rowsFilteredUnresolved += 1;
519
+ continue;
520
+ }
521
+ const perSourceDedupe = dedupeBySource.get(source) || new Set();
522
+ const dedupeKey = `${resolvedTarget}|m_SourcePrefab|${guid}|${String(row.fileId || '').trim()}|${row.sourceLayer === 'scene' ? 'scene' : 'prefab'}`;
523
+ if (perSourceDedupe.has(dedupeKey)) {
524
+ stats.rowsDeduped += 1;
525
+ continue;
526
+ }
527
+ perSourceDedupe.add(dedupeKey);
528
+ dedupeBySource.set(source, perSourceDedupe);
529
+ const sourceFileId = ensureResourceFileNode(graph, source);
530
+ const targetFileId = ensureResourceFileNode(graph, resolvedTarget);
531
+ graph.addRelationship({
532
+ id: generateId('UNITY_ASSET_GUID_REF', `${sourceFileId}->${targetFileId}:m_SourcePrefab:${guid}:${String(row.fileId || '')}`),
533
+ type: 'UNITY_ASSET_GUID_REF',
534
+ sourceId: sourceFileId,
535
+ targetId: targetFileId,
536
+ confidence: 1.0,
537
+ reason: JSON.stringify({
538
+ resourcePath: source,
539
+ targetResourcePath: resolvedTarget,
540
+ guid,
541
+ fileId: String(row.fileId || ''),
542
+ fieldName: 'm_SourcePrefab',
543
+ sourceLayer: row.sourceLayer === 'scene' ? 'scene' : 'prefab',
544
+ }),
545
+ });
546
+ stats.rowsEmitted += 1;
547
+ }
548
+ return stats;
549
+ }
550
+ function initPrefabSourcePassStats() {
551
+ return {
552
+ rowsParsed: 0,
553
+ rowsFilteredZeroGuid: 0,
554
+ rowsFilteredPlaceholder: 0,
555
+ rowsFilteredUnresolved: 0,
556
+ rowsDeduped: 0,
557
+ rowsEmitted: 0,
558
+ fileErrors: 0,
559
+ };
560
+ }
561
+ function isPrefabSourcePassDisabledByEnv() {
562
+ const value = String(process.env[PREFAB_SOURCE_PASS_DISABLE_ENV] || '').trim().toLowerCase();
563
+ return value === '1' || value === 'true' || value === 'yes' || value === 'on';
564
+ }