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/analyzer.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RIPP Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Extractive-only tool that generates DRAFT RIPP packets from existing code/schemas.
|
|
5
|
+
*
|
|
6
|
+
* CRITICAL GUARDRAILS:
|
|
7
|
+
* - Extracts ONLY observable facts from code/schemas/APIs
|
|
8
|
+
* - NEVER guesses or invents intent, business logic, or failure modes
|
|
9
|
+
* - Output is ALWAYS marked as 'draft' and requires human review
|
|
10
|
+
* - Does NOT claim generated packets are authoritative
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Analyze input and generate a DRAFT RIPP packet
|
|
18
|
+
* @param {string} inputPath - Path to input file (OpenAPI, JSON Schema, etc.)
|
|
19
|
+
* @param {Object} options - Analysis options
|
|
20
|
+
* @param {number} options.targetLevel - Target RIPP level (1, 2, or 3). Default: 1
|
|
21
|
+
* @returns {Object} Draft RIPP packet
|
|
22
|
+
*/
|
|
23
|
+
function analyzeInput(inputPath, options = {}) {
|
|
24
|
+
const ext = path.extname(inputPath).toLowerCase();
|
|
25
|
+
const content = fs.readFileSync(inputPath, 'utf8');
|
|
26
|
+
const targetLevel = options.targetLevel || 1;
|
|
27
|
+
|
|
28
|
+
let input;
|
|
29
|
+
try {
|
|
30
|
+
input = JSON.parse(content);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new Error(`Failed to parse input file: ${error.message}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Detect input type and extract accordingly
|
|
36
|
+
if (input.openapi || input.swagger) {
|
|
37
|
+
return analyzeOpenAPI(input, { ...options, targetLevel });
|
|
38
|
+
} else if (input.$schema || input.type === 'object') {
|
|
39
|
+
return analyzeJsonSchema(input, { ...options, targetLevel });
|
|
40
|
+
} else {
|
|
41
|
+
throw new Error('Unsupported input type. Currently supports: OpenAPI/Swagger, JSON Schema');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Analyze OpenAPI specification
|
|
47
|
+
* EXTRACTIVE ONLY - does not invent intent or business logic
|
|
48
|
+
*/
|
|
49
|
+
function analyzeOpenAPI(spec, options = {}) {
|
|
50
|
+
const targetLevel = options.targetLevel || 1;
|
|
51
|
+
const packet = createDraftPacket(options.packetId || 'analyzed-api', targetLevel);
|
|
52
|
+
|
|
53
|
+
// Extract title and description if present
|
|
54
|
+
if (spec.info?.title) {
|
|
55
|
+
packet.title = spec.info.title;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (spec.info?.description) {
|
|
59
|
+
packet.purpose.problem = `${spec.info.description} (extracted from OpenAPI spec)`;
|
|
60
|
+
packet.purpose.solution = 'API-based solution (review and refine this section)';
|
|
61
|
+
packet.purpose.value = 'TODO: Define business value';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// For Level 1, keep it simple with basic data contracts
|
|
65
|
+
const dataContracts = extractDataContractsFromOpenAPI(spec);
|
|
66
|
+
if (dataContracts.inputs.length > 0 || dataContracts.outputs.length > 0) {
|
|
67
|
+
packet.data_contracts = dataContracts;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Generate simple UX flow for Level 1
|
|
71
|
+
packet.ux_flow = [
|
|
72
|
+
{
|
|
73
|
+
step: 1,
|
|
74
|
+
actor: 'User',
|
|
75
|
+
action: 'TODO: Define how user initiates API interaction',
|
|
76
|
+
trigger: 'TODO: Define trigger'
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
step: 2,
|
|
80
|
+
actor: 'System',
|
|
81
|
+
action: 'Processes API request',
|
|
82
|
+
result: 'TODO: Define user-visible result'
|
|
83
|
+
}
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// For Level 2+, extract API contracts and add required fields
|
|
87
|
+
if (targetLevel >= 2) {
|
|
88
|
+
const apiContracts = [];
|
|
89
|
+
if (spec.paths) {
|
|
90
|
+
Object.keys(spec.paths).forEach(pathKey => {
|
|
91
|
+
const pathItem = spec.paths[pathKey];
|
|
92
|
+
['get', 'post', 'put', 'delete', 'patch'].forEach(method => {
|
|
93
|
+
if (pathItem[method]) {
|
|
94
|
+
const operation = pathItem[method];
|
|
95
|
+
const contract = extractApiContract(pathKey, method.toUpperCase(), operation);
|
|
96
|
+
if (contract) {
|
|
97
|
+
apiContracts.push(contract);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (apiContracts.length > 0) {
|
|
105
|
+
packet.api_contracts = apiContracts;
|
|
106
|
+
packet.level = 2;
|
|
107
|
+
|
|
108
|
+
// Update UX flow based on API operations
|
|
109
|
+
packet.ux_flow = generatePlaceholderUxFlow(apiContracts);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Add placeholder permissions and failure modes for Level 2
|
|
113
|
+
packet.permissions = [
|
|
114
|
+
{
|
|
115
|
+
action: 'api:access',
|
|
116
|
+
required_roles: ['TODO: Define roles'],
|
|
117
|
+
description: 'TODO: Review and define permission requirements'
|
|
118
|
+
}
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
packet.failure_modes = [
|
|
122
|
+
{
|
|
123
|
+
scenario: 'Invalid request (400 errors observed in spec)',
|
|
124
|
+
impact: 'Request rejected',
|
|
125
|
+
handling: 'Return 400 Bad Request',
|
|
126
|
+
user_message: 'TODO: Define user-facing error messages'
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
scenario: 'Unauthorized access (401 errors observed in spec)',
|
|
130
|
+
impact: 'Access denied',
|
|
131
|
+
handling: 'Return 401 Unauthorized',
|
|
132
|
+
user_message: 'TODO: Define authentication error message'
|
|
133
|
+
}
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return packet;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Extract API contract from OpenAPI operation
|
|
142
|
+
* EXTRACTIVE ONLY - uses only what's in the spec
|
|
143
|
+
*/
|
|
144
|
+
function extractApiContract(endpoint, method, operation) {
|
|
145
|
+
const contract = {
|
|
146
|
+
endpoint,
|
|
147
|
+
method,
|
|
148
|
+
purpose: operation.summary || operation.description || 'TODO: Define purpose',
|
|
149
|
+
response: {
|
|
150
|
+
success: {
|
|
151
|
+
status: 200, // Default, may be overridden
|
|
152
|
+
content_type: 'application/json'
|
|
153
|
+
},
|
|
154
|
+
errors: []
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Extract request schema if present
|
|
159
|
+
if (operation.requestBody?.content?.['application/json']?.schema) {
|
|
160
|
+
contract.request = {
|
|
161
|
+
content_type: 'application/json',
|
|
162
|
+
schema_ref: 'TODO: Map to data_contracts'
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Extract success responses
|
|
167
|
+
if (operation.responses) {
|
|
168
|
+
Object.keys(operation.responses).forEach(statusCode => {
|
|
169
|
+
const code = parseInt(statusCode);
|
|
170
|
+
if (code >= 200 && code < 300) {
|
|
171
|
+
contract.response.success.status = code;
|
|
172
|
+
const response = operation.responses[statusCode];
|
|
173
|
+
if (response.content?.['application/json']) {
|
|
174
|
+
contract.response.success.schema_ref = 'TODO: Map to data_contracts';
|
|
175
|
+
}
|
|
176
|
+
} else if (code >= 400) {
|
|
177
|
+
contract.response.errors.push({
|
|
178
|
+
status: code,
|
|
179
|
+
description: operation.responses[statusCode].description || `HTTP ${code} error`
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Ensure at least one error response
|
|
186
|
+
if (contract.response.errors.length === 0) {
|
|
187
|
+
contract.response.errors.push({
|
|
188
|
+
status: 500,
|
|
189
|
+
description: 'Internal server error (default)'
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return contract;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Extract data contracts from OpenAPI components/definitions
|
|
198
|
+
*/
|
|
199
|
+
function extractDataContractsFromOpenAPI(spec) {
|
|
200
|
+
const inputs = [];
|
|
201
|
+
const outputs = [];
|
|
202
|
+
|
|
203
|
+
const schemas = spec.components?.schemas || spec.definitions || {};
|
|
204
|
+
|
|
205
|
+
Object.keys(schemas).forEach(schemaName => {
|
|
206
|
+
const schema = schemas[schemaName];
|
|
207
|
+
const entity = {
|
|
208
|
+
name: schemaName,
|
|
209
|
+
description: schema.description || 'TODO: Add description',
|
|
210
|
+
fields: []
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Extract fields from schema properties
|
|
214
|
+
if (schema.properties) {
|
|
215
|
+
Object.keys(schema.properties).forEach(propName => {
|
|
216
|
+
const prop = schema.properties[propName];
|
|
217
|
+
entity.fields.push({
|
|
218
|
+
name: propName,
|
|
219
|
+
type: mapOpenApiTypeToRipp(prop.type),
|
|
220
|
+
required: (schema.required || []).includes(propName),
|
|
221
|
+
description: prop.description || 'TODO: Add description'
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (entity.fields.length > 0) {
|
|
227
|
+
// Heuristic: request-like names go to inputs, response-like to outputs
|
|
228
|
+
if (
|
|
229
|
+
schemaName.toLowerCase().includes('request') ||
|
|
230
|
+
schemaName.toLowerCase().includes('input')
|
|
231
|
+
) {
|
|
232
|
+
inputs.push(entity);
|
|
233
|
+
} else if (
|
|
234
|
+
schemaName.toLowerCase().includes('response') ||
|
|
235
|
+
schemaName.toLowerCase().includes('output')
|
|
236
|
+
) {
|
|
237
|
+
outputs.push(entity);
|
|
238
|
+
} else {
|
|
239
|
+
// Default to outputs if unclear
|
|
240
|
+
outputs.push(entity);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return { inputs, outputs };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Analyze JSON Schema
|
|
250
|
+
*/
|
|
251
|
+
function analyzeJsonSchema(schema, options = {}) {
|
|
252
|
+
const targetLevel = options.targetLevel || 1;
|
|
253
|
+
const packet = createDraftPacket(options.packetId || 'analyzed-schema', targetLevel);
|
|
254
|
+
|
|
255
|
+
packet.title = schema.title || 'Analyzed Schema';
|
|
256
|
+
packet.purpose.problem = 'TODO: Define the problem this schema solves';
|
|
257
|
+
packet.purpose.solution = 'TODO: Define the solution approach';
|
|
258
|
+
packet.purpose.value = 'TODO: Define business value';
|
|
259
|
+
|
|
260
|
+
// Extract data contracts
|
|
261
|
+
const entity = {
|
|
262
|
+
name: schema.title || 'MainEntity',
|
|
263
|
+
description: schema.description || 'TODO: Add description',
|
|
264
|
+
fields: []
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
if (schema.properties) {
|
|
268
|
+
Object.keys(schema.properties).forEach(propName => {
|
|
269
|
+
const prop = schema.properties[propName];
|
|
270
|
+
entity.fields.push({
|
|
271
|
+
name: propName,
|
|
272
|
+
type: mapJsonSchemaTypeToRipp(prop.type),
|
|
273
|
+
required: (schema.required || []).includes(propName),
|
|
274
|
+
description: prop.description || 'TODO: Add description'
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (entity.fields.length > 0) {
|
|
280
|
+
packet.data_contracts = {
|
|
281
|
+
outputs: [entity]
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Generate placeholder UX flow
|
|
286
|
+
packet.ux_flow = [
|
|
287
|
+
{
|
|
288
|
+
step: 1,
|
|
289
|
+
actor: 'User',
|
|
290
|
+
action: 'TODO: Define user action',
|
|
291
|
+
trigger: 'TODO: Define trigger'
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
step: 2,
|
|
295
|
+
actor: 'System',
|
|
296
|
+
action: 'TODO: Define system action',
|
|
297
|
+
result: 'TODO: Define result'
|
|
298
|
+
}
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
return packet;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Create a base DRAFT RIPP packet
|
|
306
|
+
* Marked as 'draft' status to require human review
|
|
307
|
+
*/
|
|
308
|
+
function createDraftPacket(packetId, targetLevel = 1) {
|
|
309
|
+
const now = new Date().toISOString().split('T')[0];
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
ripp_version: '1.0',
|
|
313
|
+
packet_id: packetId,
|
|
314
|
+
title: 'DRAFT: Analyzed Feature',
|
|
315
|
+
created: now,
|
|
316
|
+
updated: now,
|
|
317
|
+
status: 'draft', // CRITICAL: Always draft
|
|
318
|
+
level: targetLevel,
|
|
319
|
+
purpose: {
|
|
320
|
+
problem: 'TODO: Define the problem',
|
|
321
|
+
solution: 'TODO: Define the solution',
|
|
322
|
+
value: 'TODO: Define the value'
|
|
323
|
+
},
|
|
324
|
+
ux_flow: [],
|
|
325
|
+
data_contracts: {
|
|
326
|
+
inputs: [],
|
|
327
|
+
outputs: []
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Generate placeholder UX flow from API contracts
|
|
334
|
+
*/
|
|
335
|
+
function generatePlaceholderUxFlow(apiContracts) {
|
|
336
|
+
const flow = [];
|
|
337
|
+
let stepNum = 1;
|
|
338
|
+
|
|
339
|
+
// Add initial step
|
|
340
|
+
flow.push({
|
|
341
|
+
step: stepNum++,
|
|
342
|
+
actor: 'User',
|
|
343
|
+
action: 'TODO: Define how user initiates this workflow',
|
|
344
|
+
trigger: 'TODO: Define trigger'
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Add steps for each API operation
|
|
348
|
+
apiContracts.forEach(api => {
|
|
349
|
+
flow.push({
|
|
350
|
+
step: stepNum++,
|
|
351
|
+
actor: 'System',
|
|
352
|
+
action: `Processes ${api.method} ${api.endpoint}`,
|
|
353
|
+
result: 'TODO: Define user-visible result'
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Ensure at least 2 steps
|
|
358
|
+
if (flow.length < 2) {
|
|
359
|
+
flow.push({
|
|
360
|
+
step: stepNum,
|
|
361
|
+
actor: 'System',
|
|
362
|
+
action: 'TODO: Define system response',
|
|
363
|
+
result: 'TODO: Define result'
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return flow;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Map OpenAPI type to RIPP type
|
|
372
|
+
*/
|
|
373
|
+
function mapOpenApiTypeToRipp(type) {
|
|
374
|
+
const typeMap = {
|
|
375
|
+
string: 'string',
|
|
376
|
+
number: 'number',
|
|
377
|
+
integer: 'integer',
|
|
378
|
+
boolean: 'boolean',
|
|
379
|
+
object: 'object',
|
|
380
|
+
array: 'array'
|
|
381
|
+
};
|
|
382
|
+
return typeMap[type] || 'string';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Map JSON Schema type to RIPP type
|
|
387
|
+
*/
|
|
388
|
+
function mapJsonSchemaTypeToRipp(type) {
|
|
389
|
+
return mapOpenApiTypeToRipp(type);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
module.exports = {
|
|
393
|
+
analyzeInput
|
|
394
|
+
};
|