@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/rule-lab/curate.js
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { getRuleLabPaths } from './paths.js';
|
|
4
|
-
const PLACEHOLDER_RE = /TODO|TBD|placeholder|<[^>]+>/i;
|
|
5
|
-
function hasPlaceholderText(value) {
|
|
6
|
-
return PLACEHOLDER_RE.test(String(value || ''));
|
|
7
|
-
}
|
|
8
|
-
function normalizeForSet(values) {
|
|
9
|
-
return new Set(values.map((value) => value.trim().toLowerCase()));
|
|
10
|
-
}
|
|
11
|
-
function ensureStringArray(values, field) {
|
|
12
|
-
if (!Array.isArray(values) || values.length === 0) {
|
|
13
|
-
throw new Error(`${field} must be non-empty`);
|
|
14
|
-
}
|
|
15
|
-
if (values.some((value) => !String(value || '').trim())) {
|
|
16
|
-
throw new Error(`${field} entries must be non-empty strings`);
|
|
17
|
-
}
|
|
18
|
-
return values.map((value) => String(value).trim());
|
|
19
|
-
}
|
|
20
|
-
function validateDslFields(item) {
|
|
21
|
-
if (!item.match || !Array.isArray(item.match.trigger_tokens) || item.match.trigger_tokens.length === 0) {
|
|
22
|
-
throw new Error('match.trigger_tokens must be non-empty');
|
|
23
|
-
}
|
|
24
|
-
ensureStringArray(item.match.trigger_tokens, 'match.trigger_tokens');
|
|
25
|
-
if (!Array.isArray(item.topology) || item.topology.length === 0) {
|
|
26
|
-
throw new Error('topology must be non-empty');
|
|
27
|
-
}
|
|
28
|
-
item.topology.forEach((hop, index) => {
|
|
29
|
-
if (!String(hop.hop || '').trim()) {
|
|
30
|
-
throw new Error(`topology[${index}].hop must be non-empty`);
|
|
31
|
-
}
|
|
32
|
-
if (!String(hop.edge?.kind || '').trim()) {
|
|
33
|
-
throw new Error(`topology[${index}].edge.kind must be non-empty`);
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
if (!item.closure || !Array.isArray(item.closure.required_hops) || item.closure.required_hops.length === 0) {
|
|
37
|
-
throw new Error('closure.required_hops must be non-empty');
|
|
38
|
-
}
|
|
39
|
-
if (!item.closure.failure_map || Object.keys(item.closure.failure_map).length === 0) {
|
|
40
|
-
throw new Error('closure.failure_map must be non-empty');
|
|
41
|
-
}
|
|
42
|
-
ensureStringArray(item.closure.required_hops, 'closure.required_hops');
|
|
43
|
-
if (!item.claims) {
|
|
44
|
-
throw new Error('claims must be present');
|
|
45
|
-
}
|
|
46
|
-
ensureStringArray(item.claims.guarantees, 'claims.guarantees');
|
|
47
|
-
ensureStringArray(item.claims.non_guarantees, 'claims.non_guarantees');
|
|
48
|
-
if (!String(item.claims.next_action || '').trim()) {
|
|
49
|
-
throw new Error('claims.next_action must be non-empty');
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function validateCuratedItem(item) {
|
|
53
|
-
if (!Array.isArray(item.confirmed_chain?.steps) || item.confirmed_chain.steps.length === 0) {
|
|
54
|
-
throw new Error('confirmed_chain.steps must be non-empty for promotion');
|
|
55
|
-
}
|
|
56
|
-
item.confirmed_chain.steps.forEach((step, index) => {
|
|
57
|
-
if (!String(step.anchor || '').trim()) {
|
|
58
|
-
throw new Error(`confirmed_chain.steps[${index}].anchor must be non-empty`);
|
|
59
|
-
}
|
|
60
|
-
if (!String(step.snippet || '').trim()) {
|
|
61
|
-
throw new Error(`confirmed_chain.steps[${index}].snippet must be non-empty`);
|
|
62
|
-
}
|
|
63
|
-
if (hasPlaceholderText(step.anchor) || hasPlaceholderText(step.snippet)) {
|
|
64
|
-
throw new Error(`confirmed_chain.steps[${index}] contains placeholder text`);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
if (!Array.isArray(item.guarantees) || item.guarantees.length === 0) {
|
|
68
|
-
throw new Error('guarantees must be non-empty');
|
|
69
|
-
}
|
|
70
|
-
if (!Array.isArray(item.non_guarantees) || item.non_guarantees.length === 0) {
|
|
71
|
-
throw new Error('non_guarantees must be non-empty');
|
|
72
|
-
}
|
|
73
|
-
if (item.guarantees.some((entry) => !String(entry || '').trim()) || item.non_guarantees.some((entry) => !String(entry || '').trim())) {
|
|
74
|
-
throw new Error('guarantees/non_guarantees entries must be non-empty strings');
|
|
75
|
-
}
|
|
76
|
-
if (hasPlaceholderText(JSON.stringify(item))) {
|
|
77
|
-
throw new Error('curated item contains placeholder text');
|
|
78
|
-
}
|
|
79
|
-
if (Array.isArray(item.resource_bindings)) {
|
|
80
|
-
const rawBindings = JSON.stringify(item.resource_bindings);
|
|
81
|
-
if (/UnknownClass|UnknownMethod|UnknownSource|UnknownTarget/i.test(rawBindings)) {
|
|
82
|
-
throw new Error('binding unresolved: unknown placeholder binding values are forbidden');
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const guaranteeSet = normalizeForSet(item.guarantees);
|
|
86
|
-
const nonGuaranteeSet = normalizeForSet(item.non_guarantees);
|
|
87
|
-
const overlap = [...guaranteeSet].filter((entry) => nonGuaranteeSet.has(entry));
|
|
88
|
-
if (overlap.length === guaranteeSet.size && overlap.length === nonGuaranteeSet.size) {
|
|
89
|
-
throw new Error('guarantees and non_guarantees must have semantic distinction');
|
|
90
|
-
}
|
|
91
|
-
validateDslFields(item);
|
|
92
|
-
}
|
|
93
|
-
function toDslDraft(item) {
|
|
94
|
-
return {
|
|
95
|
-
id: String(item.rule_id || item.id || '').trim(),
|
|
96
|
-
version: '2.0.0',
|
|
97
|
-
match: item.match,
|
|
98
|
-
topology: item.topology,
|
|
99
|
-
closure: item.closure,
|
|
100
|
-
claims: item.claims,
|
|
101
|
-
...(Array.isArray(item.resource_bindings) && item.resource_bindings.length > 0
|
|
102
|
-
? { resource_bindings: item.resource_bindings }
|
|
103
|
-
: {}),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
function validateDslDraft(draft) {
|
|
107
|
-
if (!String(draft.id || '').trim()) {
|
|
108
|
-
throw new Error('dsl draft id must be non-empty');
|
|
109
|
-
}
|
|
110
|
-
ensureStringArray(draft.match.trigger_tokens, 'match.trigger_tokens');
|
|
111
|
-
if (!Array.isArray(draft.topology) || draft.topology.length === 0) {
|
|
112
|
-
throw new Error('topology must be non-empty');
|
|
113
|
-
}
|
|
114
|
-
ensureStringArray(draft.closure.required_hops, 'closure.required_hops');
|
|
115
|
-
if (!draft.closure.failure_map || Object.keys(draft.closure.failure_map).length === 0) {
|
|
116
|
-
throw new Error('closure.failure_map must be non-empty');
|
|
117
|
-
}
|
|
118
|
-
ensureStringArray(draft.claims.guarantees, 'claims.guarantees');
|
|
119
|
-
ensureStringArray(draft.claims.non_guarantees, 'claims.non_guarantees');
|
|
120
|
-
if (!String(draft.claims.next_action || '').trim()) {
|
|
121
|
-
throw new Error('claims.next_action must be non-empty');
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
export async function curateRuleLabSlice(input) {
|
|
125
|
-
const normalizedRepoPath = path.resolve(input.repoPath);
|
|
126
|
-
const paths = getRuleLabPaths(normalizedRepoPath, input.runId, input.sliceId);
|
|
127
|
-
const raw = await fs.readFile(path.resolve(input.inputPath), 'utf-8');
|
|
128
|
-
const parsed = JSON.parse(raw);
|
|
129
|
-
const curated = Array.isArray(parsed.curated) ? parsed.curated : [];
|
|
130
|
-
if (curated.length === 0) {
|
|
131
|
-
throw new Error('curated must contain at least one candidate');
|
|
132
|
-
}
|
|
133
|
-
curated.forEach(validateCuratedItem);
|
|
134
|
-
const drafts = curated.map((item) => toDslDraft(item));
|
|
135
|
-
drafts.forEach(validateDslDraft);
|
|
136
|
-
const firstDraft = drafts[0];
|
|
137
|
-
const sliceDir = path.dirname(paths.curatedPath);
|
|
138
|
-
await fs.mkdir(path.dirname(paths.curatedPath), { recursive: true });
|
|
139
|
-
await fs.writeFile(paths.curatedPath, `${JSON.stringify({ run_id: input.runId, slice_id: input.sliceId, curated }, null, 2)}\n`, 'utf-8');
|
|
140
|
-
await fs.writeFile(path.join(sliceDir, 'dsl-drafts.json'), `${JSON.stringify({ run_id: input.runId, slice_id: input.sliceId, drafts }, null, 2)}\n`, 'utf-8');
|
|
141
|
-
if (drafts.length === 1) {
|
|
142
|
-
await fs.writeFile(path.join(sliceDir, 'dsl-draft.json'), `${JSON.stringify(firstDraft, null, 2)}\n`, 'utf-8');
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
await fs.writeFile(path.join(sliceDir, 'dsl-draft.json'), `${JSON.stringify({
|
|
146
|
-
compatibility_warning: 'multi-draft mode active; use dsl-drafts.json for complete draft set',
|
|
147
|
-
primary_draft_id: firstDraft.id,
|
|
148
|
-
primary_draft: firstDraft,
|
|
149
|
-
}, null, 2)}\n`, 'utf-8');
|
|
150
|
-
}
|
|
151
|
-
return {
|
|
152
|
-
paths,
|
|
153
|
-
curated,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { curateRuleLabSlice } from './curate.js';
|
|
6
|
-
describe('rule-lab curate', () => {
|
|
7
|
-
it('rejects curation input with empty confirmed_chain.steps', async () => {
|
|
8
|
-
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'rule-lab-curate-'));
|
|
9
|
-
const inputPath = path.join(repoRoot, 'curation-input.json');
|
|
10
|
-
await fs.writeFile(inputPath, JSON.stringify({
|
|
11
|
-
run_id: 'run-x',
|
|
12
|
-
slice_id: 'slice-a',
|
|
13
|
-
curated: [
|
|
14
|
-
{
|
|
15
|
-
id: 'candidate-1',
|
|
16
|
-
title: 'reload rule',
|
|
17
|
-
match: { trigger_tokens: ['reload'] },
|
|
18
|
-
topology: [
|
|
19
|
-
{ hop: 'resource', from: { entity: 'resource' }, to: { entity: 'script' }, edge: { kind: 'binds_script' } },
|
|
20
|
-
],
|
|
21
|
-
closure: {
|
|
22
|
-
required_hops: ['resource'],
|
|
23
|
-
failure_map: { missing_evidence: 'rule_matched_but_evidence_missing' },
|
|
24
|
-
},
|
|
25
|
-
claims: {
|
|
26
|
-
guarantees: ['reload_chain_closed'],
|
|
27
|
-
non_guarantees: ['no_runtime_execution'],
|
|
28
|
-
next_action: 'gitnexus query "Reload"',
|
|
29
|
-
},
|
|
30
|
-
confirmed_chain: { steps: [] },
|
|
31
|
-
guarantees: ['can verify reload trigger'],
|
|
32
|
-
non_guarantees: ['does not prove runtime ordering'],
|
|
33
|
-
},
|
|
34
|
-
],
|
|
35
|
-
}), 'utf-8');
|
|
36
|
-
await expect(curateRuleLabSlice({ repoPath: repoRoot, runId: 'run-x', sliceId: 'slice-a', inputPath })).rejects.toThrow(/confirmed_chain\.steps/i);
|
|
37
|
-
await fs.rm(repoRoot, { recursive: true, force: true });
|
|
38
|
-
});
|
|
39
|
-
it('writes dsl-draft.json and rejects missing failure mapping', async () => {
|
|
40
|
-
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'rule-lab-curate-'));
|
|
41
|
-
const inputPath = path.join(repoRoot, 'curation-input.json');
|
|
42
|
-
await fs.writeFile(inputPath, JSON.stringify({
|
|
43
|
-
run_id: 'run-x',
|
|
44
|
-
slice_id: 'slice-a',
|
|
45
|
-
curated: [
|
|
46
|
-
{
|
|
47
|
-
id: 'candidate-1',
|
|
48
|
-
rule_id: 'demo.reload.v2',
|
|
49
|
-
match: { trigger_tokens: ['reload'] },
|
|
50
|
-
topology: [
|
|
51
|
-
{ hop: 'resource', from: { entity: 'resource' }, to: { entity: 'script' }, edge: { kind: 'binds_script' } },
|
|
52
|
-
],
|
|
53
|
-
closure: { required_hops: ['resource'] },
|
|
54
|
-
claims: {
|
|
55
|
-
guarantees: ['reload_chain_closed'],
|
|
56
|
-
non_guarantees: ['no_runtime_execution'],
|
|
57
|
-
next_action: 'gitnexus query "Reload"',
|
|
58
|
-
},
|
|
59
|
-
confirmed_chain: {
|
|
60
|
-
steps: [
|
|
61
|
-
{ hop_type: 'resource', anchor: 'Assets/Example.prefab:1', snippet: 'ReloadGraph' },
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
guarantees: ['can verify reload trigger'],
|
|
65
|
-
non_guarantees: ['does not prove runtime ordering'],
|
|
66
|
-
},
|
|
67
|
-
],
|
|
68
|
-
}), 'utf-8');
|
|
69
|
-
await expect(curateRuleLabSlice({ repoPath: repoRoot, runId: 'run-x', sliceId: 'slice-a', inputPath })).rejects.toThrow(/failure_map/i);
|
|
70
|
-
await fs.rm(repoRoot, { recursive: true, force: true });
|
|
71
|
-
});
|
|
72
|
-
it('preserves multi-candidate curation and writes dsl-drafts with compatibility warning', async () => {
|
|
73
|
-
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'rule-lab-curate-'));
|
|
74
|
-
const inputPath = path.join(repoRoot, 'curation-input.json');
|
|
75
|
-
const curated = [
|
|
76
|
-
{
|
|
77
|
-
id: 'candidate-1',
|
|
78
|
-
rule_id: 'demo.rule.first.v1',
|
|
79
|
-
title: 'first rule',
|
|
80
|
-
match: {
|
|
81
|
-
trigger_tokens: ['reload'],
|
|
82
|
-
resource_types: ['syncvar_hook'],
|
|
83
|
-
host_base_type: ['network_behaviour'],
|
|
84
|
-
},
|
|
85
|
-
topology: [
|
|
86
|
-
{ hop: 'code_runtime', from: { entity: 'script' }, to: { entity: 'runtime' }, edge: { kind: 'calls' } },
|
|
87
|
-
],
|
|
88
|
-
closure: { required_hops: ['code_runtime'], failure_map: { missing_evidence: 'rule_matched_but_evidence_missing' } },
|
|
89
|
-
claims: {
|
|
90
|
-
guarantees: ['reload_chain_closed'],
|
|
91
|
-
non_guarantees: ['no_runtime_execution'],
|
|
92
|
-
next_action: 'gitnexus query "reload"',
|
|
93
|
-
},
|
|
94
|
-
confirmed_chain: { steps: [{ hop_type: 'code_runtime', anchor: 'Assets/A.cs:1', snippet: 'A' }] },
|
|
95
|
-
guarantees: ['reload_chain_closed'],
|
|
96
|
-
non_guarantees: ['no_runtime_execution'],
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
id: 'candidate-2',
|
|
100
|
-
rule_id: 'demo.rule.second.v1',
|
|
101
|
-
title: 'second rule',
|
|
102
|
-
match: {
|
|
103
|
-
trigger_tokens: ['reload'],
|
|
104
|
-
resource_types: ['syncvar_hook'],
|
|
105
|
-
host_base_type: ['network_behaviour'],
|
|
106
|
-
},
|
|
107
|
-
topology: [
|
|
108
|
-
{ hop: 'code_runtime', from: { entity: 'script' }, to: { entity: 'runtime' }, edge: { kind: 'calls' } },
|
|
109
|
-
],
|
|
110
|
-
closure: { required_hops: ['code_runtime'], failure_map: { missing_evidence: 'rule_matched_but_evidence_missing' } },
|
|
111
|
-
claims: {
|
|
112
|
-
guarantees: ['reload_chain_closed'],
|
|
113
|
-
non_guarantees: ['no_runtime_execution'],
|
|
114
|
-
next_action: 'gitnexus query "reload"',
|
|
115
|
-
},
|
|
116
|
-
confirmed_chain: { steps: [{ hop_type: 'code_runtime', anchor: 'Assets/B.cs:2', snippet: 'B' }] },
|
|
117
|
-
guarantees: ['reload_chain_closed'],
|
|
118
|
-
non_guarantees: ['no_runtime_execution'],
|
|
119
|
-
},
|
|
120
|
-
];
|
|
121
|
-
await fs.writeFile(inputPath, JSON.stringify({ run_id: 'run-x', slice_id: 'slice-a', curated }, null, 2), 'utf-8');
|
|
122
|
-
const out = await curateRuleLabSlice({
|
|
123
|
-
repoPath: repoRoot,
|
|
124
|
-
runId: 'run-x',
|
|
125
|
-
sliceId: 'slice-a',
|
|
126
|
-
inputPath,
|
|
127
|
-
});
|
|
128
|
-
const baseDir = path.dirname(out.paths.curatedPath);
|
|
129
|
-
const curatedOut = JSON.parse(await fs.readFile(out.paths.curatedPath, 'utf-8'));
|
|
130
|
-
const drafts = JSON.parse(await fs.readFile(path.join(baseDir, 'dsl-drafts.json'), 'utf-8'));
|
|
131
|
-
const legacy = JSON.parse(await fs.readFile(path.join(baseDir, 'dsl-draft.json'), 'utf-8'));
|
|
132
|
-
expect(curatedOut.curated).toHaveLength(2);
|
|
133
|
-
expect(drafts.drafts).toHaveLength(2);
|
|
134
|
-
expect(legacy.compatibility_warning).toMatch(/multi-draft/i);
|
|
135
|
-
await fs.rm(repoRoot, { recursive: true, force: true });
|
|
136
|
-
});
|
|
137
|
-
});
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { RuleLabCandidate, RuleLabSlice, UnityResourceBinding } from './types.js';
|
|
2
|
-
interface CurationInputItem {
|
|
3
|
-
id: string;
|
|
4
|
-
rule_id: string;
|
|
5
|
-
title: string;
|
|
6
|
-
match: {
|
|
7
|
-
trigger_tokens: string[];
|
|
8
|
-
symbol_kind: string[];
|
|
9
|
-
module_scope: string[];
|
|
10
|
-
resource_types: string[];
|
|
11
|
-
host_base_type: string[];
|
|
12
|
-
};
|
|
13
|
-
topology: NonNullable<RuleLabCandidate['topology']>;
|
|
14
|
-
closure: {
|
|
15
|
-
required_hops: string[];
|
|
16
|
-
failure_map: Record<string, string>;
|
|
17
|
-
};
|
|
18
|
-
claims: {
|
|
19
|
-
guarantees: string[];
|
|
20
|
-
non_guarantees: string[];
|
|
21
|
-
next_action: string;
|
|
22
|
-
};
|
|
23
|
-
confirmed_chain: {
|
|
24
|
-
steps: Array<{
|
|
25
|
-
hop_type?: string;
|
|
26
|
-
anchor: string;
|
|
27
|
-
snippet: string;
|
|
28
|
-
}>;
|
|
29
|
-
};
|
|
30
|
-
guarantees: string[];
|
|
31
|
-
non_guarantees: string[];
|
|
32
|
-
resource_bindings: UnityResourceBinding[];
|
|
33
|
-
}
|
|
34
|
-
export interface CurationInputDocument {
|
|
35
|
-
run_id: string;
|
|
36
|
-
slice_id: string;
|
|
37
|
-
curated: CurationInputItem[];
|
|
38
|
-
}
|
|
39
|
-
export declare function buildCurationInput(input: {
|
|
40
|
-
runId: string;
|
|
41
|
-
sliceId: string;
|
|
42
|
-
slice: RuleLabSlice;
|
|
43
|
-
candidates: RuleLabCandidate[];
|
|
44
|
-
}): CurationInputDocument;
|
|
45
|
-
export {};
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
function unique(values) {
|
|
2
|
-
return [...new Set(values.map((value) => String(value || '').trim()).filter(Boolean))];
|
|
3
|
-
}
|
|
4
|
-
function splitSymbol(symbol) {
|
|
5
|
-
const raw = String(symbol || '').trim();
|
|
6
|
-
const parts = raw.split('.');
|
|
7
|
-
if (parts.length < 2) {
|
|
8
|
-
return { className: '', methodName: '' };
|
|
9
|
-
}
|
|
10
|
-
const className = String(parts[parts.length - 2] || '').trim();
|
|
11
|
-
const methodName = String(parts[parts.length - 1] || '').trim();
|
|
12
|
-
return { className, methodName };
|
|
13
|
-
}
|
|
14
|
-
function assertResolvedBindingParts(parts, side, candidateId) {
|
|
15
|
-
const className = String(parts.className || '').trim();
|
|
16
|
-
const methodName = String(parts.methodName || '').trim();
|
|
17
|
-
if (!className || !methodName) {
|
|
18
|
-
throw new Error(`binding_unresolved: ${side} symbol unresolved for candidate ${candidateId}`);
|
|
19
|
-
}
|
|
20
|
-
if (/^unknown/i.test(className) || /^unknown/i.test(methodName)) {
|
|
21
|
-
throw new Error(`binding_unresolved: ${side} symbol contains unknown placeholder for candidate ${candidateId}`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function assertResolvedSceneToken(value, candidateId) {
|
|
25
|
-
const token = String(value || '').trim();
|
|
26
|
-
if (!token) {
|
|
27
|
-
throw new Error(`binding_unresolved: scene token unresolved for candidate ${candidateId}`);
|
|
28
|
-
}
|
|
29
|
-
if (/^unknown/i.test(token)) {
|
|
30
|
-
throw new Error(`binding_unresolved: scene token contains unknown placeholder for candidate ${candidateId}`);
|
|
31
|
-
}
|
|
32
|
-
return token;
|
|
33
|
-
}
|
|
34
|
-
function buildBinding(candidate, pair) {
|
|
35
|
-
const source = splitSymbol(pair.source_anchor.symbol);
|
|
36
|
-
assertResolvedBindingParts(source, 'source', candidate.id);
|
|
37
|
-
const kind = candidate.binding_kind === 'method_triggers_scene_load'
|
|
38
|
-
? 'method_triggers_scene_load'
|
|
39
|
-
: 'method_triggers_method';
|
|
40
|
-
if (kind === 'method_triggers_scene_load') {
|
|
41
|
-
const sceneName = assertResolvedSceneToken(String(pair.target_anchor.symbol || pair.target_anchor.file || ''), candidate.id);
|
|
42
|
-
return [{
|
|
43
|
-
kind,
|
|
44
|
-
host_class_pattern: source.className,
|
|
45
|
-
loader_methods: [source.methodName],
|
|
46
|
-
scene_name: sceneName,
|
|
47
|
-
description: `Derived from exact pair ${String(pair.id || candidate.id)}`,
|
|
48
|
-
}];
|
|
49
|
-
}
|
|
50
|
-
const target = splitSymbol(pair.target_anchor.symbol);
|
|
51
|
-
assertResolvedBindingParts(target, 'target', candidate.id);
|
|
52
|
-
return [{
|
|
53
|
-
kind,
|
|
54
|
-
source_class_pattern: source.className,
|
|
55
|
-
source_method: source.methodName,
|
|
56
|
-
target_class_pattern: target.className,
|
|
57
|
-
target_method: target.methodName,
|
|
58
|
-
description: `Derived from exact pair ${String(pair.id || candidate.id)}`,
|
|
59
|
-
}];
|
|
60
|
-
}
|
|
61
|
-
function buildConfirmedChain(candidate, pair) {
|
|
62
|
-
const hops = (candidate.evidence?.hops || []).filter((hop) => String(hop.anchor || '').trim() && String(hop.snippet || '').trim());
|
|
63
|
-
if (hops.length > 0)
|
|
64
|
-
return hops;
|
|
65
|
-
const sourceAnchor = String(pair.source_anchor.file || '').trim();
|
|
66
|
-
const targetAnchor = String(pair.target_anchor.file || '').trim();
|
|
67
|
-
const sourceSnippet = String(pair.source_anchor.symbol || '').trim();
|
|
68
|
-
const targetSnippet = String(pair.target_anchor.symbol || '').trim();
|
|
69
|
-
const fallback = [
|
|
70
|
-
sourceAnchor ? {
|
|
71
|
-
hop_type: 'code_runtime',
|
|
72
|
-
anchor: `${sourceAnchor}:${Number(pair.source_anchor.line || 1)}`,
|
|
73
|
-
snippet: sourceSnippet || 'source',
|
|
74
|
-
} : undefined,
|
|
75
|
-
targetAnchor ? {
|
|
76
|
-
hop_type: 'code_runtime',
|
|
77
|
-
anchor: `${targetAnchor}:${Number(pair.target_anchor.line || 1)}`,
|
|
78
|
-
snippet: targetSnippet || 'target',
|
|
79
|
-
} : undefined,
|
|
80
|
-
].filter((item) => Boolean(item));
|
|
81
|
-
if (fallback.length === 0) {
|
|
82
|
-
throw new Error(`confirmed_chain_empty: no evidence or anchor fallback for candidate ${candidate.id}`);
|
|
83
|
-
}
|
|
84
|
-
return fallback;
|
|
85
|
-
}
|
|
86
|
-
export function buildCurationInput(input) {
|
|
87
|
-
const curated = input.candidates.map((candidate) => {
|
|
88
|
-
const pair = candidate.exact_pair;
|
|
89
|
-
if (!pair) {
|
|
90
|
-
throw new Error(`binding_unresolved: exact_pair missing for candidate ${candidate.id}`);
|
|
91
|
-
}
|
|
92
|
-
const requiredHops = unique((candidate.topology || []).map((hop) => hop.hop));
|
|
93
|
-
const guaranteed = unique(candidate.claims?.guarantees || [`exact pair candidate: ${candidate.id}`]);
|
|
94
|
-
const nonGuaranteed = unique(candidate.claims?.non_guarantees || ['sparse gap path only']);
|
|
95
|
-
const bindings = buildBinding(candidate, pair);
|
|
96
|
-
const confirmedChain = buildConfirmedChain(candidate, pair);
|
|
97
|
-
return {
|
|
98
|
-
id: candidate.id,
|
|
99
|
-
rule_id: String(candidate.draft_rule_id || candidate.id),
|
|
100
|
-
title: String(candidate.title || ''),
|
|
101
|
-
match: {
|
|
102
|
-
trigger_tokens: [input.slice.trigger_family],
|
|
103
|
-
symbol_kind: ['method'],
|
|
104
|
-
module_scope: [input.slice.id],
|
|
105
|
-
resource_types: [...input.slice.resource_types],
|
|
106
|
-
host_base_type: [...input.slice.host_base_type],
|
|
107
|
-
},
|
|
108
|
-
topology: candidate.topology || [],
|
|
109
|
-
closure: {
|
|
110
|
-
required_hops: requiredHops.length > 0 ? requiredHops : ['code_runtime'],
|
|
111
|
-
failure_map: {
|
|
112
|
-
missing_evidence: 'rule_matched_but_evidence_missing',
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
claims: {
|
|
116
|
-
guarantees: guaranteed,
|
|
117
|
-
non_guarantees: nonGuaranteed,
|
|
118
|
-
next_action: String(candidate.claims?.next_action || `gitnexus query "${input.slice.trigger_family}"`),
|
|
119
|
-
},
|
|
120
|
-
confirmed_chain: {
|
|
121
|
-
steps: confirmedChain,
|
|
122
|
-
},
|
|
123
|
-
guarantees: guaranteed,
|
|
124
|
-
non_guarantees: nonGuaranteed,
|
|
125
|
-
resource_bindings: bindings,
|
|
126
|
-
};
|
|
127
|
-
});
|
|
128
|
-
return {
|
|
129
|
-
run_id: input.runId,
|
|
130
|
-
slice_id: input.sliceId,
|
|
131
|
-
curated,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { getRuleLabPaths } from './paths.js';
|
|
2
|
-
import type { RuleLabManifest, RuleLabScope } from './types.js';
|
|
3
|
-
export interface DiscoverInput {
|
|
4
|
-
repoPath: string;
|
|
5
|
-
scope: RuleLabScope;
|
|
6
|
-
seed?: string;
|
|
7
|
-
}
|
|
8
|
-
export interface DiscoverOutput {
|
|
9
|
-
runId: string;
|
|
10
|
-
manifest: RuleLabManifest;
|
|
11
|
-
paths: ReturnType<typeof getRuleLabPaths>;
|
|
12
|
-
}
|
|
13
|
-
export declare function discoverRuleLabRun(input: DiscoverInput): Promise<DiscoverOutput>;
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { loadRuleRegistry } from '../mcp/local/runtime-claim-rule-registry.js';
|
|
5
|
-
import { buildRunId, getRuleLabPaths } from './paths.js';
|
|
6
|
-
import { loadCompiledRuleBundle } from './compiled-bundles.js';
|
|
7
|
-
function buildSliceId(rule) {
|
|
8
|
-
const hash = createHash('sha1')
|
|
9
|
-
.update(JSON.stringify({
|
|
10
|
-
id: rule.id,
|
|
11
|
-
trigger_family: rule.trigger_family,
|
|
12
|
-
resource_types: [...rule.resource_types].sort(),
|
|
13
|
-
host_base_type: [...rule.host_base_type].sort(),
|
|
14
|
-
required_hops: [...(rule.required_hops || [])].sort(),
|
|
15
|
-
}))
|
|
16
|
-
.digest('hex')
|
|
17
|
-
.slice(0, 10);
|
|
18
|
-
return `slice-${hash}`;
|
|
19
|
-
}
|
|
20
|
-
async function writeJson(filePath, value) {
|
|
21
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
22
|
-
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
|
|
23
|
-
}
|
|
24
|
-
export async function discoverRuleLabRun(input) {
|
|
25
|
-
const normalizedRepoPath = path.resolve(input.repoPath);
|
|
26
|
-
const analyzeBundle = await loadCompiledRuleBundle(normalizedRepoPath, 'analyze_rules');
|
|
27
|
-
const registry = analyzeBundle ? undefined : await loadRuleRegistry(normalizedRepoPath);
|
|
28
|
-
const runId = buildRunId({
|
|
29
|
-
repo: path.basename(normalizedRepoPath),
|
|
30
|
-
scope: input.scope,
|
|
31
|
-
seed: input.seed || 'default',
|
|
32
|
-
});
|
|
33
|
-
const runPaths = getRuleLabPaths(normalizedRepoPath, runId);
|
|
34
|
-
const sourceRules = analyzeBundle?.rules || registry?.activeRules || [];
|
|
35
|
-
const slices = sourceRules.map((rule) => ({
|
|
36
|
-
id: buildSliceId(rule),
|
|
37
|
-
trigger_family: rule.trigger_family,
|
|
38
|
-
resource_types: rule.resource_types,
|
|
39
|
-
host_base_type: rule.host_base_type,
|
|
40
|
-
required_hops: rule.required_hops,
|
|
41
|
-
}));
|
|
42
|
-
const manifest = {
|
|
43
|
-
run_id: runId,
|
|
44
|
-
repo_path: normalizedRepoPath,
|
|
45
|
-
scope: input.scope,
|
|
46
|
-
generated_at: new Date().toISOString(),
|
|
47
|
-
slices,
|
|
48
|
-
stages: ['discover'],
|
|
49
|
-
next_actions: [
|
|
50
|
-
`gitnexus rule-lab analyze --run-id ${runId}`,
|
|
51
|
-
`gitnexus rule-lab review-pack --run-id ${runId}`,
|
|
52
|
-
],
|
|
53
|
-
};
|
|
54
|
-
await writeJson(runPaths.manifestPath, manifest);
|
|
55
|
-
await writeJson(path.join(runPaths.runRoot, 'slice-plan.json'), {
|
|
56
|
-
run_id: runId,
|
|
57
|
-
generated_at: manifest.generated_at,
|
|
58
|
-
slices: slices.map((slice) => ({
|
|
59
|
-
slice_id: slice.id,
|
|
60
|
-
trigger_family: slice.trigger_family,
|
|
61
|
-
required_hops: slice.required_hops || [],
|
|
62
|
-
candidate_count_target: 2,
|
|
63
|
-
})),
|
|
64
|
-
});
|
|
65
|
-
await Promise.all(slices.map(async (slice) => {
|
|
66
|
-
const slicePath = path.join(runPaths.slicesRoot, slice.id, 'slice.json');
|
|
67
|
-
await writeJson(slicePath, slice);
|
|
68
|
-
}));
|
|
69
|
-
return {
|
|
70
|
-
runId,
|
|
71
|
-
manifest,
|
|
72
|
-
paths: runPaths,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { discoverRuleLabRun } from './discover.js';
|
|
6
|
-
describe('rule-lab discover', () => {
|
|
7
|
-
it('writes manifest with slices and next_actions', async () => {
|
|
8
|
-
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'rule-lab-discover-'));
|
|
9
|
-
const rulesRoot = path.join(repoRoot, '.gitnexus', 'rules');
|
|
10
|
-
await fs.mkdir(path.join(rulesRoot, 'approved'), { recursive: true });
|
|
11
|
-
await fs.writeFile(path.join(rulesRoot, 'catalog.json'), JSON.stringify({
|
|
12
|
-
rules: [
|
|
13
|
-
{
|
|
14
|
-
id: 'demo.reload.rule.v1',
|
|
15
|
-
version: '1.0.0',
|
|
16
|
-
file: 'approved/demo.reload.rule.v1.yaml',
|
|
17
|
-
},
|
|
18
|
-
],
|
|
19
|
-
}), 'utf-8');
|
|
20
|
-
await fs.writeFile(path.join(rulesRoot, 'approved', 'demo.reload.rule.v1.yaml'), [
|
|
21
|
-
'id: demo.reload.rule.v1',
|
|
22
|
-
'version: 1.0.0',
|
|
23
|
-
'trigger_family: reload',
|
|
24
|
-
'resource_types:',
|
|
25
|
-
' - prefab',
|
|
26
|
-
'host_base_type:',
|
|
27
|
-
' - ReloadBase',
|
|
28
|
-
].join('\n'), 'utf-8');
|
|
29
|
-
const out = await discoverRuleLabRun({ repoPath: repoRoot, scope: 'full' });
|
|
30
|
-
expect(out.manifest.slices.length).toBeGreaterThan(0);
|
|
31
|
-
expect(out.manifest.next_actions.join(' ')).toContain('rule-lab analyze');
|
|
32
|
-
const manifestOnDisk = JSON.parse(await fs.readFile(out.paths.manifestPath, 'utf-8'));
|
|
33
|
-
expect(manifestOnDisk.run_id).toBe(out.manifest.run_id);
|
|
34
|
-
expect(Array.isArray(manifestOnDisk.slices)).toBe(true);
|
|
35
|
-
const slicePlanPath = path.join(out.paths.runRoot, 'slice-plan.json');
|
|
36
|
-
const slicePlan = JSON.parse(await fs.readFile(slicePlanPath, 'utf-8'));
|
|
37
|
-
expect(slicePlan.run_id).toBe(out.manifest.run_id);
|
|
38
|
-
expect(Array.isArray(slicePlan.slices)).toBe(true);
|
|
39
|
-
expect(slicePlan.slices[0]).toHaveProperty('candidate_count_target');
|
|
40
|
-
await fs.rm(repoRoot, { recursive: true, force: true });
|
|
41
|
-
});
|
|
42
|
-
});
|
package/dist/rule-lab/paths.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { RuleLabScope } from './types.js';
|
|
2
|
-
export interface BuildRunIdInput {
|
|
3
|
-
repo: string;
|
|
4
|
-
scope: RuleLabScope;
|
|
5
|
-
seed: string;
|
|
6
|
-
}
|
|
7
|
-
export interface RuleLabPaths {
|
|
8
|
-
rulesRoot: string;
|
|
9
|
-
compiledRoot: string;
|
|
10
|
-
runsRoot: string;
|
|
11
|
-
runRoot: string;
|
|
12
|
-
slicesRoot: string;
|
|
13
|
-
manifestPath: string;
|
|
14
|
-
candidatesPath: string;
|
|
15
|
-
reviewCardsPath: string;
|
|
16
|
-
curatedPath: string;
|
|
17
|
-
promotedRoot: string;
|
|
18
|
-
reportsRoot: string;
|
|
19
|
-
}
|
|
20
|
-
export declare function buildRunId(input: BuildRunIdInput): string;
|
|
21
|
-
export declare function getRuleLabPaths(repoPath: string, runId: string, sliceId?: string): RuleLabPaths;
|
package/dist/rule-lab/paths.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
function normalizeIdPart(value) {
|
|
4
|
-
return String(value || '')
|
|
5
|
-
.trim()
|
|
6
|
-
.toLowerCase()
|
|
7
|
-
.replace(/[^a-z0-9._-]+/g, '-')
|
|
8
|
-
.replace(/^-+|-+$/g, '') || 'unknown';
|
|
9
|
-
}
|
|
10
|
-
export function buildRunId(input) {
|
|
11
|
-
return createHash('sha1')
|
|
12
|
-
.update(`${normalizeIdPart(input.repo)}:${input.scope}:${input.seed}`)
|
|
13
|
-
.digest('hex')
|
|
14
|
-
.slice(0, 12);
|
|
15
|
-
}
|
|
16
|
-
export function getRuleLabPaths(repoPath, runId, sliceId = 'default') {
|
|
17
|
-
const normalizedRepoPath = path.resolve(repoPath);
|
|
18
|
-
const rulesRoot = path.join(normalizedRepoPath, '.gitnexus', 'rules');
|
|
19
|
-
const compiledRoot = path.join(rulesRoot, 'compiled');
|
|
20
|
-
const runsRoot = path.join(rulesRoot, 'lab', 'runs');
|
|
21
|
-
const runRoot = path.join(runsRoot, normalizeIdPart(runId));
|
|
22
|
-
const slicesRoot = path.join(runRoot, 'slices');
|
|
23
|
-
const sliceRoot = path.join(slicesRoot, normalizeIdPart(sliceId));
|
|
24
|
-
return {
|
|
25
|
-
rulesRoot,
|
|
26
|
-
compiledRoot,
|
|
27
|
-
runsRoot,
|
|
28
|
-
runRoot,
|
|
29
|
-
slicesRoot,
|
|
30
|
-
manifestPath: path.join(runRoot, 'manifest.json'),
|
|
31
|
-
candidatesPath: path.join(sliceRoot, 'candidates.jsonl'),
|
|
32
|
-
reviewCardsPath: path.join(sliceRoot, 'review-cards.md'),
|
|
33
|
-
curatedPath: path.join(sliceRoot, 'curated.json'),
|
|
34
|
-
promotedRoot: path.join(rulesRoot, 'approved'),
|
|
35
|
-
reportsRoot: path.join(rulesRoot, 'reports'),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|