ripp-cli 1.0.0 → 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/CHANGELOG.md +63 -0
- package/README.md +57 -0
- package/index.js +138 -8
- package/lib/build.js +116 -10
- package/lib/checklist-parser.js +224 -0
- package/lib/config.js +2 -3
- package/lib/confirmation.js +77 -6
- package/lib/doctor.js +370 -0
- package/lib/evidence.js +210 -7
- package/lib/metrics.js +410 -0
- package/lib/packager.js +26 -14
- package/package.json +25 -8
- package/schema/evidence-pack.schema.json +201 -0
- package/schema/intent-candidates.schema.json +109 -0
- package/schema/intent-confirmed.schema.json +85 -0
- package/schema/ripp-1.0.schema.json +543 -0
- package/schema/ripp-config.schema.json +104 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
const yaml = require('js-yaml');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RIPP Checklist Parser
|
|
5
|
+
* Parses markdown checklist files generated by `ripp confirm --checklist`
|
|
6
|
+
* and extracts checked candidates with their YAML content.
|
|
7
|
+
*
|
|
8
|
+
* Handles edge cases:
|
|
9
|
+
* - Missing or empty files
|
|
10
|
+
* - Malformed YAML blocks
|
|
11
|
+
* - Partial/truncated blocks
|
|
12
|
+
* - Windows line endings
|
|
13
|
+
* - No items checked
|
|
14
|
+
* - Duplicate entries
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a checklist markdown file and extract checked candidates
|
|
19
|
+
*
|
|
20
|
+
* @param {string} checklistContent - Raw markdown content
|
|
21
|
+
* @returns {Object} - { candidates: Array, errors: Array, warnings: Array }
|
|
22
|
+
*/
|
|
23
|
+
function parseChecklist(checklistContent) {
|
|
24
|
+
if (!checklistContent || checklistContent.trim().length === 0) {
|
|
25
|
+
return {
|
|
26
|
+
candidates: [],
|
|
27
|
+
errors: ['Checklist file is empty'],
|
|
28
|
+
warnings: []
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Normalize line endings (handle Windows CRLF)
|
|
33
|
+
const normalizedContent = checklistContent.replace(/\r\n/g, '\n');
|
|
34
|
+
|
|
35
|
+
const candidates = [];
|
|
36
|
+
const errors = [];
|
|
37
|
+
const warnings = [];
|
|
38
|
+
const seenSections = new Set(); // Track duplicates
|
|
39
|
+
|
|
40
|
+
// Split by candidate sections (## Candidate N: section_name)
|
|
41
|
+
const candidatePattern = /^## Candidate (\d+): (.+)$/gm;
|
|
42
|
+
const matches = [...normalizedContent.matchAll(candidatePattern)];
|
|
43
|
+
|
|
44
|
+
if (matches.length === 0) {
|
|
45
|
+
errors.push('No candidate sections found in checklist');
|
|
46
|
+
return { candidates, errors, warnings };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < matches.length; i++) {
|
|
50
|
+
const match = matches[i];
|
|
51
|
+
const candidateNum = match[1];
|
|
52
|
+
const section = match[2].trim();
|
|
53
|
+
const startIndex = match.index;
|
|
54
|
+
const endIndex = i < matches.length - 1 ? matches[i + 1].index : normalizedContent.length;
|
|
55
|
+
|
|
56
|
+
// Extract the content between this candidate and the next
|
|
57
|
+
const candidateBlock = normalizedContent.substring(startIndex, endIndex);
|
|
58
|
+
|
|
59
|
+
// Check if this candidate is accepted (has [x] checkbox)
|
|
60
|
+
const acceptPattern = /^- \[x\] Accept this candidate$/im;
|
|
61
|
+
const isAccepted = acceptPattern.test(candidateBlock);
|
|
62
|
+
|
|
63
|
+
if (!isAccepted) {
|
|
64
|
+
continue; // Skip unchecked candidates
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Extract confidence (optional, for metadata)
|
|
68
|
+
const confidenceMatch = candidateBlock.match(/\*\*Confidence\*\*: ([\d.]+)%/);
|
|
69
|
+
const confidence = confidenceMatch ? parseFloat(confidenceMatch[1]) / 100 : 0.8;
|
|
70
|
+
|
|
71
|
+
// Extract evidence count (optional, for metadata)
|
|
72
|
+
const evidenceMatch = candidateBlock.match(/\*\*Evidence\*\*: (\d+) reference/);
|
|
73
|
+
const evidenceCount = evidenceMatch ? parseInt(evidenceMatch[1], 10) : 0;
|
|
74
|
+
|
|
75
|
+
// Extract YAML content from code block
|
|
76
|
+
const yamlPattern = /```yaml\n([\s\S]*?)\n```/;
|
|
77
|
+
const yamlMatch = candidateBlock.match(yamlPattern);
|
|
78
|
+
|
|
79
|
+
if (!yamlMatch) {
|
|
80
|
+
errors.push(`Candidate ${candidateNum} (${section}): No YAML content block found`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const yamlContent = yamlMatch[1];
|
|
85
|
+
|
|
86
|
+
// Validate YAML can be parsed
|
|
87
|
+
let parsedContent;
|
|
88
|
+
try {
|
|
89
|
+
parsedContent = yaml.load(yamlContent);
|
|
90
|
+
} catch (yamlError) {
|
|
91
|
+
errors.push(`Candidate ${candidateNum} (${section}): Invalid YAML - ${yamlError.message}`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check for duplicate sections
|
|
96
|
+
if (seenSections.has(section)) {
|
|
97
|
+
warnings.push(
|
|
98
|
+
`Candidate ${candidateNum} (${section}): Duplicate section detected, using first occurrence`
|
|
99
|
+
);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
seenSections.add(section);
|
|
104
|
+
|
|
105
|
+
// Build candidate object
|
|
106
|
+
candidates.push({
|
|
107
|
+
candidateNum,
|
|
108
|
+
section,
|
|
109
|
+
confidence,
|
|
110
|
+
evidenceCount,
|
|
111
|
+
content: parsedContent,
|
|
112
|
+
rawYaml: yamlContent
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
candidates,
|
|
118
|
+
errors,
|
|
119
|
+
warnings
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Convert parsed checklist candidates into confirmed intent format
|
|
125
|
+
*
|
|
126
|
+
* @param {Array} candidates - Parsed candidates from parseChecklist
|
|
127
|
+
* @param {Object} metadata - Optional metadata (user, timestamp)
|
|
128
|
+
* @returns {Object} - Confirmed intent data structure
|
|
129
|
+
*/
|
|
130
|
+
function buildConfirmedIntent(candidates, metadata = {}) {
|
|
131
|
+
const confirmed = candidates.map(candidate => ({
|
|
132
|
+
section: candidate.section,
|
|
133
|
+
source: 'confirmed',
|
|
134
|
+
confirmed_at: metadata.timestamp || new Date().toISOString(),
|
|
135
|
+
confirmed_by: metadata.user || 'checklist',
|
|
136
|
+
original_confidence: candidate.confidence,
|
|
137
|
+
evidence: [], // Evidence references not preserved in checklist format
|
|
138
|
+
content: candidate.content
|
|
139
|
+
}));
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
version: '1.0',
|
|
143
|
+
confirmed
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Validate confirmed intent blocks against quality rules
|
|
149
|
+
*
|
|
150
|
+
* @param {Array} confirmed - Array of confirmed intent blocks
|
|
151
|
+
* @returns {Object} - { accepted: Array, rejected: Array, reasons: Object }
|
|
152
|
+
*/
|
|
153
|
+
function validateConfirmedBlocks(confirmed) {
|
|
154
|
+
const accepted = [];
|
|
155
|
+
const rejected = [];
|
|
156
|
+
const reasons = {};
|
|
157
|
+
|
|
158
|
+
for (const block of confirmed) {
|
|
159
|
+
const blockErrors = [];
|
|
160
|
+
const section = block.section;
|
|
161
|
+
|
|
162
|
+
// Rule 1: Section must be a known RIPP section or full-packet
|
|
163
|
+
const knownSections = [
|
|
164
|
+
'purpose',
|
|
165
|
+
'ux_flow',
|
|
166
|
+
'data_contracts',
|
|
167
|
+
'api_contracts',
|
|
168
|
+
'permissions',
|
|
169
|
+
'failure_modes',
|
|
170
|
+
'audit_events',
|
|
171
|
+
'nfrs',
|
|
172
|
+
'acceptance_tests',
|
|
173
|
+
'design_philosophy',
|
|
174
|
+
'design_decisions',
|
|
175
|
+
'constraints',
|
|
176
|
+
'success_criteria',
|
|
177
|
+
'full-packet' // Allow full packet candidates
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
if (!knownSections.includes(section)) {
|
|
181
|
+
blockErrors.push(`Unknown section type: ${section}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Rule 2: Content must not be empty
|
|
185
|
+
if (!block.content || Object.keys(block.content).length === 0) {
|
|
186
|
+
blockErrors.push('Content is empty');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Rule 3: Check for placeholder values (based on linter rules)
|
|
190
|
+
const contentStr = JSON.stringify(block.content).toLowerCase();
|
|
191
|
+
const placeholders = ['unknown', 'todo', 'tbd', 'fixme', 'placeholder', 'xxx'];
|
|
192
|
+
|
|
193
|
+
for (const placeholder of placeholders) {
|
|
194
|
+
if (contentStr.includes(placeholder)) {
|
|
195
|
+
blockErrors.push(`Contains placeholder value: ${placeholder}`);
|
|
196
|
+
break; // Only report once per block
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Rule 4: Confidence threshold (if available and low)
|
|
201
|
+
if (block.original_confidence && block.original_confidence < 0.5) {
|
|
202
|
+
blockErrors.push(`Low confidence: ${(block.original_confidence * 100).toFixed(1)}%`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (blockErrors.length > 0) {
|
|
206
|
+
rejected.push(block);
|
|
207
|
+
reasons[section] = blockErrors;
|
|
208
|
+
} else {
|
|
209
|
+
accepted.push(block);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
accepted,
|
|
215
|
+
rejected,
|
|
216
|
+
reasons
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = {
|
|
221
|
+
parseChecklist,
|
|
222
|
+
buildConfirmedIntent,
|
|
223
|
+
validateConfirmedBlocks
|
|
224
|
+
};
|
package/lib/config.js
CHANGED
|
@@ -58,9 +58,8 @@ function loadConfig(cwd = process.cwd()) {
|
|
|
58
58
|
config = mergeConfig(config, repoConfig);
|
|
59
59
|
|
|
60
60
|
// Validate against schema
|
|
61
|
-
// Resolve schema path from
|
|
62
|
-
const
|
|
63
|
-
const schemaPath = path.join(projectRoot, 'schema/ripp-config.schema.json');
|
|
61
|
+
// Resolve schema path from bundled schema directory
|
|
62
|
+
const schemaPath = path.join(__dirname, '../schema/ripp-config.schema.json');
|
|
64
63
|
|
|
65
64
|
if (!fs.existsSync(schemaPath)) {
|
|
66
65
|
throw new Error(`Schema file not found at: ${schemaPath}`);
|
package/lib/confirmation.js
CHANGED
|
@@ -53,23 +53,69 @@ async function interactiveConfirm(cwd, candidates) {
|
|
|
53
53
|
const candidate = candidates.candidates[i];
|
|
54
54
|
|
|
55
55
|
console.log(`\n--- Candidate ${i + 1}/${candidates.candidates.length} ---`);
|
|
56
|
-
|
|
56
|
+
const sectionName = candidate.purpose?.problem ? 'purpose' : 'full-packet';
|
|
57
|
+
console.log(`Section: ${sectionName}`);
|
|
57
58
|
console.log(`Confidence: ${(candidate.confidence * 100).toFixed(1)}%`);
|
|
58
59
|
console.log(`Evidence: ${candidate.evidence.length} reference(s)`);
|
|
59
60
|
console.log('\nContent:');
|
|
60
|
-
|
|
61
|
+
// Build content object from candidate fields
|
|
62
|
+
const content = {};
|
|
63
|
+
const contentFields = [
|
|
64
|
+
'purpose',
|
|
65
|
+
'ux_flow',
|
|
66
|
+
'data_contracts',
|
|
67
|
+
'api_contracts',
|
|
68
|
+
'permissions',
|
|
69
|
+
'failure_modes',
|
|
70
|
+
'audit_events',
|
|
71
|
+
'nfrs',
|
|
72
|
+
'acceptance_tests',
|
|
73
|
+
'design_philosophy',
|
|
74
|
+
'design_decisions',
|
|
75
|
+
'constraints',
|
|
76
|
+
'success_criteria'
|
|
77
|
+
];
|
|
78
|
+
contentFields.forEach(field => {
|
|
79
|
+
if (candidate[field]) {
|
|
80
|
+
content[field] = candidate[field];
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
console.log(yaml.dump(content, { indent: 2 }));
|
|
61
84
|
|
|
62
85
|
const answer = await question(rl, '\nAccept this candidate? (y/n/e to edit/s to skip): ');
|
|
63
86
|
|
|
64
87
|
if (answer.toLowerCase() === 'y') {
|
|
88
|
+
// Build content object from candidate fields
|
|
89
|
+
const content = {};
|
|
90
|
+
const contentFields = [
|
|
91
|
+
'purpose',
|
|
92
|
+
'ux_flow',
|
|
93
|
+
'data_contracts',
|
|
94
|
+
'api_contracts',
|
|
95
|
+
'permissions',
|
|
96
|
+
'failure_modes',
|
|
97
|
+
'audit_events',
|
|
98
|
+
'nfrs',
|
|
99
|
+
'acceptance_tests',
|
|
100
|
+
'design_philosophy',
|
|
101
|
+
'design_decisions',
|
|
102
|
+
'constraints',
|
|
103
|
+
'success_criteria'
|
|
104
|
+
];
|
|
105
|
+
contentFields.forEach(field => {
|
|
106
|
+
if (candidate[field]) {
|
|
107
|
+
content[field] = candidate[field];
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
65
111
|
confirmed.push({
|
|
66
|
-
section: candidate.
|
|
112
|
+
section: candidate.purpose?.problem ? 'purpose' : 'full-packet',
|
|
67
113
|
source: 'confirmed',
|
|
68
114
|
confirmed_at: new Date().toISOString(),
|
|
69
115
|
confirmed_by: options.user || 'unknown',
|
|
70
116
|
original_confidence: candidate.confidence,
|
|
71
117
|
evidence: candidate.evidence,
|
|
72
|
-
content:
|
|
118
|
+
content: content
|
|
73
119
|
});
|
|
74
120
|
console.log('✓ Accepted');
|
|
75
121
|
} else if (answer.toLowerCase() === 'e') {
|
|
@@ -136,7 +182,10 @@ async function generateChecklistConfirm(cwd, candidates) {
|
|
|
136
182
|
markdown += '---\n\n';
|
|
137
183
|
|
|
138
184
|
candidates.candidates.forEach((candidate, index) => {
|
|
139
|
-
|
|
185
|
+
// Extract section name from purpose or use generic identifier
|
|
186
|
+
const sectionName = candidate.purpose?.problem ? 'purpose' : 'full-packet';
|
|
187
|
+
|
|
188
|
+
markdown += `## Candidate ${index + 1}: ${sectionName}\n\n`;
|
|
140
189
|
markdown += `- **Confidence**: ${(candidate.confidence * 100).toFixed(1)}%\n`;
|
|
141
190
|
markdown += `- **Evidence**: ${candidate.evidence.length} reference(s)\n\n`;
|
|
142
191
|
|
|
@@ -145,7 +194,29 @@ async function generateChecklistConfirm(cwd, candidates) {
|
|
|
145
194
|
|
|
146
195
|
markdown += '### Content\n\n';
|
|
147
196
|
markdown += '```yaml\n';
|
|
148
|
-
|
|
197
|
+
// Build content object from candidate fields (purpose, ux_flow, data_contracts, etc.)
|
|
198
|
+
const content = {};
|
|
199
|
+
const contentFields = [
|
|
200
|
+
'purpose',
|
|
201
|
+
'ux_flow',
|
|
202
|
+
'data_contracts',
|
|
203
|
+
'api_contracts',
|
|
204
|
+
'permissions',
|
|
205
|
+
'failure_modes',
|
|
206
|
+
'audit_events',
|
|
207
|
+
'nfrs',
|
|
208
|
+
'acceptance_tests',
|
|
209
|
+
'design_philosophy',
|
|
210
|
+
'design_decisions',
|
|
211
|
+
'constraints',
|
|
212
|
+
'success_criteria'
|
|
213
|
+
];
|
|
214
|
+
contentFields.forEach(field => {
|
|
215
|
+
if (candidate[field]) {
|
|
216
|
+
content[field] = candidate[field];
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
markdown += yaml.dump(content, { indent: 2 });
|
|
149
220
|
markdown += '```\n\n';
|
|
150
221
|
|
|
151
222
|
markdown += '### Evidence References\n\n';
|