ripp-cli 1.0.1 → 1.2.1

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/lib/metrics.js ADDED
@@ -0,0 +1,410 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+
5
+ /**
6
+ * Gather metrics about the RIPP workflow in the current repository.
7
+ * Metrics are best-effort and never fabricated - if data is unavailable, it is marked as N/A.
8
+ *
9
+ * @param {string} rippDir - Path to .ripp directory (default: ./.ripp)
10
+ * @returns {object} Metrics object with evidence, discovery, validation, and workflow stats
11
+ */
12
+ function gatherMetrics(rippDir = './.ripp') {
13
+ const metrics = {
14
+ timestamp: new Date().toISOString(),
15
+ evidence: gatherEvidenceMetrics(rippDir),
16
+ discovery: gatherDiscoveryMetrics(rippDir),
17
+ validation: gatherValidationMetrics(rippDir),
18
+ workflow: gatherWorkflowMetrics(rippDir)
19
+ };
20
+
21
+ return metrics;
22
+ }
23
+
24
+ /**
25
+ * Gather evidence pack metrics
26
+ */
27
+ function gatherEvidenceMetrics(rippDir) {
28
+ const evidenceIndexPath = path.join(rippDir, 'evidence', 'evidence.index.json');
29
+
30
+ if (!fs.existsSync(evidenceIndexPath)) {
31
+ return {
32
+ status: 'not_built',
33
+ file_count: 0,
34
+ total_size: 0,
35
+ coverage_percent: 0
36
+ };
37
+ }
38
+
39
+ try {
40
+ const evidenceIndex = JSON.parse(fs.readFileSync(evidenceIndexPath, 'utf8'));
41
+ const fileCount = evidenceIndex.total_files || evidenceIndex.files?.length || 0;
42
+ const totalSize = evidenceIndex.total_size || 0;
43
+
44
+ // Calculate coverage: evidence files vs total git-tracked files
45
+ let gitFileCount = 0;
46
+ try {
47
+ const gitFiles = execSync('git ls-files --exclude-standard', {
48
+ encoding: 'utf8',
49
+ stdio: ['pipe', 'pipe', 'ignore'],
50
+ cwd: path.dirname(rippDir)
51
+ });
52
+ gitFileCount = gitFiles
53
+ .trim()
54
+ .split('\n')
55
+ .filter(f => f.length > 0).length;
56
+ } catch (error) {
57
+ // Not a git repo or git command failed, coverage unknown
58
+ gitFileCount = fileCount; // Assume 100% if git unavailable
59
+ }
60
+
61
+ const coveragePercent = gitFileCount > 0 ? Math.round((fileCount / gitFileCount) * 100) : 0;
62
+
63
+ return {
64
+ status: 'built',
65
+ file_count: fileCount,
66
+ total_size: totalSize,
67
+ coverage_percent: coveragePercent,
68
+ last_build: evidenceIndex.timestamp || 'unknown'
69
+ };
70
+ } catch (error) {
71
+ return {
72
+ status: 'error',
73
+ file_count: 0,
74
+ total_size: 0,
75
+ coverage_percent: 0,
76
+ error: error.message
77
+ };
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Gather discovery metrics (AI-generated candidates)
83
+ */
84
+ function gatherDiscoveryMetrics(rippDir) {
85
+ const candidatesPath = path.join(rippDir, 'intent.candidates.yaml');
86
+
87
+ if (!fs.existsSync(candidatesPath)) {
88
+ return {
89
+ status: 'not_run',
90
+ candidate_count: 0
91
+ };
92
+ }
93
+
94
+ try {
95
+ const yaml = require('js-yaml');
96
+ const candidatesContent = fs.readFileSync(candidatesPath, 'utf8');
97
+ const candidates = yaml.load(candidatesContent);
98
+
99
+ const candidateCount = candidates.candidates?.length || 0;
100
+
101
+ // Calculate average confidence if available
102
+ let avgConfidence = null;
103
+ if (candidates.candidates && candidateCount > 0) {
104
+ const confidences = candidates.candidates
105
+ .map(c => c.confidence)
106
+ .filter(conf => typeof conf === 'number' && conf >= 0 && conf <= 1);
107
+
108
+ if (confidences.length > 0) {
109
+ avgConfidence = confidences.reduce((sum, c) => sum + c, 0) / confidences.length;
110
+ }
111
+ }
112
+
113
+ // Quality score (simple heuristic: avg confidence * candidate count normalization)
114
+ const qualityScore =
115
+ avgConfidence !== null
116
+ ? Math.round(avgConfidence * Math.min(candidateCount / 3, 1) * 100)
117
+ : null;
118
+
119
+ return {
120
+ status: 'completed',
121
+ candidate_count: candidateCount,
122
+ avg_confidence: avgConfidence !== null ? Math.round(avgConfidence * 100) / 100 : null,
123
+ quality_score: qualityScore,
124
+ model: candidates.metadata?.model || 'unknown'
125
+ };
126
+ } catch (error) {
127
+ return {
128
+ status: 'error',
129
+ candidate_count: 0,
130
+ error: error.message
131
+ };
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Gather validation metrics
137
+ */
138
+ function gatherValidationMetrics(rippDir) {
139
+ // Look for canonical handoff packet
140
+ const handoffPath = path.join(rippDir, 'handoff.ripp.yaml');
141
+
142
+ if (!fs.existsSync(handoffPath)) {
143
+ return {
144
+ status: 'not_validated',
145
+ last_run: null
146
+ };
147
+ }
148
+
149
+ try {
150
+ // Get file mtime as proxy for last validation
151
+ const stats = fs.statSync(handoffPath);
152
+ const lastRun = stats.mtime.toISOString();
153
+
154
+ // Attempt basic validation check (packet must be parseable YAML with ripp_version)
155
+ const yaml = require('js-yaml');
156
+ const packetContent = fs.readFileSync(handoffPath, 'utf8');
157
+ const packet = yaml.load(packetContent);
158
+
159
+ const isValid = packet && packet.ripp_version === '1.0' && packet.packet_id && packet.level;
160
+
161
+ return {
162
+ status: isValid ? 'pass' : 'fail',
163
+ last_run: lastRun,
164
+ level: packet.level || null
165
+ };
166
+ } catch (error) {
167
+ return {
168
+ status: 'fail',
169
+ last_run: null,
170
+ error: error.message
171
+ };
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Gather workflow completion metrics
177
+ */
178
+ function gatherWorkflowMetrics(rippDir) {
179
+ // Define expected artifacts for each workflow step
180
+ const steps = {
181
+ initialized: fs.existsSync(path.join(rippDir, 'config.yaml')),
182
+ evidence_built: fs.existsSync(path.join(rippDir, 'evidence', 'evidence.index.json')),
183
+ discovery_run: fs.existsSync(path.join(rippDir, 'intent.candidates.yaml')),
184
+ checklist_generated: fs.existsSync(path.join(rippDir, 'intent.checklist.md')),
185
+ artifacts_built: fs.existsSync(path.join(rippDir, 'handoff.ripp.yaml'))
186
+ };
187
+
188
+ const completedSteps = Object.values(steps).filter(Boolean).length;
189
+ const totalSteps = Object.keys(steps).length;
190
+ const completionPercent = Math.round((completedSteps / totalSteps) * 100);
191
+
192
+ return {
193
+ completion_percent: completionPercent,
194
+ steps_completed: completedSteps,
195
+ steps_total: totalSteps,
196
+ steps: steps
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Format metrics as human-readable text
202
+ */
203
+ function formatMetricsText(metrics) {
204
+ const lines = [];
205
+
206
+ lines.push('RIPP Workflow Metrics');
207
+ lines.push('='.repeat(60));
208
+ lines.push('');
209
+
210
+ // Evidence metrics
211
+ lines.push('Evidence Pack:');
212
+ if (metrics.evidence.status === 'built') {
213
+ lines.push(` Status: ✓ Built`);
214
+ lines.push(` Files: ${metrics.evidence.file_count}`);
215
+ lines.push(` Size: ${formatBytes(metrics.evidence.total_size)}`);
216
+ lines.push(` Coverage: ${metrics.evidence.coverage_percent}% of git-tracked files`);
217
+ lines.push(` Last Build: ${formatTimestamp(metrics.evidence.last_build)}`);
218
+ } else if (metrics.evidence.status === 'not_built') {
219
+ lines.push(` Status: ✗ Not built`);
220
+ lines.push(` Next Step: Run 'ripp evidence build'`);
221
+ } else {
222
+ lines.push(` Status: ✗ Error`);
223
+ lines.push(` Error: ${metrics.evidence.error || 'Unknown'}`);
224
+ }
225
+ lines.push('');
226
+
227
+ // Discovery metrics
228
+ lines.push('Intent Discovery:');
229
+ if (metrics.discovery.status === 'completed') {
230
+ lines.push(` Status: ✓ Completed`);
231
+ lines.push(` Candidates: ${metrics.discovery.candidate_count}`);
232
+ if (metrics.discovery.avg_confidence !== null) {
233
+ lines.push(` Avg Confidence: ${(metrics.discovery.avg_confidence * 100).toFixed(0)}%`);
234
+ }
235
+ if (metrics.discovery.quality_score !== null) {
236
+ lines.push(` Quality Score: ${metrics.discovery.quality_score}/100`);
237
+ }
238
+ lines.push(` Model: ${metrics.discovery.model}`);
239
+ } else if (metrics.discovery.status === 'not_run') {
240
+ lines.push(` Status: ✗ Not run`);
241
+ lines.push(` Next Step: Run 'ripp discover'`);
242
+ } else {
243
+ lines.push(` Status: ✗ Error`);
244
+ lines.push(` Error: ${metrics.discovery.error || 'Unknown'}`);
245
+ }
246
+ lines.push('');
247
+
248
+ // Validation metrics
249
+ lines.push('Validation:');
250
+ if (metrics.validation.status === 'pass') {
251
+ lines.push(` Status: ✓ Pass`);
252
+ lines.push(` Level: ${metrics.validation.level}`);
253
+ lines.push(` Last Run: ${formatTimestamp(metrics.validation.last_run)}`);
254
+ } else if (metrics.validation.status === 'fail') {
255
+ lines.push(` Status: ✗ Fail`);
256
+ if (metrics.validation.error) {
257
+ lines.push(` Error: ${metrics.validation.error}`);
258
+ }
259
+ } else {
260
+ lines.push(` Status: - Not validated`);
261
+ lines.push(` Next Step: Run 'ripp build' to create handoff packet`);
262
+ }
263
+ lines.push('');
264
+
265
+ // Workflow completion
266
+ lines.push('Workflow Progress:');
267
+ lines.push(
268
+ ` Completion: ${metrics.workflow.completion_percent}% (${metrics.workflow.steps_completed}/${metrics.workflow.steps_total} steps)`
269
+ );
270
+ lines.push(` Steps:`);
271
+ lines.push(
272
+ ` ${metrics.workflow.steps.initialized ? '✓' : '✗'} Initialized (.ripp/config.yaml)`
273
+ );
274
+ lines.push(` ${metrics.workflow.steps.evidence_built ? '✓' : '✗'} Evidence Built`);
275
+ lines.push(` ${metrics.workflow.steps.discovery_run ? '✓' : '✗'} Discovery Run`);
276
+ lines.push(` ${metrics.workflow.steps.checklist_generated ? '✓' : '✗'} Checklist Generated`);
277
+ lines.push(` ${metrics.workflow.steps.artifacts_built ? '✓' : '✗'} Artifacts Built`);
278
+ lines.push('');
279
+
280
+ lines.push('='.repeat(60));
281
+ lines.push(`Generated: ${formatTimestamp(metrics.timestamp)}`);
282
+
283
+ return lines.join('\n');
284
+ }
285
+
286
+ /**
287
+ * Format bytes as human-readable size
288
+ */
289
+ function formatBytes(bytes) {
290
+ if (bytes === 0) return '0 B';
291
+ const k = 1024;
292
+ const sizes = ['B', 'KB', 'MB', 'GB'];
293
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
294
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
295
+ }
296
+
297
+ /**
298
+ * Format ISO timestamp as human-readable
299
+ */
300
+ function formatTimestamp(timestamp) {
301
+ if (!timestamp || timestamp === 'unknown') return 'Unknown';
302
+ try {
303
+ const date = new Date(timestamp);
304
+ return date.toLocaleString();
305
+ } catch (error) {
306
+ return timestamp;
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Load metrics history from .ripp/metrics-history.json
312
+ */
313
+ function loadMetricsHistory(rippDir) {
314
+ const historyPath = path.join(rippDir, 'metrics-history.json');
315
+
316
+ if (!fs.existsSync(historyPath)) {
317
+ return [];
318
+ }
319
+
320
+ try {
321
+ const historyContent = fs.readFileSync(historyPath, 'utf8');
322
+ return JSON.parse(historyContent);
323
+ } catch (error) {
324
+ console.error(`Warning: Could not load metrics history: ${error.message}`);
325
+ return [];
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Save metrics to history
331
+ */
332
+ function saveMetricsHistory(rippDir, metrics) {
333
+ const historyPath = path.join(rippDir, 'metrics-history.json');
334
+
335
+ try {
336
+ const history = loadMetricsHistory(rippDir);
337
+
338
+ // Add current metrics to history (keep last 50 entries)
339
+ history.push({
340
+ timestamp: metrics.timestamp,
341
+ evidence: metrics.evidence,
342
+ discovery: metrics.discovery,
343
+ validation: metrics.validation,
344
+ workflow: metrics.workflow
345
+ });
346
+
347
+ const trimmedHistory = history.slice(-50);
348
+
349
+ fs.writeFileSync(historyPath, JSON.stringify(trimmedHistory, null, 2), 'utf8');
350
+ } catch (error) {
351
+ console.error(`Warning: Could not save metrics history: ${error.message}`);
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Format metrics history as text
357
+ */
358
+ function formatMetricsHistory(history) {
359
+ if (history.length === 0) {
360
+ return 'No metrics history available. Run `ripp metrics --report` multiple times to build history.';
361
+ }
362
+
363
+ const lines = [];
364
+ lines.push('RIPP Metrics History');
365
+ lines.push('='.repeat(60));
366
+ lines.push('');
367
+
368
+ // Show trends for key metrics
369
+ const recent = history.slice(-10); // Last 10 entries
370
+
371
+ lines.push('Recent Trends (last 10 runs):');
372
+ lines.push('');
373
+
374
+ // Evidence coverage trend
375
+ lines.push('Evidence Coverage:');
376
+ recent.forEach((entry, idx) => {
377
+ const coverage = entry.evidence?.coverage_percent || 0;
378
+ const bar = '█'.repeat(Math.round(coverage / 5));
379
+ lines.push(` ${formatTimestamp(entry.timestamp).padEnd(25)} ${bar} ${coverage}%`);
380
+ });
381
+ lines.push('');
382
+
383
+ // Discovery quality trend
384
+ lines.push('Discovery Quality Score:');
385
+ recent.forEach((entry, idx) => {
386
+ const quality = entry.discovery?.quality_score || 0;
387
+ const bar = '█'.repeat(Math.round(quality / 5));
388
+ lines.push(` ${formatTimestamp(entry.timestamp).padEnd(25)} ${bar} ${quality}/100`);
389
+ });
390
+ lines.push('');
391
+
392
+ // Workflow completion trend
393
+ lines.push('Workflow Completion:');
394
+ recent.forEach((entry, idx) => {
395
+ const completion = entry.workflow?.completion_percent || 0;
396
+ const bar = '█'.repeat(Math.round(completion / 5));
397
+ lines.push(` ${formatTimestamp(entry.timestamp).padEnd(25)} ${bar} ${completion}%`);
398
+ });
399
+ lines.push('');
400
+
401
+ return lines.join('\n');
402
+ }
403
+
404
+ module.exports = {
405
+ gatherMetrics,
406
+ formatMetricsText,
407
+ loadMetricsHistory,
408
+ saveMetricsHistory,
409
+ formatMetricsHistory
410
+ };
package/lib/packager.js CHANGED
@@ -150,28 +150,40 @@ function formatAsYaml(packaged, options = {}) {
150
150
 
151
151
  /**
152
152
  * Format packaged packet as Markdown
153
+ * @param {Object} packaged - Packaged RIPP packet
154
+ * @param {Object} options - Formatting options
155
+ * @param {boolean} options.single - Generate consolidated single-file format
153
156
  */
154
157
  function formatAsMarkdown(packaged, options = {}) {
158
+ const isSingle = options.single || false;
155
159
  let md = '';
156
160
 
157
161
  // Header
158
162
  md += `# ${packaged.title}\n\n`;
159
- md += `**Packet ID**: \`${packaged.packet_id}\` \n`;
160
- md += `**Level**: ${packaged.level} \n`;
161
- md += `**Status**: ${packaged.status} \n`;
162
- md += `**Created**: ${packaged.created} \n`;
163
- md += `**Updated**: ${packaged.updated} \n`;
164
-
165
- if (packaged.version) {
166
- md += `**Version**: ${packaged.version} \n`;
167
- }
168
163
 
169
- md += '\n---\n\n';
164
+ if (isSingle) {
165
+ // Consolidated single-file format (more concise, optimized for AI consumption)
166
+ md += `> **RIPP Handoff Document**\n`;
167
+ md += `> Packet ID: \`${packaged.packet_id}\` | Level: ${packaged.level} | Status: ${packaged.status}\n\n`;
168
+ } else {
169
+ // Standard format with full metadata
170
+ md += `**Packet ID**: \`${packaged.packet_id}\` \n`;
171
+ md += `**Level**: ${packaged.level} \n`;
172
+ md += `**Status**: ${packaged.status} \n`;
173
+ md += `**Created**: ${packaged.created} \n`;
174
+ md += `**Updated**: ${packaged.updated} \n`;
175
+
176
+ if (packaged.version) {
177
+ md += `**Version**: ${packaged.version} \n`;
178
+ }
179
+
180
+ md += '\n---\n\n';
170
181
 
171
- // Packaging metadata
172
- md += '## Packaging Information\n\n';
173
- md += `This document was packaged by \`${packaged._meta.packaged_by}\` on ${packaged._meta.packaged_at}.\n\n`;
174
- md += '---\n\n';
182
+ // Packaging metadata (omit in single-file mode for brevity)
183
+ md += '## Packaging Information\n\n';
184
+ md += `This document was packaged by \`${packaged._meta.packaged_by}\` on ${packaged._meta.packaged_at}.\n\n`;
185
+ md += '---\n\n';
186
+ }
175
187
 
176
188
  // Purpose
177
189
  md += '## Purpose\n\n';
package/package.json CHANGED
@@ -1,13 +1,20 @@
1
1
  {
2
2
  "name": "ripp-cli",
3
- "version": "1.0.1",
4
- "description": "Official CLI validator for Regenerative Intent Prompting Protocol (RIPP)",
3
+ "version": "1.2.1",
4
+ "description": "Official CLI validator and tooling for Regenerative Intent Prompting Protocol (RIPP)",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "ripp": "index.js"
8
8
  },
9
+ "files": [
10
+ "index.js",
11
+ "lib/",
12
+ "schema/",
13
+ "README.md",
14
+ "CHANGELOG.md"
15
+ ],
9
16
  "scripts": {
10
- "test": "echo \"Warning: No tests specified\" && exit 0"
17
+ "test": "node test/checklist-parser.test.js && node test/metrics.test.js && node test/doctor.test.js && node test/package.test.js && node test/integration.test.js"
11
18
  },
12
19
  "keywords": [
13
20
  "ripp",
@@ -0,0 +1,201 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://dylan-natter.github.io/ripp-protocol/schema/evidence-pack.schema.json",
4
+ "title": "RIPP Evidence Pack Schema",
5
+ "description": "Schema for RIPP evidence pack index",
6
+ "type": "object",
7
+ "required": ["version", "created", "stats", "files"],
8
+ "properties": {
9
+ "version": {
10
+ "type": "string",
11
+ "const": "1.0",
12
+ "description": "Evidence pack schema version"
13
+ },
14
+ "created": {
15
+ "type": "string",
16
+ "format": "date-time",
17
+ "description": "ISO 8601 timestamp of evidence pack creation"
18
+ },
19
+ "stats": {
20
+ "type": "object",
21
+ "required": ["totalFiles", "totalSize", "includedFiles", "excludedFiles"],
22
+ "properties": {
23
+ "totalFiles": {
24
+ "type": "integer",
25
+ "minimum": 0,
26
+ "description": "Total number of files scanned"
27
+ },
28
+ "totalSize": {
29
+ "type": "integer",
30
+ "minimum": 0,
31
+ "description": "Total size of included files in bytes"
32
+ },
33
+ "includedFiles": {
34
+ "type": "integer",
35
+ "minimum": 0,
36
+ "description": "Number of files included in evidence"
37
+ },
38
+ "excludedFiles": {
39
+ "type": "integer",
40
+ "minimum": 0,
41
+ "description": "Number of files excluded from evidence"
42
+ }
43
+ }
44
+ },
45
+ "includePatterns": {
46
+ "type": "array",
47
+ "items": {
48
+ "type": "string"
49
+ },
50
+ "description": "Glob patterns used for inclusion"
51
+ },
52
+ "excludePatterns": {
53
+ "type": "array",
54
+ "items": {
55
+ "type": "string"
56
+ },
57
+ "description": "Glob patterns used for exclusion"
58
+ },
59
+ "files": {
60
+ "type": "array",
61
+ "items": {
62
+ "type": "object",
63
+ "required": ["path", "hash", "size"],
64
+ "properties": {
65
+ "path": {
66
+ "type": "string",
67
+ "description": "Relative path from repository root"
68
+ },
69
+ "hash": {
70
+ "type": "string",
71
+ "description": "SHA-256 hash of file content"
72
+ },
73
+ "size": {
74
+ "type": "integer",
75
+ "minimum": 0,
76
+ "description": "File size in bytes"
77
+ },
78
+ "type": {
79
+ "type": "string",
80
+ "enum": ["source", "config", "schema", "workflow", "other"],
81
+ "description": "Detected file type"
82
+ }
83
+ }
84
+ },
85
+ "description": "List of files included in evidence pack"
86
+ },
87
+ "evidence": {
88
+ "type": "object",
89
+ "properties": {
90
+ "dependencies": {
91
+ "type": "array",
92
+ "items": {
93
+ "type": "object",
94
+ "properties": {
95
+ "name": {
96
+ "type": "string"
97
+ },
98
+ "version": {
99
+ "type": "string"
100
+ },
101
+ "type": {
102
+ "type": "string",
103
+ "enum": ["runtime", "dev", "peer"]
104
+ },
105
+ "source": {
106
+ "type": "string",
107
+ "description": "File path where dependency was found"
108
+ }
109
+ }
110
+ },
111
+ "description": "Extracted dependencies"
112
+ },
113
+ "routes": {
114
+ "type": "array",
115
+ "items": {
116
+ "type": "object",
117
+ "properties": {
118
+ "method": {
119
+ "type": "string"
120
+ },
121
+ "path": {
122
+ "type": "string"
123
+ },
124
+ "source": {
125
+ "type": "string",
126
+ "description": "File and line where route was found"
127
+ },
128
+ "snippet": {
129
+ "type": "string",
130
+ "description": "Code snippet"
131
+ }
132
+ }
133
+ },
134
+ "description": "Detected API routes"
135
+ },
136
+ "schemas": {
137
+ "type": "array",
138
+ "items": {
139
+ "type": "object",
140
+ "properties": {
141
+ "name": {
142
+ "type": "string"
143
+ },
144
+ "type": {
145
+ "type": "string",
146
+ "enum": ["model", "migration", "validation"]
147
+ },
148
+ "source": {
149
+ "type": "string"
150
+ },
151
+ "snippet": {
152
+ "type": "string"
153
+ }
154
+ }
155
+ },
156
+ "description": "Detected data schemas"
157
+ },
158
+ "auth": {
159
+ "type": "array",
160
+ "items": {
161
+ "type": "object",
162
+ "properties": {
163
+ "type": {
164
+ "type": "string",
165
+ "enum": ["middleware", "guard", "config"]
166
+ },
167
+ "source": {
168
+ "type": "string"
169
+ },
170
+ "snippet": {
171
+ "type": "string"
172
+ }
173
+ }
174
+ },
175
+ "description": "Authentication/authorization signals"
176
+ },
177
+ "workflows": {
178
+ "type": "array",
179
+ "items": {
180
+ "type": "object",
181
+ "properties": {
182
+ "name": {
183
+ "type": "string"
184
+ },
185
+ "triggers": {
186
+ "type": "array",
187
+ "items": {
188
+ "type": "string"
189
+ }
190
+ },
191
+ "source": {
192
+ "type": "string"
193
+ }
194
+ }
195
+ },
196
+ "description": "CI/CD workflow configurations"
197
+ }
198
+ }
199
+ }
200
+ }
201
+ }