@veewo/gitnexus 1.4.11-rc → 1.5.0-rc

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 (140) hide show
  1. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.d.ts +55 -0
  2. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.js +190 -0
  3. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.test.js +13 -0
  4. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.d.ts +22 -0
  5. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.js +100 -0
  6. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.test.d.ts +1 -0
  7. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.test.js +13 -0
  8. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.d.ts +27 -0
  9. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.js +118 -0
  10. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.test.d.ts +1 -0
  11. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.test.js +16 -0
  12. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.d.ts +60 -0
  13. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.js +331 -0
  14. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.d.ts +1 -0
  15. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.js +42 -0
  16. package/dist/benchmark/u2-e2e/reload-v1-acceptance-runner.js +4 -4
  17. package/dist/benchmark/unity-lazy-context-sampler.d.ts +6 -0
  18. package/dist/benchmark/unity-lazy-context-sampler.js +49 -13
  19. package/dist/benchmark/unity-lazy-context-sampler.test.js +4 -0
  20. package/dist/cli/ai-context.js +6 -7
  21. package/dist/cli/ai-context.test.js +6 -8
  22. package/dist/cli/eval-server.js +0 -3
  23. package/dist/cli/index.js +8 -0
  24. package/dist/cli/mcp.js +0 -3
  25. package/dist/cli/rule-lab.d.ts +42 -0
  26. package/dist/cli/rule-lab.js +157 -0
  27. package/dist/cli/rule-lab.test.d.ts +1 -0
  28. package/dist/cli/rule-lab.test.js +11 -0
  29. package/dist/cli/tool.d.ts +7 -1
  30. package/dist/cli/tool.js +6 -0
  31. package/dist/core/config/unity-config.d.ts +20 -0
  32. package/dist/core/config/unity-config.js +46 -0
  33. package/dist/core/graph/types.d.ts +1 -1
  34. package/dist/core/ingestion/pipeline.js +38 -13
  35. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.d.ts +0 -2
  36. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.js +26 -213
  37. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.test.js +1 -1
  38. package/dist/core/ingestion/unity-resource-processor.js +87 -22
  39. package/dist/core/ingestion/unity-resource-processor.test.js +67 -2
  40. package/dist/core/ingestion/unity-runtime-binding-rules.d.ts +11 -0
  41. package/dist/core/ingestion/unity-runtime-binding-rules.js +179 -0
  42. package/dist/core/unity/options.d.ts +4 -0
  43. package/dist/core/unity/options.js +18 -0
  44. package/dist/core/unity/options.test.js +11 -1
  45. package/dist/core/unity/resolver.js +11 -1
  46. package/dist/core/unity/resolver.test.js +62 -0
  47. package/dist/core/unity/yaml-object-graph.js +1 -1
  48. package/dist/core/unity/yaml-object-graph.test.js +16 -0
  49. package/dist/mcp/local/derived-process-reader.d.ts +2 -0
  50. package/dist/mcp/local/derived-process-reader.js +15 -0
  51. package/dist/mcp/local/local-backend.d.ts +56 -0
  52. package/dist/mcp/local/local-backend.js +1003 -53
  53. package/dist/mcp/local/local-backend.unity-merge.test.js +1 -1
  54. package/dist/mcp/local/process-confidence.js +1 -1
  55. package/dist/mcp/local/process-evidence.d.ts +1 -0
  56. package/dist/mcp/local/process-evidence.js +22 -0
  57. package/dist/mcp/local/process-evidence.test.js +11 -1
  58. package/dist/mcp/local/process-ref.d.ts +24 -0
  59. package/dist/mcp/local/process-ref.js +33 -0
  60. package/dist/mcp/local/process-ref.test.d.ts +1 -0
  61. package/dist/mcp/local/process-ref.test.js +24 -0
  62. package/dist/mcp/local/runtime-chain-verify.d.ts +15 -1
  63. package/dist/mcp/local/runtime-chain-verify.js +191 -187
  64. package/dist/mcp/local/runtime-chain-verify.test.js +546 -19
  65. package/dist/mcp/local/runtime-claim-rule-registry.d.ts +63 -0
  66. package/dist/mcp/local/runtime-claim-rule-registry.js +308 -0
  67. package/dist/mcp/local/runtime-claim-rule-registry.test.d.ts +1 -0
  68. package/dist/mcp/local/runtime-claim-rule-registry.test.js +215 -0
  69. package/dist/mcp/local/runtime-claim.d.ts +38 -0
  70. package/dist/mcp/local/runtime-claim.js +54 -0
  71. package/dist/mcp/local/runtime-claim.test.d.ts +1 -0
  72. package/dist/mcp/local/runtime-claim.test.js +27 -0
  73. package/dist/mcp/local/unity-enrichment.d.ts +1 -0
  74. package/dist/mcp/local/unity-enrichment.js +1 -1
  75. package/dist/mcp/local/unity-evidence-view.d.ts +26 -0
  76. package/dist/mcp/local/unity-evidence-view.js +96 -0
  77. package/dist/mcp/local/unity-evidence-view.test.d.ts +1 -0
  78. package/dist/mcp/local/unity-evidence-view.test.js +39 -0
  79. package/dist/mcp/local/unity-lazy-hydrator.d.ts +2 -2
  80. package/dist/mcp/local/unity-lazy-hydrator.js +3 -3
  81. package/dist/mcp/local/unity-lazy-hydrator.test.js +4 -4
  82. package/dist/mcp/local/unity-parity-cache.js +2 -6
  83. package/dist/mcp/local/unity-parity-seed-loader.d.ts +1 -0
  84. package/dist/mcp/local/unity-parity-seed-loader.js +10 -16
  85. package/dist/mcp/local/unity-parity-seed-loader.test.js +3 -12
  86. package/dist/mcp/local/unity-runtime-hydration.d.ts +3 -2
  87. package/dist/mcp/local/unity-runtime-hydration.js +13 -16
  88. package/dist/mcp/local/unity-runtime-hydration.test.js +15 -1
  89. package/dist/mcp/resources.js +13 -0
  90. package/dist/mcp/tools.js +166 -13
  91. package/dist/rule-lab/analyze.d.ts +12 -0
  92. package/dist/rule-lab/analyze.js +90 -0
  93. package/dist/rule-lab/analyze.test.d.ts +1 -0
  94. package/dist/rule-lab/analyze.test.js +28 -0
  95. package/dist/rule-lab/compile.d.ts +5 -0
  96. package/dist/rule-lab/compile.js +51 -0
  97. package/dist/rule-lab/compiled-bundles.d.ts +30 -0
  98. package/dist/rule-lab/compiled-bundles.js +36 -0
  99. package/dist/rule-lab/curate.d.ts +32 -0
  100. package/dist/rule-lab/curate.js +134 -0
  101. package/dist/rule-lab/curate.test.d.ts +1 -0
  102. package/dist/rule-lab/curate.test.js +72 -0
  103. package/dist/rule-lab/discover.d.ts +13 -0
  104. package/dist/rule-lab/discover.js +74 -0
  105. package/dist/rule-lab/discover.test.d.ts +1 -0
  106. package/dist/rule-lab/discover.test.js +42 -0
  107. package/dist/rule-lab/paths.d.ts +21 -0
  108. package/dist/rule-lab/paths.js +37 -0
  109. package/dist/rule-lab/paths.test.d.ts +1 -0
  110. package/dist/rule-lab/paths.test.js +46 -0
  111. package/dist/rule-lab/promote.d.ts +26 -0
  112. package/dist/rule-lab/promote.js +314 -0
  113. package/dist/rule-lab/promote.test.d.ts +1 -0
  114. package/dist/rule-lab/promote.test.js +164 -0
  115. package/dist/rule-lab/regress.d.ts +60 -0
  116. package/dist/rule-lab/regress.js +122 -0
  117. package/dist/rule-lab/regress.test.d.ts +1 -0
  118. package/dist/rule-lab/regress.test.js +68 -0
  119. package/dist/rule-lab/review-pack.d.ts +31 -0
  120. package/dist/rule-lab/review-pack.js +125 -0
  121. package/dist/rule-lab/review-pack.test.d.ts +1 -0
  122. package/dist/rule-lab/review-pack.test.js +49 -0
  123. package/dist/rule-lab/types.d.ts +99 -0
  124. package/dist/rule-lab/types.js +1 -0
  125. package/package.json +1 -1
  126. package/skills/_shared/unity-hydration-contract.md +11 -0
  127. package/skills/_shared/unity-ui-trace-contract.md +33 -0
  128. package/skills/gitnexus-cli.md +11 -25
  129. package/skills/gitnexus-guide.md +2 -0
  130. package/skills/gitnexus-unity-rule-gen.md +318 -0
  131. package/dist/core/ingestion/unity-lifecycle-config.d.ts +0 -5
  132. package/dist/core/ingestion/unity-lifecycle-config.js +0 -25
  133. package/dist/mcp/local/unity-lazy-config.d.ts +0 -6
  134. package/dist/mcp/local/unity-lazy-config.js +0 -7
  135. package/dist/mcp/local/unity-lazy-config.test.js +0 -9
  136. package/dist/mcp/local/unity-process-confidence-config.d.ts +0 -1
  137. package/dist/mcp/local/unity-process-confidence-config.js +0 -4
  138. package/dist/mcp/local/unity-runtime-chain-verify-config.d.ts +0 -1
  139. package/dist/mcp/local/unity-runtime-chain-verify-config.js +0 -10
  140. /package/dist/{mcp/local/unity-lazy-config.test.d.ts → benchmark/u2-e2e/hydration-policy-repeatability-runner.test.d.ts} +0 -0
