ripp-cli 1.0.0
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/README.md +292 -0
- package/index.js +1350 -0
- package/lib/ai-provider.js +354 -0
- package/lib/analyzer.js +394 -0
- package/lib/build.js +338 -0
- package/lib/config.js +277 -0
- package/lib/confirmation.js +183 -0
- package/lib/discovery.js +119 -0
- package/lib/evidence.js +368 -0
- package/lib/init.js +488 -0
- package/lib/linter.js +309 -0
- package/lib/migrate.js +203 -0
- package/lib/packager.js +374 -0
- package/package.json +40 -0
package/lib/build.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* RIPP Build - Canonical Artifact Compilation
|
|
7
|
+
* Deterministic compilation of confirmed intent into canonical RIPP artifacts
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build canonical RIPP artifacts from confirmed intent
|
|
12
|
+
*/
|
|
13
|
+
function buildCanonicalArtifacts(cwd, options = {}) {
|
|
14
|
+
const confirmedPath = path.join(cwd, '.ripp', 'intent.confirmed.yaml');
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(confirmedPath)) {
|
|
17
|
+
throw new Error('No confirmed intent found. Run "ripp confirm" first.');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const confirmedContent = fs.readFileSync(confirmedPath, 'utf8');
|
|
21
|
+
const confirmed = yaml.load(confirmedContent);
|
|
22
|
+
|
|
23
|
+
if (!confirmed.confirmed || confirmed.confirmed.length === 0) {
|
|
24
|
+
throw new Error('No confirmed intent blocks found');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Build RIPP packet from confirmed intent
|
|
28
|
+
const packet = buildRippPacket(confirmed, options);
|
|
29
|
+
|
|
30
|
+
// Validate the packet
|
|
31
|
+
const validation = validatePacket(packet);
|
|
32
|
+
if (!validation.valid) {
|
|
33
|
+
throw new Error(`Generated packet failed validation: ${validation.errors.join(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Write canonical packet
|
|
37
|
+
const packetPath = path.join(cwd, '.ripp', options.outputName || 'handoff.ripp.yaml');
|
|
38
|
+
fs.writeFileSync(packetPath, yaml.dump(packet, { indent: 2, lineWidth: 100 }), 'utf8');
|
|
39
|
+
|
|
40
|
+
// Generate handoff.ripp.md (consolidated markdown)
|
|
41
|
+
const markdown = generateHandoffMarkdown(packet, confirmed);
|
|
42
|
+
const markdownPath = path.join(cwd, '.ripp', 'handoff.ripp.md');
|
|
43
|
+
fs.writeFileSync(markdownPath, markdown, 'utf8');
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
packetPath,
|
|
47
|
+
markdownPath,
|
|
48
|
+
packet,
|
|
49
|
+
level: packet.level
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build RIPP packet from confirmed intent blocks
|
|
55
|
+
*/
|
|
56
|
+
function buildRippPacket(confirmed, options) {
|
|
57
|
+
const now = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
58
|
+
|
|
59
|
+
// Initialize packet with required metadata
|
|
60
|
+
const packet = {
|
|
61
|
+
ripp_version: '1.0',
|
|
62
|
+
packet_id: options.packetId || 'discovered-intent',
|
|
63
|
+
title: options.title || 'Discovered Intent',
|
|
64
|
+
created: now,
|
|
65
|
+
updated: now,
|
|
66
|
+
status: 'draft',
|
|
67
|
+
level: determineLevel(confirmed.confirmed),
|
|
68
|
+
purpose: null,
|
|
69
|
+
ux_flow: null,
|
|
70
|
+
data_contracts: null
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Add metadata about generation
|
|
74
|
+
packet.metadata = {
|
|
75
|
+
generated_from: 'ripp-discovery',
|
|
76
|
+
generated_at: new Date().toISOString(),
|
|
77
|
+
source: 'confirmed-intent',
|
|
78
|
+
total_confirmed_blocks: confirmed.confirmed.length
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Populate sections from confirmed intent
|
|
82
|
+
confirmed.confirmed.forEach(block => {
|
|
83
|
+
const section = block.section;
|
|
84
|
+
const content = block.content;
|
|
85
|
+
|
|
86
|
+
switch (section) {
|
|
87
|
+
case 'purpose':
|
|
88
|
+
packet.purpose = content;
|
|
89
|
+
break;
|
|
90
|
+
case 'ux_flow':
|
|
91
|
+
packet.ux_flow = Array.isArray(content) ? content : [content];
|
|
92
|
+
break;
|
|
93
|
+
case 'data_contracts':
|
|
94
|
+
packet.data_contracts = content;
|
|
95
|
+
break;
|
|
96
|
+
case 'api_contracts':
|
|
97
|
+
packet.api_contracts = Array.isArray(content) ? content : [content];
|
|
98
|
+
break;
|
|
99
|
+
case 'permissions':
|
|
100
|
+
packet.permissions = Array.isArray(content) ? content : [content];
|
|
101
|
+
break;
|
|
102
|
+
case 'failure_modes':
|
|
103
|
+
packet.failure_modes = Array.isArray(content) ? content : [content];
|
|
104
|
+
break;
|
|
105
|
+
case 'audit_events':
|
|
106
|
+
packet.audit_events = Array.isArray(content) ? content : [content];
|
|
107
|
+
break;
|
|
108
|
+
case 'nfrs':
|
|
109
|
+
packet.nfrs = content;
|
|
110
|
+
break;
|
|
111
|
+
case 'acceptance_tests':
|
|
112
|
+
packet.acceptance_tests = Array.isArray(content) ? content : [content];
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Ensure required sections exist (add placeholders if missing)
|
|
118
|
+
if (!packet.purpose) {
|
|
119
|
+
packet.purpose = {
|
|
120
|
+
problem: 'TODO: Define the problem being solved',
|
|
121
|
+
solution: 'TODO: Describe the solution approach',
|
|
122
|
+
value: 'TODO: Specify the value delivered'
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!packet.ux_flow || packet.ux_flow.length === 0) {
|
|
127
|
+
packet.ux_flow = [
|
|
128
|
+
{
|
|
129
|
+
step: 1,
|
|
130
|
+
actor: 'User',
|
|
131
|
+
action: 'TODO: Define user interaction',
|
|
132
|
+
trigger: 'TODO: Define trigger'
|
|
133
|
+
}
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!packet.data_contracts) {
|
|
138
|
+
packet.data_contracts = {
|
|
139
|
+
inputs: [],
|
|
140
|
+
outputs: []
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return packet;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Determine RIPP level based on confirmed sections
|
|
149
|
+
*/
|
|
150
|
+
function determineLevel(confirmed) {
|
|
151
|
+
const sections = new Set(confirmed.map(b => b.section));
|
|
152
|
+
|
|
153
|
+
// Level 3 requirements
|
|
154
|
+
if (sections.has('audit_events') || sections.has('nfrs') || sections.has('acceptance_tests')) {
|
|
155
|
+
return 3;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Level 2 requirements
|
|
159
|
+
if (
|
|
160
|
+
sections.has('api_contracts') ||
|
|
161
|
+
sections.has('permissions') ||
|
|
162
|
+
sections.has('failure_modes')
|
|
163
|
+
) {
|
|
164
|
+
return 2;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Default to Level 1
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Basic validation of generated packet
|
|
173
|
+
*/
|
|
174
|
+
function validatePacket(packet) {
|
|
175
|
+
const errors = [];
|
|
176
|
+
|
|
177
|
+
// Required fields
|
|
178
|
+
if (!packet.ripp_version) errors.push('Missing ripp_version');
|
|
179
|
+
if (!packet.packet_id) errors.push('Missing packet_id');
|
|
180
|
+
if (!packet.title) errors.push('Missing title');
|
|
181
|
+
if (!packet.created) errors.push('Missing created');
|
|
182
|
+
if (!packet.updated) errors.push('Missing updated');
|
|
183
|
+
if (!packet.status) errors.push('Missing status');
|
|
184
|
+
if (!packet.level) errors.push('Missing level');
|
|
185
|
+
|
|
186
|
+
// Required sections
|
|
187
|
+
if (!packet.purpose) errors.push('Missing purpose section');
|
|
188
|
+
if (!packet.ux_flow) errors.push('Missing ux_flow section');
|
|
189
|
+
if (!packet.data_contracts) errors.push('Missing data_contracts section');
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
valid: errors.length === 0,
|
|
193
|
+
errors
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Generate handoff.ripp.md (consolidated markdown output)
|
|
199
|
+
*/
|
|
200
|
+
function generateHandoffMarkdown(packet, confirmed) {
|
|
201
|
+
let md = `# ${packet.title}\n\n`;
|
|
202
|
+
md += `**RIPP Packet** — Generated from Intent Discovery Mode\n\n`;
|
|
203
|
+
md += `- **Packet ID**: ${packet.packet_id}\n`;
|
|
204
|
+
md += `- **Level**: ${packet.level}\n`;
|
|
205
|
+
md += `- **Status**: ${packet.status}\n`;
|
|
206
|
+
md += `- **Generated**: ${packet.metadata.generated_at}\n\n`;
|
|
207
|
+
|
|
208
|
+
md += `---\n\n`;
|
|
209
|
+
|
|
210
|
+
// Purpose
|
|
211
|
+
md += `## Purpose\n\n`;
|
|
212
|
+
if (packet.purpose) {
|
|
213
|
+
md += `**Problem**: ${packet.purpose.problem}\n\n`;
|
|
214
|
+
md += `**Solution**: ${packet.purpose.solution}\n\n`;
|
|
215
|
+
md += `**Value**: ${packet.purpose.value}\n\n`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// UX Flow
|
|
219
|
+
md += `## UX Flow\n\n`;
|
|
220
|
+
if (packet.ux_flow) {
|
|
221
|
+
packet.ux_flow.forEach(step => {
|
|
222
|
+
md += `### Step ${step.step}\n\n`;
|
|
223
|
+
md += `- **Actor**: ${step.actor}\n`;
|
|
224
|
+
md += `- **Action**: ${step.action}\n`;
|
|
225
|
+
if (step.trigger) md += `- **Trigger**: ${step.trigger}\n`;
|
|
226
|
+
if (step.result) md += `- **Result**: ${step.result}\n`;
|
|
227
|
+
md += `\n`;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Data Contracts
|
|
232
|
+
md += `## Data Contracts\n\n`;
|
|
233
|
+
if (packet.data_contracts) {
|
|
234
|
+
md += `### Inputs\n\n`;
|
|
235
|
+
if (packet.data_contracts.inputs && packet.data_contracts.inputs.length > 0) {
|
|
236
|
+
packet.data_contracts.inputs.forEach(input => {
|
|
237
|
+
md += `#### ${input.name}\n\n`;
|
|
238
|
+
if (input.fields) {
|
|
239
|
+
input.fields.forEach(field => {
|
|
240
|
+
md += `- **${field.name}** (${field.type})${field.required ? ' *required*' : ''}: ${field.description || 'N/A'}\n`;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
md += `\n`;
|
|
244
|
+
});
|
|
245
|
+
} else {
|
|
246
|
+
md += `*None defined*\n\n`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
md += `### Outputs\n\n`;
|
|
250
|
+
if (packet.data_contracts.outputs && packet.data_contracts.outputs.length > 0) {
|
|
251
|
+
packet.data_contracts.outputs.forEach(output => {
|
|
252
|
+
md += `#### ${output.name}\n\n`;
|
|
253
|
+
if (output.fields) {
|
|
254
|
+
output.fields.forEach(field => {
|
|
255
|
+
md += `- **${field.name}** (${field.type})${field.required ? ' *required*' : ''}: ${field.description || 'N/A'}\n`;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
md += `\n`;
|
|
259
|
+
});
|
|
260
|
+
} else {
|
|
261
|
+
md += `*None defined*\n\n`;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Level 2+ sections
|
|
266
|
+
if (packet.level >= 2) {
|
|
267
|
+
if (packet.api_contracts) {
|
|
268
|
+
md += `## API Contracts\n\n`;
|
|
269
|
+
packet.api_contracts.forEach(api => {
|
|
270
|
+
md += `### ${api.method} ${api.endpoint}\n\n`;
|
|
271
|
+
md += `**Purpose**: ${api.purpose || 'N/A'}\n\n`;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (packet.permissions) {
|
|
276
|
+
md += `## Permissions\n\n`;
|
|
277
|
+
packet.permissions.forEach(perm => {
|
|
278
|
+
md += `- **${perm.action}**: ${perm.description || 'N/A'}\n`;
|
|
279
|
+
});
|
|
280
|
+
md += `\n`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (packet.failure_modes) {
|
|
284
|
+
md += `## Failure Modes\n\n`;
|
|
285
|
+
packet.failure_modes.forEach(fm => {
|
|
286
|
+
md += `- **${fm.scenario}**: ${fm.handling || 'N/A'}\n`;
|
|
287
|
+
});
|
|
288
|
+
md += `\n`;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Level 3 sections
|
|
293
|
+
if (packet.level >= 3) {
|
|
294
|
+
if (packet.audit_events) {
|
|
295
|
+
md += `## Audit Events\n\n`;
|
|
296
|
+
packet.audit_events.forEach(event => {
|
|
297
|
+
md += `- **${event.event}** (${event.severity}): ${event.purpose || 'N/A'}\n`;
|
|
298
|
+
});
|
|
299
|
+
md += `\n`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (packet.nfrs) {
|
|
303
|
+
md += `## Non-Functional Requirements\n\n`;
|
|
304
|
+
md += `\`\`\`yaml\n${yaml.dump(packet.nfrs, { indent: 2 })}\`\`\`\n\n`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (packet.acceptance_tests) {
|
|
308
|
+
md += `## Acceptance Tests\n\n`;
|
|
309
|
+
packet.acceptance_tests.forEach(test => {
|
|
310
|
+
md += `### ${test.title}\n\n`;
|
|
311
|
+
md += `- **Given**: ${test.given}\n`;
|
|
312
|
+
md += `- **When**: ${test.when}\n`;
|
|
313
|
+
md += `- **Then**: ${test.then}\n\n`;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Provenance
|
|
319
|
+
md += `---\n\n`;
|
|
320
|
+
md += `## Provenance\n\n`;
|
|
321
|
+
md += `This RIPP packet was generated using Intent Discovery Mode.\n\n`;
|
|
322
|
+
md += `- **Total Confirmed Blocks**: ${confirmed.confirmed.length}\n`;
|
|
323
|
+
md += `- **Generation Method**: AI-assisted with human confirmation\n`;
|
|
324
|
+
md += `- **Validation**: Passed schema validation at generation time\n\n`;
|
|
325
|
+
|
|
326
|
+
md += `### Confirmed Blocks\n\n`;
|
|
327
|
+
confirmed.confirmed.forEach((block, index) => {
|
|
328
|
+
md += `${index + 1}. **${block.section}** (confidence: ${(block.original_confidence * 100).toFixed(1)}%) - confirmed at ${block.confirmed_at}\n`;
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return md;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = {
|
|
335
|
+
buildCanonicalArtifacts,
|
|
336
|
+
buildRippPacket,
|
|
337
|
+
generateHandoffMarkdown
|
|
338
|
+
};
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
const Ajv = require('ajv');
|
|
5
|
+
const addFormats = require('ajv-formats');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* RIPP Configuration Manager
|
|
9
|
+
* Handles loading and validation of .ripp/config.yaml with precedence rules
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CONFIG = {
|
|
13
|
+
rippVersion: '1.0',
|
|
14
|
+
ai: {
|
|
15
|
+
enabled: false,
|
|
16
|
+
provider: 'openai',
|
|
17
|
+
model: 'gpt-4o-mini',
|
|
18
|
+
maxRetries: 3,
|
|
19
|
+
timeout: 30000
|
|
20
|
+
},
|
|
21
|
+
evidencePack: {
|
|
22
|
+
includeGlobs: ['src/**', 'app/**', 'api/**', 'db/**', '.github/workflows/**'],
|
|
23
|
+
excludeGlobs: [
|
|
24
|
+
'**/node_modules/**',
|
|
25
|
+
'**/dist/**',
|
|
26
|
+
'**/build/**',
|
|
27
|
+
'**/*.lock',
|
|
28
|
+
'**/.git/**',
|
|
29
|
+
'**/vendor/**',
|
|
30
|
+
'**/.ripp/**'
|
|
31
|
+
],
|
|
32
|
+
maxFileSize: 1048576, // 1MB
|
|
33
|
+
secretPatterns: []
|
|
34
|
+
},
|
|
35
|
+
discovery: {
|
|
36
|
+
minConfidence: 0.5,
|
|
37
|
+
includeEvidence: true
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Load RIPP configuration from .ripp/config.yaml
|
|
43
|
+
* Applies precedence: defaults < repo config < env vars
|
|
44
|
+
*/
|
|
45
|
+
function loadConfig(cwd = process.cwd()) {
|
|
46
|
+
const configPath = path.join(cwd, '.ripp', 'config.yaml');
|
|
47
|
+
|
|
48
|
+
// Start with defaults
|
|
49
|
+
let config = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
50
|
+
|
|
51
|
+
// Load repo config if exists
|
|
52
|
+
if (fs.existsSync(configPath)) {
|
|
53
|
+
try {
|
|
54
|
+
const fileContent = fs.readFileSync(configPath, 'utf8');
|
|
55
|
+
const repoConfig = yaml.load(fileContent);
|
|
56
|
+
|
|
57
|
+
// Merge with defaults
|
|
58
|
+
config = mergeConfig(config, repoConfig);
|
|
59
|
+
|
|
60
|
+
// Validate against schema
|
|
61
|
+
// Resolve schema path from project root (3 levels up from lib/)
|
|
62
|
+
const projectRoot = path.join(__dirname, '../../..');
|
|
63
|
+
const schemaPath = path.join(projectRoot, 'schema/ripp-config.schema.json');
|
|
64
|
+
|
|
65
|
+
if (!fs.existsSync(schemaPath)) {
|
|
66
|
+
throw new Error(`Schema file not found at: ${schemaPath}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
|
70
|
+
|
|
71
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
72
|
+
addFormats(ajv);
|
|
73
|
+
const validate = ajv.compile(schema);
|
|
74
|
+
const valid = validate(config);
|
|
75
|
+
|
|
76
|
+
if (!valid) {
|
|
77
|
+
const errors = validate.errors.map(e => `${e.instancePath}: ${e.message}`).join(', ');
|
|
78
|
+
throw new Error(`Invalid .ripp/config.yaml: ${errors}`);
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error.message.includes('Invalid .ripp/config.yaml')) {
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`Failed to parse .ripp/config.yaml: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Apply environment variable overrides
|
|
89
|
+
config = applyEnvOverrides(config);
|
|
90
|
+
|
|
91
|
+
return config;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Merge configuration objects (deep merge for nested objects)
|
|
96
|
+
*/
|
|
97
|
+
function mergeConfig(base, override) {
|
|
98
|
+
const result = { ...base };
|
|
99
|
+
|
|
100
|
+
for (const key in override) {
|
|
101
|
+
if (override[key] !== undefined && override[key] !== null) {
|
|
102
|
+
if (
|
|
103
|
+
typeof override[key] === 'object' &&
|
|
104
|
+
!Array.isArray(override[key]) &&
|
|
105
|
+
typeof base[key] === 'object' &&
|
|
106
|
+
!Array.isArray(base[key])
|
|
107
|
+
) {
|
|
108
|
+
result[key] = mergeConfig(base[key] || {}, override[key]);
|
|
109
|
+
} else {
|
|
110
|
+
result[key] = override[key];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Apply environment variable overrides
|
|
120
|
+
* Env vars take precedence ONLY if ai.enabled=true in repo config
|
|
121
|
+
*/
|
|
122
|
+
function applyEnvOverrides(config) {
|
|
123
|
+
const result = { ...config };
|
|
124
|
+
|
|
125
|
+
// AI configuration (only if enabled in repo)
|
|
126
|
+
if (result.ai.enabled) {
|
|
127
|
+
// RIPP_AI_ENABLED must also be true
|
|
128
|
+
const aiEnabledEnv = process.env.RIPP_AI_ENABLED;
|
|
129
|
+
if (aiEnabledEnv !== undefined) {
|
|
130
|
+
const enabled = aiEnabledEnv.toLowerCase() === 'true';
|
|
131
|
+
if (!enabled) {
|
|
132
|
+
// Env var explicitly disables AI
|
|
133
|
+
result.ai.enabled = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Other AI settings (only if AI is enabled)
|
|
138
|
+
if (result.ai.enabled) {
|
|
139
|
+
if (process.env.RIPP_AI_PROVIDER) {
|
|
140
|
+
result.ai.provider = process.env.RIPP_AI_PROVIDER;
|
|
141
|
+
}
|
|
142
|
+
if (process.env.RIPP_AI_MODEL) {
|
|
143
|
+
result.ai.model = process.env.RIPP_AI_MODEL;
|
|
144
|
+
}
|
|
145
|
+
if (process.env.RIPP_AI_ENDPOINT) {
|
|
146
|
+
result.ai.customEndpoint = process.env.RIPP_AI_ENDPOINT;
|
|
147
|
+
}
|
|
148
|
+
if (process.env.RIPP_AI_MAX_RETRIES) {
|
|
149
|
+
result.ai.maxRetries = parseInt(process.env.RIPP_AI_MAX_RETRIES, 10);
|
|
150
|
+
}
|
|
151
|
+
if (process.env.RIPP_AI_TIMEOUT) {
|
|
152
|
+
result.ai.timeout = parseInt(process.env.RIPP_AI_TIMEOUT, 10);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
// If ai.enabled=false in repo config, AI is ALWAYS OFF
|
|
157
|
+
result.ai.enabled = false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check if AI is enabled and properly configured
|
|
165
|
+
* Returns { enabled: boolean, reason: string }
|
|
166
|
+
*/
|
|
167
|
+
function checkAiEnabled(config) {
|
|
168
|
+
if (!config.ai.enabled) {
|
|
169
|
+
return {
|
|
170
|
+
enabled: false,
|
|
171
|
+
reason: 'AI is disabled in .ripp/config.yaml (ai.enabled: false)'
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check for runtime env var
|
|
176
|
+
const aiEnabledEnv = process.env.RIPP_AI_ENABLED;
|
|
177
|
+
if (aiEnabledEnv === undefined || aiEnabledEnv.toLowerCase() !== 'true') {
|
|
178
|
+
return {
|
|
179
|
+
enabled: false,
|
|
180
|
+
reason: 'AI is enabled in config but RIPP_AI_ENABLED env var is not set to "true"'
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check for API key based on provider
|
|
185
|
+
const provider = config.ai.provider;
|
|
186
|
+
if (provider === 'openai') {
|
|
187
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
188
|
+
return {
|
|
189
|
+
enabled: false,
|
|
190
|
+
reason: 'OPENAI_API_KEY environment variable is not set'
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
} else if (provider === 'azure-openai') {
|
|
194
|
+
if (!process.env.AZURE_OPENAI_API_KEY || !process.env.AZURE_OPENAI_ENDPOINT) {
|
|
195
|
+
return {
|
|
196
|
+
enabled: false,
|
|
197
|
+
reason: 'AZURE_OPENAI_API_KEY and AZURE_OPENAI_ENDPOINT environment variables are required'
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
} else if (provider === 'custom') {
|
|
201
|
+
if (!config.ai.customEndpoint) {
|
|
202
|
+
return {
|
|
203
|
+
enabled: false,
|
|
204
|
+
reason: 'Custom provider requires customEndpoint in config'
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { enabled: true, reason: '' };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Create default .ripp/config.yaml file
|
|
214
|
+
*/
|
|
215
|
+
function createDefaultConfig(cwd = process.cwd(), options = {}) {
|
|
216
|
+
const rippDir = path.join(cwd, '.ripp');
|
|
217
|
+
const configPath = path.join(rippDir, 'config.yaml');
|
|
218
|
+
|
|
219
|
+
// Create .ripp directory if it doesn't exist
|
|
220
|
+
if (!fs.existsSync(rippDir)) {
|
|
221
|
+
fs.mkdirSync(rippDir, { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check if config already exists
|
|
225
|
+
if (fs.existsSync(configPath) && !options.force) {
|
|
226
|
+
return { created: false, path: configPath };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Create config with helpful comments
|
|
230
|
+
const configContent = `# RIPP Configuration
|
|
231
|
+
# Version: 1.0
|
|
232
|
+
# Documentation: https://dylan-natter.github.io/ripp-protocol
|
|
233
|
+
|
|
234
|
+
rippVersion: "1.0"
|
|
235
|
+
|
|
236
|
+
# AI Configuration (vNext Intent Discovery Mode)
|
|
237
|
+
# AI is DISABLED BY DEFAULT for security and trust
|
|
238
|
+
ai:
|
|
239
|
+
enabled: false # Set to true AND set RIPP_AI_ENABLED=true to enable AI features
|
|
240
|
+
provider: openai # openai | azure-openai | ollama | custom
|
|
241
|
+
model: gpt-4o-mini # Model identifier
|
|
242
|
+
|
|
243
|
+
# Evidence Pack Configuration
|
|
244
|
+
evidencePack:
|
|
245
|
+
includeGlobs:
|
|
246
|
+
- "src/**"
|
|
247
|
+
- "app/**"
|
|
248
|
+
- "api/**"
|
|
249
|
+
- "db/**"
|
|
250
|
+
- ".github/workflows/**"
|
|
251
|
+
excludeGlobs:
|
|
252
|
+
- "**/node_modules/**"
|
|
253
|
+
- "**/dist/**"
|
|
254
|
+
- "**/build/**"
|
|
255
|
+
- "**/*.lock"
|
|
256
|
+
- "**/.git/**"
|
|
257
|
+
- "**/vendor/**"
|
|
258
|
+
- "**/.ripp/**"
|
|
259
|
+
maxFileSize: 1048576 # 1MB
|
|
260
|
+
|
|
261
|
+
# Discovery Configuration
|
|
262
|
+
discovery:
|
|
263
|
+
minConfidence: 0.5 # Minimum confidence for AI-inferred intent (0.0-1.0)
|
|
264
|
+
includeEvidence: true # Include evidence references in candidates
|
|
265
|
+
`;
|
|
266
|
+
|
|
267
|
+
fs.writeFileSync(configPath, configContent, 'utf8');
|
|
268
|
+
|
|
269
|
+
return { created: true, path: configPath };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = {
|
|
273
|
+
loadConfig,
|
|
274
|
+
checkAiEnabled,
|
|
275
|
+
createDefaultConfig,
|
|
276
|
+
DEFAULT_CONFIG
|
|
277
|
+
};
|