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.
@@ -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
+ };