@@ -0,0 +1,72 @@
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
+ });
@@ -0,0 +1,13 @@
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>;
@@ -0,0 +1,74 @@
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
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
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
+ });
@@ -0,0 +1,21 @@
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;
@@ -0,0 +1,37 @@
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
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { buildRunId, getRuleLabPaths } from './paths.js';
5
+ const TEST_DIR = path.dirname(new URL(import.meta.url).pathname);
6
+ describe('rule-lab paths', () => {
7
+ it('builds deterministic run/slice paths under .gitnexus/rules/lab/runs', () => {
8
+ const runId = buildRunId({ repo: 'GitNexus', scope: 'full', seed: 'abc' });
9
+ const p = getRuleLabPaths('/repo', runId, 'slice-a');
10
+ expect(p.manifestPath).toContain('/.gitnexus/rules/lab/runs/');
11
+ expect(p.candidatesPath).toContain('/slices/slice-a/candidates.jsonl');
12
+ });
13
+ it('exposes DSL v2 topology and closure schema fields', async () => {
14
+ const sample = {
15
+ id: 'demo.reload.v2',
16
+ match: { trigger_tokens: ['reload'] },
17
+ topology: [
18
+ {
19
+ hop: 'resource',
20
+ from: { entity: 'resource' },
21
+ to: { entity: 'script' },
22
+ edge: { kind: 'binds_script' },
23
+ },
24
+ ],
25
+ closure: {
26
+ required_hops: ['resource'],
27
+ failure_map: { missing_evidence: 'rule_matched_but_evidence_missing' },
28
+ },
29
+ claims: {
30
+ guarantees: ['g1'],
31
+ non_guarantees: ['ng1'],
32
+ next_action: 'gitnexus query "Reload"',
33
+ },
34
+ };
35
+ expect(sample.topology[0].edge.kind).toBe('binds_script');
36
+ const ruleDslSchema = JSON.parse(await fs.readFile(path.join(TEST_DIR, 'schema', 'rule-dsl.schema.json'), 'utf-8'));
37
+ const draftSchema = JSON.parse(await fs.readFile(path.join(TEST_DIR, 'schema', 'dsl-draft.schema.json'), 'utf-8'));
38
+ expect(ruleDslSchema.properties).toHaveProperty('match');
39
+ expect(ruleDslSchema.properties).toHaveProperty('topology');
40
+ expect(ruleDslSchema.properties).toHaveProperty('closure');
41
+ expect(ruleDslSchema.properties).toHaveProperty('claims');
42
+ expect(draftSchema.properties).toHaveProperty('topology');
43
+ expect(draftSchema.properties).toHaveProperty('closure');
44
+ expect(draftSchema.properties).toHaveProperty('claims');
45
+ });
46
+ });
@@ -0,0 +1,26 @@
1
+ import { getRuleLabPaths } from './paths.js';
2
+ interface CatalogEntry {
3
+ id: string;
4
+ version: string;
5
+ enabled: boolean;
6
+ file: string;
7
+ family?: string;
8
+ }
9
+ interface CatalogShape {
10
+ version: number;
11
+ rules: CatalogEntry[];
12
+ }
13
+ export interface PromoteInput {
14
+ repoPath: string;
15
+ runId: string;
16
+ sliceId: string;
17
+ version?: string;
18
+ }
19
+ export interface PromoteOutput {
20
+ catalog: CatalogShape;
21
+ promotedFiles: string[];
22
+ compiledPaths: Record<'analyze_rules' | 'retrieval_rules' | 'verification_rules', string>;
23
+ paths: ReturnType<typeof getRuleLabPaths>;
24
+ }
25
+ export declare function promoteCuratedRules(input: PromoteInput): Promise<PromoteOutput>;
26
+ export {};
@@ -0,0 +1,314 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { getRuleLabPaths } from './paths.js';
4
+ import { writeCompiledRuleBundle, loadCompiledRuleBundle } from './compiled-bundles.js';
5
+ function quoteYaml(value) {
6
+ const raw = String(value || '');
7
+ if (/^[a-zA-Z0-9._-]+$/.test(raw))
8
+ return raw;
9
+ return `'${raw.replace(/'/g, "''")}'`;
10
+ }
11
+ function inferTriggerFamily(item) {
12
+ const fromTitle = String(item.title || '').trim().split(/\s+/)[0];
13
+ if (fromTitle)
14
+ return fromTitle.toLowerCase();
15
+ return 'runtime';
16
+ }
17
+ function unique(values) {
18
+ return [...new Set(values.map((value) => String(value || '').trim()).filter(Boolean))];
19
+ }
20
+ function toComparableToken(value) {
21
+ return String(value || '').trim().toLowerCase();
22
+ }
23
+ function isForbiddenPlaceholder(value) {
24
+ const token = toComparableToken(value);
25
+ return token === 'unknown' || token === 'todo' || token === 'tbd' || /<[^>]+>/.test(token);
26
+ }
27
+ function assertNoPlaceholderScope(values, field) {
28
+ if (values.length === 0) {
29
+ throw new Error(`promote lint failed: ${field} must be non-empty`);
30
+ }
31
+ if (values.some((entry) => isForbiddenPlaceholder(entry))) {
32
+ throw new Error(`promote lint failed: unknown scope placeholder is forbidden (${field})`);
33
+ }
34
+ }
35
+ function toDraftFromCurated(item) {
36
+ const triggerTokens = unique(item.match?.trigger_tokens || [inferTriggerFamily(item)]);
37
+ const topology = Array.isArray(item.topology) && item.topology.length > 0
38
+ ? item.topology
39
+ : item.confirmed_chain.steps.map((step) => ({
40
+ hop: String(step.hop_type || 'resource'),
41
+ from: { entity: 'resource' },
42
+ to: { entity: 'script' },
43
+ edge: { kind: 'binds_script' },
44
+ }));
45
+ const requiredHops = unique(item.closure?.required_hops || topology.map((step) => step.hop));
46
+ const failureMap = item.closure?.failure_map && Object.keys(item.closure.failure_map).length > 0
47
+ ? item.closure.failure_map
48
+ : { missing_evidence: 'rule_matched_but_evidence_missing' };
49
+ const guarantees = unique(item.claims?.guarantees || item.guarantees);
50
+ const nonGuarantees = unique(item.claims?.non_guarantees || item.non_guarantees);
51
+ const nextAction = String(item.claims?.next_action || '').trim() || 'gitnexus query "runtime"';
52
+ return {
53
+ id: String(item.rule_id || item.id || '').trim(),
54
+ version: '2.0.0',
55
+ match: {
56
+ trigger_tokens: triggerTokens,
57
+ symbol_kind: item.match?.symbol_kind || [],
58
+ module_scope: item.match?.module_scope || [],
59
+ resource_types: unique(item.match?.resource_types || []),
60
+ host_base_type: unique(item.match?.host_base_type || []),
61
+ },
62
+ topology,
63
+ closure: {
64
+ required_hops: requiredHops,
65
+ failure_map: failureMap,
66
+ },
67
+ claims: {
68
+ guarantees,
69
+ non_guarantees: nonGuarantees,
70
+ next_action: nextAction,
71
+ },
72
+ };
73
+ }
74
+ function compileRule(ruleId, version, draft) {
75
+ const triggerFamily = String(draft.match.trigger_tokens[0] || '').trim() || 'runtime';
76
+ const resourceTypes = unique(draft.match.resource_types || []);
77
+ const hostBaseType = unique(draft.match.host_base_type || []);
78
+ if (resourceTypes.length === 0) {
79
+ resourceTypes.push('unspecified_resource');
80
+ }
81
+ if (hostBaseType.length === 0) {
82
+ hostBaseType.push('unspecified_host');
83
+ }
84
+ const requiredHops = unique(draft.closure.required_hops);
85
+ const guarantees = unique(draft.claims.guarantees);
86
+ const nonGuarantees = unique(draft.claims.non_guarantees);
87
+ const nextAction = String(draft.claims.next_action || '').trim() || 'gitnexus query "runtime"';
88
+ assertNoPlaceholderScope(resourceTypes, 'resource_types');
89
+ assertNoPlaceholderScope(hostBaseType, 'host_base_type');
90
+ return {
91
+ id: ruleId,
92
+ version,
93
+ trigger_family: triggerFamily,
94
+ resource_types: resourceTypes,
95
+ host_base_type: hostBaseType,
96
+ required_hops: requiredHops,
97
+ guarantees,
98
+ non_guarantees: nonGuarantees,
99
+ next_action: nextAction,
100
+ match: draft.match,
101
+ topology: draft.topology,
102
+ closure: draft.closure,
103
+ claims: draft.claims,
104
+ ...(draft.resource_bindings ? { resource_bindings: draft.resource_bindings } : {}),
105
+ ...(draft.lifecycle_overrides ? { lifecycle_overrides: draft.lifecycle_overrides } : {}),
106
+ };
107
+ }
108
+ function toStageAwareCompiledRule(rule, relativeFile) {
109
+ return {
110
+ id: rule.id,
111
+ version: rule.version,
112
+ trigger_family: rule.trigger_family,
113
+ trigger_tokens: [...rule.match.trigger_tokens],
114
+ resource_types: [...rule.resource_types],
115
+ host_base_type: [...rule.host_base_type],
116
+ required_hops: [...rule.required_hops],
117
+ guarantees: [...rule.guarantees],
118
+ non_guarantees: [...rule.non_guarantees],
119
+ next_action: rule.next_action,
120
+ file_path: relativeFile,
121
+ match: rule.match,
122
+ topology: rule.topology,
123
+ closure: rule.closure,
124
+ claims: rule.claims,
125
+ ...(rule.resource_bindings ? { resource_bindings: rule.resource_bindings } : {}),
126
+ ...(rule.lifecycle_overrides ? { lifecycle_overrides: rule.lifecycle_overrides } : {}),
127
+ };
128
+ }
129
+ function pushList(lines, key, values, indent = '') {
130
+ lines.push(`${indent}${key}:`);
131
+ values.forEach((value) => lines.push(`${indent} - ${quoteYaml(value)}`));
132
+ }
133
+ function renderObjectLines(lines, object, indent = '') {
134
+ const entries = Object.entries(object || {});
135
+ for (const [key, value] of entries) {
136
+ const scalar = typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
137
+ ? String(value)
138
+ : JSON.stringify(value);
139
+ lines.push(`${indent}${key}: ${quoteYaml(scalar)}`);
140
+ }
141
+ }
142
+ function buildRuleYaml(rule) {
143
+ const lines = [
144
+ `id: ${quoteYaml(rule.id)}`,
145
+ `version: ${quoteYaml(rule.version)}`,
146
+ ...(rule.family ? [`family: ${quoteYaml(rule.family)}`] : []),
147
+ `trigger_family: ${quoteYaml(rule.trigger_family)}`,
148
+ ];
149
+ pushList(lines, 'resource_types', rule.resource_types);
150
+ pushList(lines, 'host_base_type', rule.host_base_type);
151
+ pushList(lines, 'required_hops', rule.required_hops);
152
+ pushList(lines, 'guarantees', rule.guarantees);
153
+ pushList(lines, 'non_guarantees', rule.non_guarantees);
154
+ lines.push(`next_action: ${quoteYaml(rule.next_action)}`);
155
+ lines.push('match:');
156
+ pushList(lines, 'trigger_tokens', rule.match.trigger_tokens, ' ');
157
+ if (Array.isArray(rule.match.symbol_kind) && rule.match.symbol_kind.length > 0) {
158
+ pushList(lines, 'symbol_kind', rule.match.symbol_kind, ' ');
159
+ }
160
+ if (Array.isArray(rule.match.module_scope) && rule.match.module_scope.length > 0) {
161
+ pushList(lines, 'module_scope', rule.match.module_scope, ' ');
162
+ }
163
+ if (Array.isArray(rule.match.resource_types) && rule.match.resource_types.length > 0) {
164
+ pushList(lines, 'resource_types', rule.match.resource_types, ' ');
165
+ }
166
+ if (Array.isArray(rule.match.host_base_type) && rule.match.host_base_type.length > 0) {
167
+ pushList(lines, 'host_base_type', rule.match.host_base_type, ' ');
168
+ }
169
+ lines.push('topology:');
170
+ for (const hop of rule.topology) {
171
+ lines.push(` - hop: ${quoteYaml(hop.hop)}`);
172
+ lines.push(' from:');
173
+ renderObjectLines(lines, hop.from || {}, ' ');
174
+ lines.push(' to:');
175
+ renderObjectLines(lines, hop.to || {}, ' ');
176
+ lines.push(' edge:');
177
+ lines.push(` kind: ${quoteYaml(String(hop.edge?.kind || 'calls'))}`);
178
+ if (hop.constraints && Object.keys(hop.constraints).length > 0) {
179
+ lines.push(' constraints:');
180
+ renderObjectLines(lines, hop.constraints, ' ');
181
+ }
182
+ }
183
+ lines.push('closure:');
184
+ pushList(lines, 'required_hops', rule.closure.required_hops, ' ');
185
+ lines.push(' failure_map:');
186
+ for (const [key, value] of Object.entries(rule.closure.failure_map || {})) {
187
+ lines.push(` ${quoteYaml(key)}: ${quoteYaml(String(value || 'rule_matched_but_evidence_missing'))}`);
188
+ }
189
+ lines.push('claims:');
190
+ pushList(lines, 'guarantees', rule.claims.guarantees, ' ');
191
+ pushList(lines, 'non_guarantees', rule.claims.non_guarantees, ' ');
192
+ lines.push(` next_action: ${quoteYaml(rule.claims.next_action)}`);
193
+ if (rule.resource_bindings && rule.resource_bindings.length > 0) {
194
+ lines.push('resource_bindings:');
195
+ for (const binding of rule.resource_bindings) {
196
+ lines.push(` - kind: ${binding.kind}`);
197
+ if (binding.ref_field_pattern)
198
+ lines.push(` ref_field_pattern: ${quoteYaml(binding.ref_field_pattern)}`);
199
+ if (binding.target_entry_points?.length)
200
+ pushList(lines, 'target_entry_points', binding.target_entry_points, ' ');
201
+ if (binding.host_class_pattern)
202
+ lines.push(` host_class_pattern: ${quoteYaml(binding.host_class_pattern)}`);
203
+ if (binding.field_name)
204
+ lines.push(` field_name: ${quoteYaml(binding.field_name)}`);
205
+ if (binding.loader_methods?.length)
206
+ pushList(lines, 'loader_methods', binding.loader_methods, ' ');
207
+ }
208
+ }
209
+ if (rule.lifecycle_overrides) {
210
+ lines.push('lifecycle_overrides:');
211
+ if (rule.lifecycle_overrides.additional_entry_points?.length) {
212
+ pushList(lines, 'additional_entry_points', rule.lifecycle_overrides.additional_entry_points, ' ');
213
+ }
214
+ if (rule.lifecycle_overrides.scope) {
215
+ lines.push(` scope: ${quoteYaml(rule.lifecycle_overrides.scope)}`);
216
+ }
217
+ }
218
+ return `${lines.join('\n')}\n`;
219
+ }
220
+ async function readCatalog(catalogPath) {
221
+ try {
222
+ const raw = await fs.readFile(catalogPath, 'utf-8');
223
+ const parsed = JSON.parse(raw);
224
+ return {
225
+ version: Number(parsed.version || 1),
226
+ rules: Array.isArray(parsed.rules) ? parsed.rules : [],
227
+ };
228
+ }
229
+ catch (error) {
230
+ if (error?.code === 'ENOENT') {
231
+ return { version: 1, rules: [] };
232
+ }
233
+ throw error;
234
+ }
235
+ }
236
+ export async function promoteCuratedRules(input) {
237
+ const normalizedRepoPath = path.resolve(input.repoPath);
238
+ const paths = getRuleLabPaths(normalizedRepoPath, input.runId, input.sliceId);
239
+ const version = String(input.version || '1.0.0');
240
+ const curatedRaw = await fs.readFile(paths.curatedPath, 'utf-8');
241
+ const curatedDoc = JSON.parse(curatedRaw);
242
+ const curatedItems = Array.isArray(curatedDoc.curated) ? curatedDoc.curated : [];
243
+ if (curatedItems.length === 0) {
244
+ throw new Error('No curated candidates available for promotion');
245
+ }
246
+ let dslDraftFromCurate;
247
+ try {
248
+ const dslDraftRaw = await fs.readFile(path.join(path.dirname(paths.curatedPath), 'dsl-draft.json'), 'utf-8');
249
+ dslDraftFromCurate = JSON.parse(dslDraftRaw);
250
+ }
251
+ catch (error) {
252
+ if (error?.code !== 'ENOENT')
253
+ throw error;
254
+ }
255
+ const catalogPath = path.join(paths.rulesRoot, 'catalog.json');
256
+ const catalog = await readCatalog(catalogPath);
257
+ const promotedFiles = [];
258
+ const compiledRules = [];
259
+ await fs.mkdir(paths.promotedRoot, { recursive: true });
260
+ for (const item of curatedItems) {
261
+ const ruleId = String(item.rule_id || item.id || '').trim();
262
+ if (!ruleId) {
263
+ throw new Error('curated item missing rule id');
264
+ }
265
+ const relativeFile = path.join('approved', `${ruleId}.yaml`).split(path.sep).join('/');
266
+ const absoluteFile = path.join(paths.rulesRoot, relativeFile);
267
+ const draft = dslDraftFromCurate && curatedItems.length === 1
268
+ ? { ...dslDraftFromCurate, id: ruleId, version }
269
+ : { ...toDraftFromCurated(item), id: ruleId, version };
270
+ const compiledRule = compileRule(ruleId, version, draft);
271
+ const yaml = buildRuleYaml(compiledRule);
272
+ await fs.writeFile(absoluteFile, yaml, 'utf-8');
273
+ promotedFiles.push(absoluteFile);
274
+ compiledRules.push(toStageAwareCompiledRule(compiledRule, relativeFile));
275
+ const nextEntry = {
276
+ id: ruleId,
277
+ version,
278
+ enabled: true,
279
+ file: relativeFile,
280
+ ...(compiledRule.family ? { family: compiledRule.family } : {}),
281
+ };
282
+ const existingIndex = catalog.rules.findIndex((entry) => entry.id === ruleId);
283
+ if (existingIndex >= 0) {
284
+ catalog.rules[existingIndex] = nextEntry;
285
+ }
286
+ else {
287
+ catalog.rules.push(nextEntry);
288
+ }
289
+ }
290
+ await fs.mkdir(path.dirname(catalogPath), { recursive: true });
291
+ await fs.writeFile(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`, 'utf-8');
292
+ const mergeCompiledRules = async (family) => {
293
+ const existing = await loadCompiledRuleBundle(normalizedRepoPath, family, paths.rulesRoot);
294
+ const merged = new Map();
295
+ for (const rule of existing?.rules || []) {
296
+ merged.set(rule.id, rule);
297
+ }
298
+ for (const rule of compiledRules) {
299
+ merged.set(rule.id, rule);
300
+ }
301
+ return writeCompiledRuleBundle(paths.rulesRoot, family, [...merged.values()]);
302
+ };
303
+ const compiledPaths = {
304
+ analyze_rules: await mergeCompiledRules('analyze_rules'),
305
+ retrieval_rules: await mergeCompiledRules('retrieval_rules'),
306
+ verification_rules: await mergeCompiledRules('verification_rules'),
307
+ };
308
+ return {
309
+ catalog,
310
+ promotedFiles,
311
+ compiledPaths,
312
+ paths,
313
+ };
314
+ }
@@ -0,0 +1 @@
1
+ export {};