@veewo/gitnexus 1.5.7 → 1.5.9
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/cli/ai-context.js +1 -7
- package/dist/cli/analyze-options.d.ts +4 -0
- package/dist/cli/analyze-options.js +14 -1
- package/dist/cli/analyze-options.test.js +23 -0
- package/dist/cli/analyze-runtime-summary.js +0 -1
- package/dist/cli/analyze-runtime-summary.test.js +0 -2
- package/dist/cli/analyze-summary.d.ts +0 -2
- package/dist/cli/analyze-summary.js +0 -24
- package/dist/cli/analyze-summary.test.js +1 -65
- package/dist/cli/analyze.d.ts +1 -0
- package/dist/cli/analyze.js +26 -18
- package/dist/cli/clean.js +23 -2
- package/dist/cli/index.js +3 -3
- package/dist/cli/repo-manager-alias.test.js +2 -0
- package/dist/core/ingestion/pipeline.js +0 -43
- package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
- package/dist/core/ingestion/tree-sitter-queries.js +3 -3
- package/dist/mcp/local/agent-safe-response.js +1 -1
- package/dist/mcp/local/local-backend.d.ts +0 -23
- package/dist/mcp/local/local-backend.js +69 -248
- package/dist/mcp/local/runtime-chain-verify.test.js +0 -49
- package/dist/mcp/local/runtime-claim-rule-registry.d.ts +0 -11
- package/dist/mcp/local/runtime-claim-rule-registry.js +0 -159
- package/dist/mcp/local/runtime-claim-rule-registry.test.js +67 -214
- package/dist/mcp/tools.js +0 -70
- package/dist/storage/repo-manager.d.ts +1 -0
- package/dist/types/pipeline.d.ts +0 -3
- package/package.json +4 -4
- package/skills/gitnexus-cli.md +5 -2
- package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.d.ts +0 -60
- package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.js +0 -395
- package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.d.ts +0 -1
- package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.js +0 -41
- package/dist/cli/rule-lab.d.ts +0 -38
- package/dist/cli/rule-lab.js +0 -148
- package/dist/cli/rule-lab.test.d.ts +0 -1
- package/dist/cli/rule-lab.test.js +0 -31
- package/dist/core/ingestion/unity-runtime-binding-rules.d.ts +0 -26
- package/dist/core/ingestion/unity-runtime-binding-rules.js +0 -408
- package/dist/rule-lab/analyze.d.ts +0 -13
- package/dist/rule-lab/analyze.js +0 -125
- package/dist/rule-lab/analyze.test.d.ts +0 -1
- package/dist/rule-lab/analyze.test.js +0 -246
- package/dist/rule-lab/compile.d.ts +0 -5
- package/dist/rule-lab/compile.js +0 -51
- package/dist/rule-lab/compiled-bundles.d.ts +0 -30
- package/dist/rule-lab/compiled-bundles.js +0 -36
- package/dist/rule-lab/curate.d.ts +0 -33
- package/dist/rule-lab/curate.js +0 -155
- package/dist/rule-lab/curate.test.d.ts +0 -1
- package/dist/rule-lab/curate.test.js +0 -137
- package/dist/rule-lab/curation-input-builder.d.ts +0 -45
- package/dist/rule-lab/curation-input-builder.js +0 -133
- package/dist/rule-lab/discover.d.ts +0 -13
- package/dist/rule-lab/discover.js +0 -74
- package/dist/rule-lab/discover.test.d.ts +0 -1
- package/dist/rule-lab/discover.test.js +0 -42
- package/dist/rule-lab/paths.d.ts +0 -21
- package/dist/rule-lab/paths.js +0 -37
- package/dist/rule-lab/paths.test.d.ts +0 -1
- package/dist/rule-lab/paths.test.js +0 -46
- package/dist/rule-lab/promote.d.ts +0 -26
- package/dist/rule-lab/promote.js +0 -387
- package/dist/rule-lab/promote.test.d.ts +0 -1
- package/dist/rule-lab/promote.test.js +0 -314
- package/dist/rule-lab/regress.d.ts +0 -60
- package/dist/rule-lab/regress.js +0 -122
- package/dist/rule-lab/regress.test.d.ts +0 -1
- package/dist/rule-lab/regress.test.js +0 -68
- package/dist/rule-lab/review-pack.d.ts +0 -34
- package/dist/rule-lab/review-pack.js +0 -165
- package/dist/rule-lab/review-pack.test.d.ts +0 -1
- package/dist/rule-lab/review-pack.test.js +0 -116
- package/dist/rule-lab/types.d.ts +0 -135
- package/dist/rule-lab/types.js +0 -1
- package/skills/_shared/unity-rule-authoring-contract.md +0 -64
- package/skills/gitnexus-unity-rule-gen.md +0 -107
package/dist/cli/rule-lab.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { writeSync } from 'node:fs';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { analyzeRuleLabSlice } from '../rule-lab/analyze.js';
|
|
5
|
-
import { buildReviewPack } from '../rule-lab/review-pack.js';
|
|
6
|
-
import { curateRuleLabSlice } from '../rule-lab/curate.js';
|
|
7
|
-
import { promoteCuratedRules } from '../rule-lab/promote.js';
|
|
8
|
-
import { runRuleLabRegress } from '../rule-lab/regress.js';
|
|
9
|
-
import { compileRules } from '../rule-lab/compile.js';
|
|
10
|
-
const RULE_LAB_COMMANDS = ['analyze', 'review-pack', 'curate', 'promote', 'regress'];
|
|
11
|
-
function output(data) {
|
|
12
|
-
const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
13
|
-
writeSync(1, `${text}\n`);
|
|
14
|
-
}
|
|
15
|
-
function resolveRepoPath(repoPath) {
|
|
16
|
-
return path.resolve(repoPath || process.cwd());
|
|
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
|
-
}
|
|
23
|
-
export function getRuleLabCommandNames(program) {
|
|
24
|
-
const root = program.commands.find((command) => command.name() === 'rule-lab');
|
|
25
|
-
if (!root)
|
|
26
|
-
return [];
|
|
27
|
-
return root.commands.map((command) => command.name());
|
|
28
|
-
}
|
|
29
|
-
export function attachRuleLabCommands(program, lazyFactory) {
|
|
30
|
-
const action = (handlerName) => {
|
|
31
|
-
if (lazyFactory)
|
|
32
|
-
return lazyFactory(handlerName);
|
|
33
|
-
switch (handlerName) {
|
|
34
|
-
case 'ruleLabAnalyzeCommand':
|
|
35
|
-
return (options) => ruleLabAnalyzeCommand(options);
|
|
36
|
-
case 'ruleLabReviewPackCommand':
|
|
37
|
-
return (options) => ruleLabReviewPackCommand(options);
|
|
38
|
-
case 'ruleLabCurateCommand':
|
|
39
|
-
return (options) => ruleLabCurateCommand(options);
|
|
40
|
-
case 'ruleLabPromoteCommand':
|
|
41
|
-
return (options) => ruleLabPromoteCommand(options);
|
|
42
|
-
case 'ruleLabRegressCommand':
|
|
43
|
-
return (options) => ruleLabRegressCommand(options);
|
|
44
|
-
default:
|
|
45
|
-
return () => {
|
|
46
|
-
throw new Error(`Unknown rule lab handler: ${handlerName}`);
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
const root = program
|
|
51
|
-
.command('rule-lab')
|
|
52
|
-
.description('Offline rule-lab workflow for analyze/review-pack/curate/promote/regress');
|
|
53
|
-
root
|
|
54
|
-
.command('analyze')
|
|
55
|
-
.requiredOption('--run-id <id>', 'Rule-lab run id')
|
|
56
|
-
.requiredOption('--slice-id <id>', 'Slice id')
|
|
57
|
-
.option('--repo-path <path>', 'Repository path (default: cwd)')
|
|
58
|
-
.action(action('ruleLabAnalyzeCommand'));
|
|
59
|
-
root
|
|
60
|
-
.command('review-pack')
|
|
61
|
-
.requiredOption('--run-id <id>', 'Rule-lab run id')
|
|
62
|
-
.requiredOption('--slice-id <id>', 'Slice id')
|
|
63
|
-
.option('--repo-path <path>', 'Repository path (default: cwd)')
|
|
64
|
-
.option('--max-tokens <n>', 'Token budget', '6000')
|
|
65
|
-
.action(action('ruleLabReviewPackCommand'));
|
|
66
|
-
root
|
|
67
|
-
.command('curate')
|
|
68
|
-
.requiredOption('--run-id <id>', 'Rule-lab run id')
|
|
69
|
-
.requiredOption('--slice-id <id>', 'Slice id')
|
|
70
|
-
.requiredOption('--input-path <path>', 'Path to curation input JSON')
|
|
71
|
-
.option('--repo-path <path>', 'Repository path (default: cwd)')
|
|
72
|
-
.action(action('ruleLabCurateCommand'));
|
|
73
|
-
root
|
|
74
|
-
.command('promote')
|
|
75
|
-
.requiredOption('--run-id <id>', 'Rule-lab run id')
|
|
76
|
-
.requiredOption('--slice-id <id>', 'Slice id')
|
|
77
|
-
.option('--repo-path <path>', 'Repository path (default: cwd)')
|
|
78
|
-
.option('--rule-version <version>', 'Promoted rule version', '1.0.0')
|
|
79
|
-
.action(action('ruleLabPromoteCommand'));
|
|
80
|
-
root
|
|
81
|
-
.command('regress')
|
|
82
|
-
.requiredOption('--precision <n>', 'Precision metric')
|
|
83
|
-
.requiredOption('--coverage <n>', 'Coverage metric')
|
|
84
|
-
.option('--repo-path <path>', 'Repository path (default: cwd)')
|
|
85
|
-
.option('--run-id <id>', 'Run id (if provided, write report to .gitnexus/rules/reports)')
|
|
86
|
-
.option('--probes-path <path>', 'Optional JSON file containing regress probes')
|
|
87
|
-
.action(action('ruleLabRegressCommand'));
|
|
88
|
-
root
|
|
89
|
-
.command('compile')
|
|
90
|
-
.description('Compile approved YAML rules into a JSON bundle')
|
|
91
|
-
.option('--repo-path <path>', 'Repository path (default: cwd)')
|
|
92
|
-
.option('--family <family>', 'Rule family to compile', 'analyze_rules')
|
|
93
|
-
.action((options) => compileRules({ repoPath: options.repoPath, family: options.family }));
|
|
94
|
-
}
|
|
95
|
-
export async function ruleLabAnalyzeCommand(options) {
|
|
96
|
-
assertConcreteRunSliceIds(options.runId, options.sliceId);
|
|
97
|
-
const repoPath = resolveRepoPath(options?.repoPath);
|
|
98
|
-
const result = await analyzeRuleLabSlice({
|
|
99
|
-
repoPath,
|
|
100
|
-
runId: options.runId,
|
|
101
|
-
sliceId: options.sliceId,
|
|
102
|
-
});
|
|
103
|
-
output(result);
|
|
104
|
-
}
|
|
105
|
-
export async function ruleLabReviewPackCommand(options) {
|
|
106
|
-
const result = await buildReviewPack({
|
|
107
|
-
repoPath: resolveRepoPath(options?.repoPath),
|
|
108
|
-
runId: options.runId,
|
|
109
|
-
sliceId: options.sliceId,
|
|
110
|
-
maxTokens: Number(options.maxTokens || 6000),
|
|
111
|
-
});
|
|
112
|
-
output(result);
|
|
113
|
-
}
|
|
114
|
-
export async function ruleLabCurateCommand(options) {
|
|
115
|
-
const result = await curateRuleLabSlice({
|
|
116
|
-
repoPath: resolveRepoPath(options?.repoPath),
|
|
117
|
-
runId: options.runId,
|
|
118
|
-
sliceId: options.sliceId,
|
|
119
|
-
inputPath: path.resolve(options.inputPath),
|
|
120
|
-
});
|
|
121
|
-
output(result);
|
|
122
|
-
}
|
|
123
|
-
export async function ruleLabPromoteCommand(options) {
|
|
124
|
-
const version = options.ruleVersion ?? options.version;
|
|
125
|
-
const result = await promoteCuratedRules({
|
|
126
|
-
repoPath: resolveRepoPath(options?.repoPath),
|
|
127
|
-
runId: options.runId,
|
|
128
|
-
sliceId: options.sliceId,
|
|
129
|
-
version,
|
|
130
|
-
});
|
|
131
|
-
output(result);
|
|
132
|
-
}
|
|
133
|
-
export async function ruleLabRegressCommand(options) {
|
|
134
|
-
let probes;
|
|
135
|
-
if (options.probesPath) {
|
|
136
|
-
const raw = await fs.readFile(path.resolve(options.probesPath), 'utf-8');
|
|
137
|
-
probes = JSON.parse(raw);
|
|
138
|
-
}
|
|
139
|
-
const result = await runRuleLabRegress({
|
|
140
|
-
precision: Number(options.precision),
|
|
141
|
-
coverage: Number(options.coverage),
|
|
142
|
-
repoPath: options.repoPath ? resolveRepoPath(options.repoPath) : undefined,
|
|
143
|
-
runId: options.runId,
|
|
144
|
-
probes,
|
|
145
|
-
});
|
|
146
|
-
output(result);
|
|
147
|
-
}
|
|
148
|
-
export { RULE_LAB_COMMANDS };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
import { attachRuleLabCommands, getRuleLabCommandNames, ruleLabAnalyzeCommand } from './rule-lab.js';
|
|
4
|
-
describe('rule-lab cli', () => {
|
|
5
|
-
it('registers all rule-lab subcommands', async () => {
|
|
6
|
-
const program = new Command();
|
|
7
|
-
program.version('test-version');
|
|
8
|
-
attachRuleLabCommands(program);
|
|
9
|
-
const cmds = getRuleLabCommandNames(program);
|
|
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);
|
|
30
|
-
});
|
|
31
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { KnowledgeGraph } from '../graph/types.js';
|
|
2
|
-
import type { RuntimeClaimRule } from '../../mcp/local/runtime-claim-rule-registry.js';
|
|
3
|
-
import type { UnityConfig } from '../config/unity-config.js';
|
|
4
|
-
export interface UnityRuntimeBindingResult {
|
|
5
|
-
edgesInjected: number;
|
|
6
|
-
ruleResults: Array<{
|
|
7
|
-
ruleId: string;
|
|
8
|
-
edgesInjected: number;
|
|
9
|
-
}>;
|
|
10
|
-
diagnostics: UnityRuntimeBindingDiagnostics;
|
|
11
|
-
}
|
|
12
|
-
export interface UnityRuntimeBindingDiagnostics {
|
|
13
|
-
rulesEvaluated: number;
|
|
14
|
-
bindingsEvaluated: number;
|
|
15
|
-
bindingsByKind: Record<string, number>;
|
|
16
|
-
methodLookupCalls: number;
|
|
17
|
-
methodLookupCacheHits: number;
|
|
18
|
-
sceneRuntimeTraversalCalls: number;
|
|
19
|
-
sceneRuntimeTraversalCacheHits: number;
|
|
20
|
-
sceneRuntimeResourcesVisited: number;
|
|
21
|
-
anomalies: string[];
|
|
22
|
-
shouldAgentReport: boolean;
|
|
23
|
-
agentReportReason: string;
|
|
24
|
-
summary: string[];
|
|
25
|
-
}
|
|
26
|
-
export declare function applyUnityRuntimeBindingRules(graph: KnowledgeGraph, rules: RuntimeClaimRule[], config: UnityConfig): UnityRuntimeBindingResult;
|
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
import { generateId } from '../../lib/utils.js';
|
|
2
|
-
const RULE_EDGE_CONFIDENCE = 0.75;
|
|
3
|
-
const RULE_ANOMALY_PREVIEW_LIMIT = 5;
|
|
4
|
-
export function applyUnityRuntimeBindingRules(graph, rules, config) {
|
|
5
|
-
const ruleResults = [];
|
|
6
|
-
let totalEdges = 0;
|
|
7
|
-
const existingPairs = new Set();
|
|
8
|
-
for (const rel of graph.iterRelationships()) {
|
|
9
|
-
if (rel.type === 'CALLS')
|
|
10
|
-
existingPairs.add(`${rel.sourceId}->${rel.targetId}`);
|
|
11
|
-
}
|
|
12
|
-
const addSyntheticEdge = (sourceId, targetId, reason) => {
|
|
13
|
-
if (sourceId === targetId)
|
|
14
|
-
return false;
|
|
15
|
-
const key = `${sourceId}->${targetId}`;
|
|
16
|
-
if (existingPairs.has(key))
|
|
17
|
-
return false;
|
|
18
|
-
graph.addRelationship({
|
|
19
|
-
id: generateId('CALLS', `${sourceId}->${targetId}:${reason}`),
|
|
20
|
-
sourceId,
|
|
21
|
-
targetId,
|
|
22
|
-
type: 'CALLS',
|
|
23
|
-
confidence: RULE_EDGE_CONFIDENCE,
|
|
24
|
-
reason,
|
|
25
|
-
});
|
|
26
|
-
existingPairs.add(key);
|
|
27
|
-
return true;
|
|
28
|
-
};
|
|
29
|
-
// Pre-build indexes
|
|
30
|
-
const methodsByClassId = new Map();
|
|
31
|
-
const containerNodes = [];
|
|
32
|
-
const containerLabels = config.enableContainerNodes
|
|
33
|
-
? new Set(['Class', 'Struct', 'Interface', 'Record'])
|
|
34
|
-
: new Set(['Class']);
|
|
35
|
-
for (const rel of graph.iterRelationships()) {
|
|
36
|
-
if (rel.type !== 'HAS_METHOD')
|
|
37
|
-
continue;
|
|
38
|
-
const method = graph.getNode(rel.targetId);
|
|
39
|
-
if (!method || (method.label !== 'Method' && method.label !== 'Function'))
|
|
40
|
-
continue;
|
|
41
|
-
const list = methodsByClassId.get(rel.sourceId) ?? [];
|
|
42
|
-
list.push(method);
|
|
43
|
-
methodsByClassId.set(rel.sourceId, list);
|
|
44
|
-
}
|
|
45
|
-
for (const node of graph.iterNodes()) {
|
|
46
|
-
if (containerLabels.has(node.label))
|
|
47
|
-
containerNodes.push(node);
|
|
48
|
-
}
|
|
49
|
-
// Collect UNITY_ASSET_GUID_REF and UNITY_COMPONENT_INSTANCE edges
|
|
50
|
-
const assetGuidRefs = [];
|
|
51
|
-
const componentInstances = [];
|
|
52
|
-
for (const rel of graph.iterRelationships()) {
|
|
53
|
-
if (rel.type === 'UNITY_ASSET_GUID_REF')
|
|
54
|
-
assetGuidRefs.push(rel);
|
|
55
|
-
else if (rel.type === 'UNITY_COMPONENT_INSTANCE')
|
|
56
|
-
componentInstances.push(rel);
|
|
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
|
-
};
|
|
76
|
-
// Pre-build scene file index: lowercase scene name → fileId[]
|
|
77
|
-
const sceneFilesByName = new Map();
|
|
78
|
-
for (const node of graph.iterNodes()) {
|
|
79
|
-
if (node.label !== 'File')
|
|
80
|
-
continue;
|
|
81
|
-
const filePath = String(node.properties.filePath ?? '');
|
|
82
|
-
if (!filePath.endsWith('.unity'))
|
|
83
|
-
continue;
|
|
84
|
-
const fileName = (filePath.split('/').pop() ?? '').replace(/\.unity$/, '').toLowerCase();
|
|
85
|
-
const list = sceneFilesByName.get(fileName) ?? [];
|
|
86
|
-
list.push(node.id);
|
|
87
|
-
sceneFilesByName.set(fileName, list);
|
|
88
|
-
}
|
|
89
|
-
for (const rule of rules) {
|
|
90
|
-
let ruleEdges = 0;
|
|
91
|
-
for (const binding of rule.resource_bindings ?? []) {
|
|
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);
|
|
96
|
-
}
|
|
97
|
-
if (rule.lifecycle_overrides?.additional_entry_points?.length) {
|
|
98
|
-
ruleEdges += processLifecycleOverrides(rule, methodsByClassId, containerNodes, addSyntheticEdge);
|
|
99
|
-
}
|
|
100
|
-
totalEdges += ruleEdges;
|
|
101
|
-
ruleResults.push({ ruleId: rule.id, edgesInjected: ruleEdges });
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
edgesInjected: totalEdges,
|
|
105
|
-
ruleResults,
|
|
106
|
-
diagnostics: finalizeRuleBindingDiagnostics(executionState.diagnostics, totalEdges),
|
|
107
|
-
};
|
|
108
|
-
}
|
|
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
|
-
}
|
|
122
|
-
const entrySet = new Set(entryPoints);
|
|
123
|
-
const results = [];
|
|
124
|
-
for (const method of methods) {
|
|
125
|
-
if (entrySet.has(method.properties.name))
|
|
126
|
-
results.push(method);
|
|
127
|
-
}
|
|
128
|
-
executionState.resourceMethodLookupCache.set(cacheKey, results);
|
|
129
|
-
return results;
|
|
130
|
-
}
|
|
131
|
-
function processBinding(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, containerNodes, sceneFilesByName, prefabSourceTargetsBySource, executionState, addEdge) {
|
|
132
|
-
if (binding.kind === 'asset_ref_loads_components') {
|
|
133
|
-
return processAssetRefLoadsComponents(binding, ruleId, assetGuidRefs, executionState, addEdge);
|
|
134
|
-
}
|
|
135
|
-
if (binding.kind === 'method_triggers_field_load') {
|
|
136
|
-
return processMethodTriggersFieldLoad(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, containerNodes, executionState, addEdge);
|
|
137
|
-
}
|
|
138
|
-
if (binding.kind === 'method_triggers_scene_load') {
|
|
139
|
-
return processMethodTriggersSceneLoad(binding, ruleId, componentInstances, methodsByClassId, containerNodes, sceneFilesByName, prefabSourceTargetsBySource, executionState, addEdge);
|
|
140
|
-
}
|
|
141
|
-
if (binding.kind === 'method_triggers_method') {
|
|
142
|
-
return processMethodTriggersMethod(binding, ruleId, methodsByClassId, containerNodes, addEdge);
|
|
143
|
-
}
|
|
144
|
-
addAnomaly(executionState.diagnostics, `rule=${ruleId}: unsupported resource_binding kind "${binding.kind}"`);
|
|
145
|
-
return 0;
|
|
146
|
-
}
|
|
147
|
-
function processAssetRefLoadsComponents(binding, ruleId, assetGuidRefs, executionState, addEdge) {
|
|
148
|
-
let count = 0;
|
|
149
|
-
const pattern = binding.ref_field_pattern ? new RegExp(binding.ref_field_pattern) : null;
|
|
150
|
-
const entryPoints = binding.target_entry_points ?? [];
|
|
151
|
-
if (!pattern || entryPoints.length === 0) {
|
|
152
|
-
addAnomaly(executionState.diagnostics, `rule=${ruleId}: asset_ref_loads_components missing ref_field_pattern or target_entry_points`);
|
|
153
|
-
return 0;
|
|
154
|
-
}
|
|
155
|
-
const runtimeRootId = generateId('Method', 'unity-runtime-root');
|
|
156
|
-
for (const ref of assetGuidRefs) {
|
|
157
|
-
let fieldName = '';
|
|
158
|
-
try {
|
|
159
|
-
const parsed = JSON.parse(ref.reason);
|
|
160
|
-
fieldName = parsed.fieldName ?? '';
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
if (!pattern.test(fieldName))
|
|
166
|
-
continue;
|
|
167
|
-
const targetMethods = findMethodsOnResource(ref.targetId, executionState, entryPoints);
|
|
168
|
-
for (const method of targetMethods) {
|
|
169
|
-
if (addEdge(runtimeRootId, method.id, `unity-rule-resource-load:${ruleId}`))
|
|
170
|
-
count++;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return count;
|
|
174
|
-
}
|
|
175
|
-
function processMethodTriggersFieldLoad(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, containerNodes, executionState, addEdge) {
|
|
176
|
-
let count = 0;
|
|
177
|
-
const classPattern = binding.host_class_pattern ? new RegExp(binding.host_class_pattern) : null;
|
|
178
|
-
const loaderMethodNames = new Set(binding.loader_methods ?? []);
|
|
179
|
-
const entryPoints = binding.target_entry_points ?? [];
|
|
180
|
-
const defaultEntryPoints = ['OnEnable', 'Awake', 'Start'];
|
|
181
|
-
const resolvedEntryPoints = entryPoints.length > 0 ? entryPoints : defaultEntryPoints;
|
|
182
|
-
if (!classPattern || loaderMethodNames.size === 0) {
|
|
183
|
-
addAnomaly(executionState.diagnostics, `rule=${ruleId}: method_triggers_field_load missing host_class_pattern or loader_methods`);
|
|
184
|
-
return 0;
|
|
185
|
-
}
|
|
186
|
-
// Build asset ref index by source file
|
|
187
|
-
const refsBySource = new Map();
|
|
188
|
-
for (const ref of assetGuidRefs) {
|
|
189
|
-
const list = refsBySource.get(ref.sourceId) ?? [];
|
|
190
|
-
list.push(ref);
|
|
191
|
-
refsBySource.set(ref.sourceId, list);
|
|
192
|
-
}
|
|
193
|
-
for (const cls of containerNodes) {
|
|
194
|
-
if (!classPattern.test(cls.properties.name))
|
|
195
|
-
continue;
|
|
196
|
-
const methods = methodsByClassId.get(cls.id) ?? [];
|
|
197
|
-
const loaders = methods.filter(m => loaderMethodNames.has(m.properties.name));
|
|
198
|
-
if (loaders.length === 0)
|
|
199
|
-
continue;
|
|
200
|
-
// Find resource files this class is mounted on
|
|
201
|
-
const resourceFileIds = new Set();
|
|
202
|
-
for (const ci of componentInstances) {
|
|
203
|
-
if (ci.sourceId === cls.id)
|
|
204
|
-
resourceFileIds.add(ci.targetId);
|
|
205
|
-
}
|
|
206
|
-
// Follow asset refs from those resource files
|
|
207
|
-
for (const resourceFileId of resourceFileIds) {
|
|
208
|
-
for (const ref of refsBySource.get(resourceFileId) ?? []) {
|
|
209
|
-
const targetMethods = findMethodsOnResource(ref.targetId, executionState, resolvedEntryPoints);
|
|
210
|
-
for (const loader of loaders) {
|
|
211
|
-
for (const target of targetMethods) {
|
|
212
|
-
if (addEdge(loader.id, target.id, `unity-rule-loader-bridge:${ruleId}`))
|
|
213
|
-
count++;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return count;
|
|
220
|
-
}
|
|
221
|
-
function processMethodTriggersSceneLoad(binding, ruleId, componentInstances, methodsByClassId, containerNodes, sceneFilesByName, prefabSourceTargetsBySource, executionState, addEdge) {
|
|
222
|
-
const classPattern = binding.host_class_pattern ? new RegExp(binding.host_class_pattern) : null;
|
|
223
|
-
const loaderMethodNames = new Set(binding.loader_methods ?? []);
|
|
224
|
-
const sceneName = binding.scene_name;
|
|
225
|
-
const defaultEntryPoints = ['OnEnable', 'Awake', 'Start'];
|
|
226
|
-
const entryPoints = (binding.target_entry_points ?? []).length > 0
|
|
227
|
-
? binding.target_entry_points
|
|
228
|
-
: defaultEntryPoints;
|
|
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`);
|
|
231
|
-
return 0;
|
|
232
|
-
}
|
|
233
|
-
const sceneFileIds = sceneFilesByName.get(sceneName.toLowerCase()) ?? [];
|
|
234
|
-
if (sceneFileIds.length === 0) {
|
|
235
|
-
addAnomaly(executionState.diagnostics, `rule=${ruleId}: scene "${sceneName}" not found in File(.unity) index`);
|
|
236
|
-
return 0;
|
|
237
|
-
}
|
|
238
|
-
let count = 0;
|
|
239
|
-
for (const cls of containerNodes) {
|
|
240
|
-
if (!classPattern.test(cls.properties.name))
|
|
241
|
-
continue;
|
|
242
|
-
const methods = methodsByClassId.get(cls.id) ?? [];
|
|
243
|
-
const loaders = methods.filter(m => loaderMethodNames.has(m.properties.name));
|
|
244
|
-
if (loaders.length === 0)
|
|
245
|
-
continue;
|
|
246
|
-
for (const sceneFileId of sceneFileIds) {
|
|
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
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return count;
|
|
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
|
-
}
|
|
322
|
-
function processMethodTriggersMethod(binding, ruleId, methodsByClassId, containerNodes, addEdge) {
|
|
323
|
-
const { source_class_pattern, source_method, target_class_pattern, target_method } = binding;
|
|
324
|
-
if (!source_class_pattern || !source_method || !target_class_pattern || !target_method)
|
|
325
|
-
return 0;
|
|
326
|
-
const srcPattern = new RegExp(source_class_pattern);
|
|
327
|
-
const tgtPattern = new RegExp(target_class_pattern);
|
|
328
|
-
let sourceMethodId;
|
|
329
|
-
let targetMethodId;
|
|
330
|
-
for (const cls of containerNodes) {
|
|
331
|
-
if (!sourceMethodId && srcPattern.test(cls.properties.name)) {
|
|
332
|
-
const match = (methodsByClassId.get(cls.id) ?? []).find(m => m.properties.name === source_method);
|
|
333
|
-
if (match)
|
|
334
|
-
sourceMethodId = match.id;
|
|
335
|
-
}
|
|
336
|
-
if (!targetMethodId && tgtPattern.test(cls.properties.name)) {
|
|
337
|
-
const match = (methodsByClassId.get(cls.id) ?? []).find(m => m.properties.name === target_method);
|
|
338
|
-
if (match)
|
|
339
|
-
targetMethodId = match.id;
|
|
340
|
-
}
|
|
341
|
-
if (sourceMethodId && targetMethodId)
|
|
342
|
-
break;
|
|
343
|
-
}
|
|
344
|
-
if (!sourceMethodId || !targetMethodId)
|
|
345
|
-
return 0;
|
|
346
|
-
return addEdge(sourceMethodId, targetMethodId, `unity-rule-method-bridge:${ruleId}`) ? 1 : 0;
|
|
347
|
-
}
|
|
348
|
-
function processLifecycleOverrides(rule, methodsByClassId, containerNodes, addEdge) {
|
|
349
|
-
const overrides = rule.lifecycle_overrides;
|
|
350
|
-
if (!overrides?.additional_entry_points?.length)
|
|
351
|
-
return 0;
|
|
352
|
-
const entrySet = new Set(overrides.additional_entry_points);
|
|
353
|
-
const scopePattern = overrides.scope ? new RegExp(overrides.scope) : null;
|
|
354
|
-
let count = 0;
|
|
355
|
-
const runtimeRootId = generateId('Method', 'unity-runtime-root');
|
|
356
|
-
for (const cls of containerNodes) {
|
|
357
|
-
if (scopePattern && !scopePattern.test(cls.properties.filePath ?? cls.properties.name))
|
|
358
|
-
continue;
|
|
359
|
-
for (const method of methodsByClassId.get(cls.id) ?? []) {
|
|
360
|
-
if (!entrySet.has(method.properties.name))
|
|
361
|
-
continue;
|
|
362
|
-
if (addEdge(runtimeRootId, method.id, `unity-rule-lifecycle-override:${rule.id}`))
|
|
363
|
-
count++;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
return count;
|
|
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,13 +0,0 @@
|
|
|
1
|
-
import { getRuleLabPaths } from './paths.js';
|
|
2
|
-
import type { RuleLabCandidate, RuleLabSlice } from './types.js';
|
|
3
|
-
export interface AnalyzeInput {
|
|
4
|
-
repoPath: string;
|
|
5
|
-
runId: string;
|
|
6
|
-
sliceId: string;
|
|
7
|
-
}
|
|
8
|
-
export interface AnalyzeOutput {
|
|
9
|
-
paths: ReturnType<typeof getRuleLabPaths>;
|
|
10
|
-
candidates: RuleLabCandidate[];
|
|
11
|
-
slice: RuleLabSlice;
|
|
12
|
-
}
|
|
13
|
-
export declare function analyzeRuleLabSlice(input: AnalyzeInput): Promise<AnalyzeOutput>;
|