@veewo/gitnexus 1.4.11-rc.2 → 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 (139) 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 -1
  21. package/dist/cli/eval-server.js +0 -3
  22. package/dist/cli/index.js +8 -0
  23. package/dist/cli/mcp.js +0 -3
  24. package/dist/cli/rule-lab.d.ts +42 -0
  25. package/dist/cli/rule-lab.js +157 -0
  26. package/dist/cli/rule-lab.test.d.ts +1 -0
  27. package/dist/cli/rule-lab.test.js +11 -0
  28. package/dist/cli/tool.d.ts +7 -1
  29. package/dist/cli/tool.js +6 -0
  30. package/dist/core/config/unity-config.d.ts +20 -0
  31. package/dist/core/config/unity-config.js +46 -0
  32. package/dist/core/graph/types.d.ts +1 -1
  33. package/dist/core/ingestion/pipeline.js +38 -13
  34. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.d.ts +0 -2
  35. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.js +26 -213
  36. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.test.js +1 -1
  37. package/dist/core/ingestion/unity-resource-processor.js +87 -22
  38. package/dist/core/ingestion/unity-resource-processor.test.js +67 -2
  39. package/dist/core/ingestion/unity-runtime-binding-rules.d.ts +11 -0
  40. package/dist/core/ingestion/unity-runtime-binding-rules.js +179 -0
  41. package/dist/core/unity/options.d.ts +4 -0
  42. package/dist/core/unity/options.js +18 -0
  43. package/dist/core/unity/options.test.js +11 -1
  44. package/dist/core/unity/resolver.js +11 -1
  45. package/dist/core/unity/resolver.test.js +62 -0
  46. package/dist/core/unity/yaml-object-graph.js +1 -1
  47. package/dist/core/unity/yaml-object-graph.test.js +16 -0
  48. package/dist/mcp/local/derived-process-reader.d.ts +2 -0
  49. package/dist/mcp/local/derived-process-reader.js +15 -0
  50. package/dist/mcp/local/local-backend.d.ts +56 -0
  51. package/dist/mcp/local/local-backend.js +1003 -53
  52. package/dist/mcp/local/local-backend.unity-merge.test.js +1 -1
  53. package/dist/mcp/local/process-confidence.js +1 -1
  54. package/dist/mcp/local/process-evidence.d.ts +1 -0
  55. package/dist/mcp/local/process-evidence.js +22 -0
  56. package/dist/mcp/local/process-evidence.test.js +11 -1
  57. package/dist/mcp/local/process-ref.d.ts +24 -0
  58. package/dist/mcp/local/process-ref.js +33 -0
  59. package/dist/mcp/local/process-ref.test.d.ts +1 -0
  60. package/dist/mcp/local/process-ref.test.js +24 -0
  61. package/dist/mcp/local/runtime-chain-verify.d.ts +15 -1
  62. package/dist/mcp/local/runtime-chain-verify.js +191 -187
  63. package/dist/mcp/local/runtime-chain-verify.test.js +546 -19
  64. package/dist/mcp/local/runtime-claim-rule-registry.d.ts +63 -0
  65. package/dist/mcp/local/runtime-claim-rule-registry.js +308 -0
  66. package/dist/mcp/local/runtime-claim-rule-registry.test.d.ts +1 -0
  67. package/dist/mcp/local/runtime-claim-rule-registry.test.js +215 -0
  68. package/dist/mcp/local/runtime-claim.d.ts +38 -0
  69. package/dist/mcp/local/runtime-claim.js +54 -0
  70. package/dist/mcp/local/runtime-claim.test.d.ts +1 -0
  71. package/dist/mcp/local/runtime-claim.test.js +27 -0
  72. package/dist/mcp/local/unity-enrichment.d.ts +1 -0
  73. package/dist/mcp/local/unity-enrichment.js +1 -1
  74. package/dist/mcp/local/unity-evidence-view.d.ts +26 -0
  75. package/dist/mcp/local/unity-evidence-view.js +96 -0
  76. package/dist/mcp/local/unity-evidence-view.test.d.ts +1 -0
  77. package/dist/mcp/local/unity-evidence-view.test.js +39 -0
  78. package/dist/mcp/local/unity-lazy-hydrator.d.ts +2 -2
  79. package/dist/mcp/local/unity-lazy-hydrator.js +3 -3
  80. package/dist/mcp/local/unity-lazy-hydrator.test.js +4 -4
  81. package/dist/mcp/local/unity-parity-cache.js +2 -6
  82. package/dist/mcp/local/unity-parity-seed-loader.d.ts +1 -0
  83. package/dist/mcp/local/unity-parity-seed-loader.js +10 -16
  84. package/dist/mcp/local/unity-parity-seed-loader.test.js +3 -12
  85. package/dist/mcp/local/unity-runtime-hydration.d.ts +3 -2
  86. package/dist/mcp/local/unity-runtime-hydration.js +13 -16
  87. package/dist/mcp/local/unity-runtime-hydration.test.js +15 -1
  88. package/dist/mcp/resources.js +13 -0
  89. package/dist/mcp/tools.js +166 -13
  90. package/dist/rule-lab/analyze.d.ts +12 -0
  91. package/dist/rule-lab/analyze.js +90 -0
  92. package/dist/rule-lab/analyze.test.d.ts +1 -0
  93. package/dist/rule-lab/analyze.test.js +28 -0
  94. package/dist/rule-lab/compile.d.ts +5 -0
  95. package/dist/rule-lab/compile.js +51 -0
  96. package/dist/rule-lab/compiled-bundles.d.ts +30 -0
  97. package/dist/rule-lab/compiled-bundles.js +36 -0
  98. package/dist/rule-lab/curate.d.ts +32 -0
  99. package/dist/rule-lab/curate.js +134 -0
  100. package/dist/rule-lab/curate.test.d.ts +1 -0
  101. package/dist/rule-lab/curate.test.js +72 -0
  102. package/dist/rule-lab/discover.d.ts +13 -0
  103. package/dist/rule-lab/discover.js +74 -0
  104. package/dist/rule-lab/discover.test.d.ts +1 -0
  105. package/dist/rule-lab/discover.test.js +42 -0
  106. package/dist/rule-lab/paths.d.ts +21 -0
  107. package/dist/rule-lab/paths.js +37 -0
  108. package/dist/rule-lab/paths.test.d.ts +1 -0
  109. package/dist/rule-lab/paths.test.js +46 -0
  110. package/dist/rule-lab/promote.d.ts +26 -0
  111. package/dist/rule-lab/promote.js +314 -0
  112. package/dist/rule-lab/promote.test.d.ts +1 -0
  113. package/dist/rule-lab/promote.test.js +164 -0
  114. package/dist/rule-lab/regress.d.ts +60 -0
  115. package/dist/rule-lab/regress.js +122 -0
  116. package/dist/rule-lab/regress.test.d.ts +1 -0
  117. package/dist/rule-lab/regress.test.js +68 -0
  118. package/dist/rule-lab/review-pack.d.ts +31 -0
  119. package/dist/rule-lab/review-pack.js +125 -0
  120. package/dist/rule-lab/review-pack.test.d.ts +1 -0
  121. package/dist/rule-lab/review-pack.test.js +49 -0
  122. package/dist/rule-lab/types.d.ts +99 -0
  123. package/dist/rule-lab/types.js +1 -0
  124. package/package.json +1 -1
  125. package/skills/_shared/unity-hydration-contract.md +11 -0
  126. package/skills/_shared/unity-ui-trace-contract.md +33 -0
  127. package/skills/gitnexus-cli.md +11 -25
  128. package/skills/gitnexus-guide.md +2 -0
  129. package/skills/gitnexus-unity-rule-gen.md +318 -0
  130. package/dist/core/ingestion/unity-lifecycle-config.d.ts +0 -5
  131. package/dist/core/ingestion/unity-lifecycle-config.js +0 -25
  132. package/dist/mcp/local/unity-lazy-config.d.ts +0 -6
  133. package/dist/mcp/local/unity-lazy-config.js +0 -7
  134. package/dist/mcp/local/unity-lazy-config.test.js +0 -9
  135. package/dist/mcp/local/unity-process-confidence-config.d.ts +0 -1
  136. package/dist/mcp/local/unity-process-confidence-config.js +0 -4
  137. package/dist/mcp/local/unity-runtime-chain-verify-config.d.ts +0 -1
  138. package/dist/mcp/local/unity-runtime-chain-verify-config.js +0 -10
  139. /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,164 @@
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 { promoteCuratedRules } from './promote.js';
6
+ describe('rule-lab promote', () => {
7
+ it('promotes curated candidate into approved yaml and catalog entry', async () => {
8
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'rule-lab-promote-'));
9
+ const rulesRoot = path.join(repoRoot, '.gitnexus', 'rules');
10
+ const sliceDir = path.join(rulesRoot, 'lab', 'runs', 'run-x', 'slices', 'slice-a');
11
+ await fs.mkdir(path.join(rulesRoot, 'approved'), { recursive: true });
12
+ await fs.mkdir(sliceDir, { recursive: true });
13
+ await fs.writeFile(path.join(rulesRoot, 'catalog.json'), JSON.stringify({ version: 1, rules: [] }, null, 2), 'utf-8');
14
+ await fs.writeFile(path.join(sliceDir, 'curated.json'), JSON.stringify({
15
+ run_id: 'run-x',
16
+ slice_id: 'slice-a',
17
+ curated: [
18
+ {
19
+ id: 'candidate-1',
20
+ rule_id: 'demo.rule.v1',
21
+ title: 'demo rule',
22
+ match: { trigger_tokens: ['reload'] },
23
+ topology: [
24
+ { hop: 'resource', from: { entity: 'resource' }, to: { entity: 'script' }, edge: { kind: 'binds_script' } },
25
+ ],
26
+ closure: {
27
+ required_hops: ['resource'],
28
+ failure_map: { missing_evidence: 'rule_matched_but_evidence_missing' },
29
+ },
30
+ claims: {
31
+ guarantees: ['can verify startup graph trigger'],
32
+ non_guarantees: ['does not prove all runtime states'],
33
+ next_action: 'gitnexus query "reload"',
34
+ },
35
+ confirmed_chain: {
36
+ steps: [{ hop_type: 'resource', anchor: 'Assets/Demo.prefab:12', snippet: 'Reload' }],
37
+ },
38
+ guarantees: ['can verify startup graph trigger'],
39
+ non_guarantees: ['does not prove all runtime states'],
40
+ },
41
+ ],
42
+ }, null, 2), 'utf-8');
43
+ const out = await promoteCuratedRules({ repoPath: repoRoot, runId: 'run-x', sliceId: 'slice-a' });
44
+ expect(out.catalog.rules.some((r) => r.id === 'demo.rule.v1')).toBe(true);
45
+ expect(out.promotedFiles[0]).toMatch(/rules\/approved\/.*\.yaml$/);
46
+ await fs.rm(repoRoot, { recursive: true, force: true });
47
+ });
48
+ it('emits stage-aware compiled bundles for analyze, retrieval, and verification', async () => {
49
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'rule-lab-promote-'));
50
+ const rulesRoot = path.join(repoRoot, '.gitnexus', 'rules');
51
+ const sliceDir = path.join(rulesRoot, 'lab', 'runs', 'run-x', 'slices', 'slice-a');
52
+ await fs.mkdir(path.join(rulesRoot, 'approved'), { recursive: true });
53
+ await fs.mkdir(sliceDir, { recursive: true });
54
+ await fs.writeFile(path.join(rulesRoot, 'catalog.json'), JSON.stringify({ version: 1, rules: [] }, null, 2), 'utf-8');
55
+ await fs.writeFile(path.join(sliceDir, 'curated.json'), JSON.stringify({
56
+ run_id: 'run-x',
57
+ slice_id: 'slice-a',
58
+ curated: [
59
+ {
60
+ id: 'candidate-1',
61
+ rule_id: 'demo.rule.v2',
62
+ title: 'demo rule',
63
+ match: {
64
+ trigger_tokens: ['reload'],
65
+ resource_types: ['asset'],
66
+ host_base_type: ['ReloadBase'],
67
+ },
68
+ topology: [
69
+ { hop: 'resource', from: { entity: 'resource' }, to: { entity: 'script' }, edge: { kind: 'binds_script' } },
70
+ { hop: 'code_runtime', from: { entity: 'script' }, to: { entity: 'runtime' }, edge: { kind: 'calls' } },
71
+ ],
72
+ closure: {
73
+ required_hops: ['resource', 'code_runtime'],
74
+ failure_map: { missing_evidence: 'rule_matched_but_evidence_missing' },
75
+ },
76
+ claims: {
77
+ guarantees: ['reload_chain_closed'],
78
+ non_guarantees: ['no_runtime_execution'],
79
+ next_action: 'gitnexus query "reload"',
80
+ },
81
+ confirmed_chain: {
82
+ steps: [{ hop_type: 'resource', anchor: 'Assets/Demo.prefab:12', snippet: 'Reload' }],
83
+ },
84
+ guarantees: ['reload_chain_closed'],
85
+ non_guarantees: ['no_runtime_execution'],
86
+ },
87
+ ],
88
+ }, null, 2), 'utf-8');
89
+ const out = await promoteCuratedRules({ repoPath: repoRoot, runId: 'run-x', sliceId: 'slice-a', version: '2.0.0' });
90
+ expect(out).toHaveProperty('compiledPaths');
91
+ const analyzeBundlePath = path.join(rulesRoot, 'compiled', 'analyze_rules.v2.json');
92
+ const retrievalBundlePath = path.join(rulesRoot, 'compiled', 'retrieval_rules.v2.json');
93
+ const verificationBundlePath = path.join(rulesRoot, 'compiled', 'verification_rules.v2.json');
94
+ const analyzeBundle = JSON.parse(await fs.readFile(analyzeBundlePath, 'utf-8'));
95
+ const retrievalBundle = JSON.parse(await fs.readFile(retrievalBundlePath, 'utf-8'));
96
+ const verificationBundle = JSON.parse(await fs.readFile(verificationBundlePath, 'utf-8'));
97
+ expect(analyzeBundle.family).toBe('analyze_rules');
98
+ expect(retrievalBundle.family).toBe('retrieval_rules');
99
+ expect(verificationBundle.family).toBe('verification_rules');
100
+ expect(analyzeBundle.rules[0].id).toBe('demo.rule.v2');
101
+ expect(retrievalBundle.rules[0].claims.next_action).toBe('gitnexus query "reload"');
102
+ expect(verificationBundle.rules[0].closure.required_hops).toEqual(['resource', 'code_runtime']);
103
+ await fs.rm(repoRoot, { recursive: true, force: true });
104
+ });
105
+ it('rejects promote when resource_types or host_base_type are unknown', async () => {
106
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'rule-lab-promote-'));
107
+ const rulesRoot = path.join(repoRoot, '.gitnexus', 'rules');
108
+ const sliceDir = path.join(rulesRoot, 'lab', 'runs', 'run-x', 'slices', 'slice-a');
109
+ await fs.mkdir(path.join(rulesRoot, 'approved'), { recursive: true });
110
+ await fs.mkdir(sliceDir, { recursive: true });
111
+ await fs.writeFile(path.join(rulesRoot, 'catalog.json'), JSON.stringify({ version: 1, rules: [] }, null, 2), 'utf-8');
112
+ await fs.writeFile(path.join(sliceDir, 'curated.json'), JSON.stringify({
113
+ run_id: 'run-x',
114
+ slice_id: 'slice-a',
115
+ curated: [
116
+ {
117
+ id: 'candidate-unknown',
118
+ rule_id: 'demo.rule.v2',
119
+ match: { trigger_tokens: ['reload'] },
120
+ topology: [
121
+ { hop: 'resource', from: { entity: 'resource' }, to: { entity: 'script' }, edge: { kind: 'binds_script' } },
122
+ ],
123
+ closure: {
124
+ required_hops: ['resource'],
125
+ failure_map: { missing_evidence: 'rule_matched_but_evidence_missing' },
126
+ },
127
+ claims: {
128
+ guarantees: ['reload_chain_closed'],
129
+ non_guarantees: ['no_runtime_execution'],
130
+ next_action: 'gitnexus query "reload"',
131
+ },
132
+ confirmed_chain: {
133
+ steps: [{ hop_type: 'resource', anchor: 'Assets/Demo.prefab:9', snippet: 'Reload' }],
134
+ },
135
+ guarantees: ['reload_chain_closed'],
136
+ non_guarantees: ['no_runtime_execution'],
137
+ },
138
+ ],
139
+ }, null, 2), 'utf-8');
140
+ await fs.writeFile(path.join(sliceDir, 'dsl-draft.json'), JSON.stringify({
141
+ id: 'demo.rule.v2',
142
+ version: '2.0.0',
143
+ match: {
144
+ trigger_tokens: ['reload'],
145
+ resource_types: ['unknown'],
146
+ host_base_type: ['unknown'],
147
+ },
148
+ topology: [
149
+ { hop: 'resource', from: { entity: 'resource' }, to: { entity: 'script' }, edge: { kind: 'binds_script' } },
150
+ ],
151
+ closure: {
152
+ required_hops: ['resource'],
153
+ failure_map: { missing_evidence: 'rule_matched_but_evidence_missing' },
154
+ },
155
+ claims: {
156
+ guarantees: ['reload_chain_closed'],
157
+ non_guarantees: ['no_runtime_execution'],
158
+ next_action: 'gitnexus query "reload"',
159
+ },
160
+ }, null, 2), 'utf-8');
161
+ await expect(promoteCuratedRules({ repoPath: repoRoot, runId: 'run-x', sliceId: 'slice-a' })).rejects.toThrow(/unknown/i);
162
+ await fs.rm(repoRoot, { recursive: true, force: true });
163
+ });
164
+ });
@@ -0,0 +1,60 @@
1
+ export interface RuleLabRegressInput {
2
+ precision: number;
3
+ coverage: number;
4
+ probes?: Array<{
5
+ id: string;
6
+ pass: boolean;
7
+ replay_command: string;
8
+ bucket?: 'anchor' | 'holdout' | 'negative';
9
+ key_resource_hit?: boolean;
10
+ next_hop_usable?: boolean;
11
+ hint_drift?: boolean;
12
+ false_positive_anchor_leak?: boolean;
13
+ }>;
14
+ repoPath?: string;
15
+ runId?: string;
16
+ }
17
+ export interface RuleLabRegressOutput {
18
+ pass: boolean;
19
+ failures: string[];
20
+ metrics: {
21
+ precision: number;
22
+ coverage: number;
23
+ probe_pass_rate: number;
24
+ key_resource_hit_rate: number;
25
+ next_hop_usability_rate: number;
26
+ hint_drift_rate: number;
27
+ };
28
+ bucket_metrics: {
29
+ anchor: {
30
+ total: number;
31
+ passed: number;
32
+ anchor_pass_rate: number;
33
+ };
34
+ holdout: {
35
+ total: number;
36
+ usable: number;
37
+ next_hop_usability_rate: number;
38
+ };
39
+ negative: {
40
+ total: number;
41
+ false_positive: number;
42
+ false_positive_rate: number;
43
+ };
44
+ };
45
+ threshold_checks: {
46
+ precision_pass: boolean;
47
+ coverage_pass: boolean;
48
+ probe_pass_rate_pass: boolean;
49
+ anchor_pass: boolean;
50
+ holdout_pass: boolean;
51
+ negative_pass: boolean;
52
+ };
53
+ probe_results: Array<{
54
+ id: string;
55
+ pass: boolean;
56
+ replay_command: string;
57
+ }>;
58
+ reportPath?: string;
59
+ }
60
+ export declare function runRuleLabRegress(input: RuleLabRegressInput): Promise<RuleLabRegressOutput>;
@@ -0,0 +1,122 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ const PRECISION_THRESHOLD = 0.9;
4
+ const COVERAGE_THRESHOLD = 0.8;
5
+ const PROBE_PASS_RATE_THRESHOLD = 0.85;
6
+ function buildReportMarkdown(output) {
7
+ const lines = [];
8
+ lines.push('# Rule Lab Regression Report');
9
+ lines.push('');
10
+ lines.push('## Metrics');
11
+ lines.push(`- metrics.precision: ${output.metrics.precision}`);
12
+ lines.push(`- metrics.coverage: ${output.metrics.coverage}`);
13
+ lines.push(`- metrics.probe_pass_rate: ${output.metrics.probe_pass_rate}`);
14
+ lines.push(`- metrics.key_resource_hit_rate: ${output.metrics.key_resource_hit_rate}`);
15
+ lines.push(`- metrics.next_hop_usability_rate: ${output.metrics.next_hop_usability_rate}`);
16
+ lines.push(`- metrics.hint_drift_rate: ${output.metrics.hint_drift_rate}`);
17
+ lines.push('');
18
+ lines.push('## Gate');
19
+ lines.push(`- pass: ${output.pass}`);
20
+ lines.push(`- failures: ${output.failures.join(', ') || 'none'}`);
21
+ lines.push(`- threshold_checks: ${JSON.stringify(output.threshold_checks)}`);
22
+ lines.push('');
23
+ lines.push('## Buckets');
24
+ lines.push(`- anchor: ${JSON.stringify(output.bucket_metrics.anchor)}`);
25
+ lines.push(`- holdout: ${JSON.stringify(output.bucket_metrics.holdout)}`);
26
+ lines.push(`- negative: ${JSON.stringify(output.bucket_metrics.negative)}`);
27
+ lines.push('');
28
+ lines.push('## Probe Results');
29
+ if (output.probe_results.length === 0) {
30
+ lines.push('- none');
31
+ }
32
+ else {
33
+ for (const probe of output.probe_results) {
34
+ lines.push(`- ${probe.id}: pass=${probe.pass} | replay_command=${probe.replay_command}`);
35
+ }
36
+ }
37
+ lines.push('');
38
+ return `${lines.join('\n')}\n`;
39
+ }
40
+ export async function runRuleLabRegress(input) {
41
+ const failures = [];
42
+ const probes = Array.isArray(input.probes) ? input.probes : [];
43
+ if (input.precision < PRECISION_THRESHOLD) {
44
+ failures.push('precision_below_threshold');
45
+ }
46
+ if (input.coverage < COVERAGE_THRESHOLD) {
47
+ failures.push('coverage_below_threshold');
48
+ }
49
+ const passedProbes = probes.filter((probe) => probe.pass).length;
50
+ const keyResourceProbeCount = probes.filter((probe) => typeof probe.key_resource_hit === 'boolean').length;
51
+ const keyResourceHitCount = probes.filter((probe) => probe.key_resource_hit === true).length;
52
+ const nextHopProbeCount = probes.filter((probe) => typeof probe.next_hop_usable === 'boolean').length;
53
+ const nextHopUsableCount = probes.filter((probe) => probe.next_hop_usable === true).length;
54
+ const hintDriftProbeCount = probes.filter((probe) => typeof probe.hint_drift === 'boolean').length;
55
+ const hintDriftCount = probes.filter((probe) => probe.hint_drift === true).length;
56
+ const probePassRate = probes.length > 0
57
+ ? passedProbes / probes.length
58
+ : 0;
59
+ if (probePassRate < PROBE_PASS_RATE_THRESHOLD) {
60
+ failures.push('probe_pass_rate_below_threshold');
61
+ }
62
+ const anchorProbes = probes.filter((probe) => probe.bucket === 'anchor');
63
+ const holdoutProbes = probes.filter((probe) => probe.bucket === 'holdout');
64
+ const negativeProbes = probes.filter((probe) => probe.bucket === 'negative');
65
+ const anchorPassed = anchorProbes.filter((probe) => probe.pass).length;
66
+ const holdoutUsable = holdoutProbes.filter((probe) => probe.next_hop_usable === true).length;
67
+ const negativeFalsePositive = negativeProbes.filter((probe) => probe.false_positive_anchor_leak === true).length;
68
+ const anchorPassRate = anchorProbes.length > 0 ? anchorPassed / anchorProbes.length : 0;
69
+ const holdoutUsabilityRate = holdoutProbes.length > 0 ? holdoutUsable / holdoutProbes.length : 0;
70
+ const negativeFalsePositiveRate = negativeProbes.length > 0 ? negativeFalsePositive / negativeProbes.length : 0;
71
+ if (anchorProbes.length === 0) {
72
+ failures.push('anchor_bucket_missing');
73
+ }
74
+ if (holdoutProbes.length === 0) {
75
+ failures.push('holdout_bucket_missing');
76
+ }
77
+ if (negativeProbes.length === 0) {
78
+ failures.push('negative_bucket_missing');
79
+ }
80
+ if (anchorProbes.length > 0 && anchorPassRate < 1) {
81
+ failures.push('anchor_pass_rate_below_threshold');
82
+ }
83
+ if (holdoutProbes.length > 0 && holdoutUsabilityRate < 0.85) {
84
+ failures.push('holdout_next_hop_usability_below_threshold');
85
+ }
86
+ if (negativeProbes.length > 0 && negativeFalsePositiveRate > 0.1) {
87
+ failures.push('negative_false_positive_rate_above_threshold');
88
+ }
89
+ const output = {
90
+ pass: failures.length === 0,
91
+ failures,
92
+ metrics: {
93
+ precision: input.precision,
94
+ coverage: input.coverage,
95
+ probe_pass_rate: probePassRate,
96
+ key_resource_hit_rate: keyResourceProbeCount > 0 ? keyResourceHitCount / keyResourceProbeCount : 0,
97
+ next_hop_usability_rate: nextHopProbeCount > 0 ? nextHopUsableCount / nextHopProbeCount : 0,
98
+ hint_drift_rate: hintDriftProbeCount > 0 ? hintDriftCount / hintDriftProbeCount : 0,
99
+ },
100
+ bucket_metrics: {
101
+ anchor: { total: anchorProbes.length, passed: anchorPassed, anchor_pass_rate: anchorPassRate },
102
+ holdout: { total: holdoutProbes.length, usable: holdoutUsable, next_hop_usability_rate: holdoutUsabilityRate },
103
+ negative: { total: negativeProbes.length, false_positive: negativeFalsePositive, false_positive_rate: negativeFalsePositiveRate },
104
+ },
105
+ threshold_checks: {
106
+ precision_pass: input.precision >= PRECISION_THRESHOLD,
107
+ coverage_pass: input.coverage >= COVERAGE_THRESHOLD,
108
+ probe_pass_rate_pass: probePassRate >= PROBE_PASS_RATE_THRESHOLD,
109
+ anchor_pass: anchorProbes.length > 0 && anchorPassRate >= 1,
110
+ holdout_pass: holdoutProbes.length > 0 && holdoutUsabilityRate >= 0.85,
111
+ negative_pass: negativeProbes.length > 0 && negativeFalsePositiveRate <= 0.1,
112
+ },
113
+ probe_results: probes,
114
+ };
115
+ if (input.repoPath && input.runId) {
116
+ const reportPath = path.join(path.resolve(input.repoPath), '.gitnexus', 'rules', 'reports', `${input.runId}-regress.md`);
117
+ await fs.mkdir(path.dirname(reportPath), { recursive: true });
118
+ await fs.writeFile(reportPath, buildReportMarkdown(output), 'utf-8');
119
+ output.reportPath = reportPath;
120
+ }
121
+ return output;
122
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { runRuleLabRegress } from './regress.js';
3
+ describe('rule-lab regress', () => {
4
+ it('fails when precision or coverage is below threshold', async () => {
5
+ const out = await runRuleLabRegress({ precision: 0.85, coverage: 0.92 });
6
+ expect(out.pass).toBe(false);
7
+ expect(out.failures).toContain('precision_below_threshold');
8
+ });
9
+ it('fails regress when probe pass-rate is below threshold even if metrics are high', async () => {
10
+ const out = await runRuleLabRegress({
11
+ precision: 0.95,
12
+ coverage: 0.95,
13
+ probes: [
14
+ { id: 'p1', pass: false, replay_command: 'gitnexus query "Reload"' },
15
+ ],
16
+ });
17
+ expect(out.pass).toBe(false);
18
+ expect(out.failures).toContain('probe_pass_rate_below_threshold');
19
+ expect(out.metrics.probe_pass_rate).toBeLessThan(0.85);
20
+ });
21
+ it('reports stage-aware metrics and three-bucket threshold checks', async () => {
22
+ const out = await runRuleLabRegress({
23
+ precision: 0.95,
24
+ coverage: 0.9,
25
+ probes: [
26
+ {
27
+ id: 'anchor-1',
28
+ bucket: 'anchor',
29
+ pass: true,
30
+ replay_command: 'gitnexus query "anchor"',
31
+ key_resource_hit: true,
32
+ next_hop_usable: true,
33
+ hint_drift: false,
34
+ false_positive_anchor_leak: false,
35
+ },
36
+ {
37
+ id: 'holdout-1',
38
+ bucket: 'holdout',
39
+ pass: true,
40
+ replay_command: 'gitnexus query "holdout"',
41
+ key_resource_hit: true,
42
+ next_hop_usable: true,
43
+ hint_drift: false,
44
+ false_positive_anchor_leak: false,
45
+ },
46
+ {
47
+ id: 'negative-1',
48
+ bucket: 'negative',
49
+ pass: true,
50
+ replay_command: 'gitnexus query "negative"',
51
+ key_resource_hit: false,
52
+ next_hop_usable: false,
53
+ hint_drift: false,
54
+ false_positive_anchor_leak: false,
55
+ },
56
+ ],
57
+ });
58
+ expect(out.metrics).toHaveProperty('key_resource_hit_rate');
59
+ expect(out.metrics).toHaveProperty('next_hop_usability_rate');
60
+ expect(out.metrics).toHaveProperty('hint_drift_rate');
61
+ expect(out.bucket_metrics.anchor.anchor_pass_rate).toBe(1);
62
+ expect(out.bucket_metrics.holdout.next_hop_usability_rate).toBe(1);
63
+ expect(out.bucket_metrics.negative.false_positive_rate).toBe(0);
64
+ expect(out.threshold_checks.anchor_pass).toBe(true);
65
+ expect(out.threshold_checks.holdout_pass).toBe(true);
66
+ expect(out.threshold_checks.negative_pass).toBe(true);
67
+ });
68
+ });
@@ -0,0 +1,31 @@
1
+ import { getRuleLabPaths } from './paths.js';
2
+ export interface ReviewPackInput {
3
+ repoPath: string;
4
+ runId: string;
5
+ sliceId: string;
6
+ maxTokens: number;
7
+ }
8
+ export interface ReviewPackCard {
9
+ card_id: string;
10
+ title: string;
11
+ candidate_ids: string[];
12
+ decision_inputs: {
13
+ required_hops: string[];
14
+ failure_map: Record<string, string>;
15
+ guarantees: string[];
16
+ non_guarantees: string[];
17
+ };
18
+ }
19
+ export interface ReviewPackMeta {
20
+ token_budget: number;
21
+ token_budget_estimate: number;
22
+ truncated: boolean;
23
+ total_candidates: number;
24
+ included_candidates: number;
25
+ }
26
+ export interface ReviewPackOutput {
27
+ paths: ReturnType<typeof getRuleLabPaths>;
28
+ meta: ReviewPackMeta;
29
+ cards: ReviewPackCard[];
30
+ }
31
+ export declare function buildReviewPack(input: ReviewPackInput): Promise<ReviewPackOutput>;
@@ -0,0 +1,125 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { getRuleLabPaths } from './paths.js';
4
+ function estimateTokens(value) {
5
+ return Math.ceil(JSON.stringify(value).length / 4);
6
+ }
7
+ function parseCandidates(raw) {
8
+ return raw
9
+ .split(/\r?\n/)
10
+ .map((line) => line.trim())
11
+ .filter(Boolean)
12
+ .map((line) => JSON.parse(line));
13
+ }
14
+ function unique(values) {
15
+ return [...new Set(values.map((value) => String(value || '').trim()).filter(Boolean))];
16
+ }
17
+ function mergeFailureMaps(candidates) {
18
+ const out = {};
19
+ for (const candidate of candidates) {
20
+ const failureMap = candidate.closure?.failure_map || {};
21
+ for (const [key, value] of Object.entries(failureMap)) {
22
+ if (String(key || '').trim() && String(value || '').trim()) {
23
+ out[key] = value;
24
+ }
25
+ }
26
+ }
27
+ return out;
28
+ }
29
+ function collectRequiredHops(candidates) {
30
+ const fromClosure = candidates.flatMap((candidate) => {
31
+ const closure = candidate.closure;
32
+ return Array.isArray(closure?.required_hops) ? closure.required_hops : [];
33
+ });
34
+ if (fromClosure.length > 0)
35
+ return unique(fromClosure);
36
+ return unique(candidates.flatMap((candidate) => (candidate.topology || []).map((hop) => hop.hop)));
37
+ }
38
+ function collectClaims(candidates) {
39
+ const guarantees = unique(candidates.flatMap((candidate) => {
40
+ const claims = candidate.claims;
41
+ return Array.isArray(claims?.guarantees) ? claims.guarantees : [];
42
+ }));
43
+ const nonGuarantees = unique(candidates.flatMap((candidate) => {
44
+ const claims = candidate.claims;
45
+ return Array.isArray(claims?.non_guarantees) ? claims.non_guarantees : [];
46
+ }));
47
+ return {
48
+ guarantees,
49
+ non_guarantees: nonGuarantees,
50
+ };
51
+ }
52
+ function buildCards(candidates) {
53
+ const cards = [];
54
+ const chunkSize = 4;
55
+ for (let i = 0; i < candidates.length; i += chunkSize) {
56
+ const chunk = candidates.slice(i, i + chunkSize);
57
+ const claims = collectClaims(chunk);
58
+ cards.push({
59
+ card_id: `card-${Math.floor(i / chunkSize) + 1}`,
60
+ title: `Rule Lab Card ${Math.floor(i / chunkSize) + 1}`,
61
+ candidate_ids: chunk.map((item) => item.id),
62
+ decision_inputs: {
63
+ required_hops: collectRequiredHops(chunk),
64
+ failure_map: mergeFailureMaps(chunk),
65
+ guarantees: claims.guarantees,
66
+ non_guarantees: claims.non_guarantees,
67
+ },
68
+ });
69
+ }
70
+ return cards;
71
+ }
72
+ function renderReviewPack(meta, cards) {
73
+ const lines = [];
74
+ lines.push('# Rule Lab Review Pack');
75
+ lines.push('');
76
+ lines.push('## Meta');
77
+ lines.push(`- token_budget: ${meta.token_budget}`);
78
+ lines.push(`- token_budget_estimate: ${meta.token_budget_estimate}`);
79
+ lines.push(`- truncated: ${meta.truncated}`);
80
+ lines.push(`- total_candidates: ${meta.total_candidates}`);
81
+ lines.push(`- included_candidates: ${meta.included_candidates}`);
82
+ lines.push('');
83
+ for (const card of cards) {
84
+ lines.push(`## ${card.title}`);
85
+ lines.push(`- card_id: ${card.card_id}`);
86
+ lines.push(`- candidate_ids: ${card.candidate_ids.join(', ')}`);
87
+ lines.push(`- required_hops: ${card.decision_inputs.required_hops.join(', ')}`);
88
+ lines.push(`- guarantees: ${card.decision_inputs.guarantees.join(', ')}`);
89
+ lines.push(`- non_guarantees: ${card.decision_inputs.non_guarantees.join(', ')}`);
90
+ lines.push(`- failure_map: ${JSON.stringify(card.decision_inputs.failure_map)}`);
91
+ lines.push('');
92
+ }
93
+ return `${lines.join('\n')}\n`;
94
+ }
95
+ export async function buildReviewPack(input) {
96
+ const normalizedRepoPath = path.resolve(input.repoPath);
97
+ const paths = getRuleLabPaths(normalizedRepoPath, input.runId, input.sliceId);
98
+ const raw = await fs.readFile(paths.candidatesPath, 'utf-8');
99
+ const candidates = parseCandidates(raw);
100
+ const included = [];
101
+ let tokenEstimate = 0;
102
+ for (const candidate of candidates) {
103
+ const nextTokens = estimateTokens(candidate);
104
+ if (tokenEstimate + nextTokens > input.maxTokens) {
105
+ break;
106
+ }
107
+ included.push(candidate);
108
+ tokenEstimate += nextTokens;
109
+ }
110
+ const meta = {
111
+ token_budget: input.maxTokens,
112
+ token_budget_estimate: tokenEstimate,
113
+ truncated: included.length < candidates.length,
114
+ total_candidates: candidates.length,
115
+ included_candidates: included.length,
116
+ };
117
+ const cards = buildCards(included);
118
+ await fs.mkdir(path.dirname(paths.reviewCardsPath), { recursive: true });
119
+ await fs.writeFile(paths.reviewCardsPath, renderReviewPack(meta, cards), 'utf-8');
120
+ return {
121
+ paths,
122
+ meta,
123
+ cards,
124
+ };
125
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
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 { buildReviewPack } from './review-pack.js';
6
+ describe('rule-lab review-pack', () => {
7
+ it('splits cards to keep token budget <= 6000', async () => {
8
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'rule-lab-review-pack-'));
9
+ const sliceDir = path.join(repoRoot, '.gitnexus', 'rules', 'lab', 'runs', 'run-x', 'slices', 'slice-a');
10
+ await fs.mkdir(sliceDir, { recursive: true });
11
+ const candidate = {
12
+ id: 'cand-1',
13
+ title: 'reload candidate',
14
+ topology: [
15
+ {
16
+ hop: 'resource',
17
+ from: { entity: 'resource' },
18
+ to: { entity: 'script' },
19
+ edge: { kind: 'binds_script' },
20
+ },
21
+ ],
22
+ closure: {
23
+ required_hops: ['resource'],
24
+ failure_map: { missing_evidence: 'rule_matched_but_evidence_missing' },
25
+ },
26
+ claims: {
27
+ guarantees: ['reload_chain_closed'],
28
+ non_guarantees: ['no_runtime_execution'],
29
+ },
30
+ evidence: {
31
+ hops: [
32
+ { hop_type: 'resource', anchor: 'Assets/Example.prefab:42', snippet: 'ReloadGraph' },
33
+ ],
34
+ },
35
+ };
36
+ const lines = Array.from({ length: 12 }).map((_, i) => JSON.stringify({ ...candidate, id: `cand-${i}` }));
37
+ await fs.writeFile(path.join(sliceDir, 'candidates.jsonl'), `${lines.join('\n')}\n`, 'utf-8');
38
+ const out = await buildReviewPack({ repoPath: repoRoot, runId: 'run-x', sliceId: 'slice-a', maxTokens: 6000 });
39
+ expect(out.meta.token_budget_estimate).toBeLessThanOrEqual(6000);
40
+ expect(out.meta.truncated || out.cards.length > 0).toBe(true);
41
+ expect(out.cards[0]).toHaveProperty('decision_inputs.required_hops');
42
+ expect(out.cards[0]).toHaveProperty('decision_inputs.failure_map');
43
+ expect(out.cards[0]).toHaveProperty('decision_inputs.guarantees');
44
+ expect(out.cards[0]).toHaveProperty('decision_inputs.non_guarantees');
45
+ const persisted = await fs.readFile(out.paths.reviewCardsPath, 'utf-8');
46
+ expect(persisted).toContain('token_budget_estimate');
47
+ await fs.rm(repoRoot, { recursive: true, force: true });
48
+ });
49
+ });