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/CHANGELOG.md +35 -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/metrics.js +410 -0
- package/lib/packager.js +26 -14
- package/package.json +10 -3
- 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
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,41 @@ All notable changes to the RIPP CLI will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.1] (2025-12-24)
|
|
9
|
+
|
|
10
|
+
### Bug Fixes
|
|
11
|
+
|
|
12
|
+
* **cli:** bundle schema files in npm package to fix validation when installed globally ([ed94409](https://github.com/Dylan-Natter/ripp-protocol/commit/ed94409))
|
|
13
|
+
- Schema files are now included in the npm package under `schema/` directory
|
|
14
|
+
- Updated schema loading paths to use bundled schemas instead of parent directory
|
|
15
|
+
- Fixes post-publish smoke test failures from version 1.2.0
|
|
16
|
+
* **cli:** fix checklist generation and parsing bugs in `ripp confirm` command
|
|
17
|
+
- Fixed extraction of content fields (purpose, ux_flow, data_contracts, etc.) from candidates
|
|
18
|
+
- Use 'purpose' or 'full-packet' as section name instead of 'unknown'
|
|
19
|
+
- Add 'full-packet' to valid section types in checklist parser
|
|
20
|
+
- Fixes empty YAML blocks in generated checklists
|
|
21
|
+
- Fixes 'Unknown section type' error when building from checklist
|
|
22
|
+
|
|
23
|
+
## [1.2.0](https://github.com/Dylan-Natter/ripp-protocol/compare/ripp-cli-v1.1.0...ripp-cli-v1.2.0) (2025-12-23)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
|
|
28
|
+
* **cli:** enhance CLI description to include tooling capabilities ([8f97965](https://github.com/Dylan-Natter/ripp-protocol/commit/8f97965379bbb24287b8d69bb9d4e5af16bca1df))
|
|
29
|
+
|
|
30
|
+
## [1.1.0](https://github.com/Dylan-Natter/ripp-protocol/compare/ripp-cli-v1.0.1...ripp-cli-v1.1.0) (2025-12-23)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### Features
|
|
34
|
+
|
|
35
|
+
* **cli:** add metrics, doctor, and enhanced workflow commands ([b5c413d](https://github.com/Dylan-Natter/ripp-protocol/commit/b5c413d335088350527e6e9aa8cd6fa1f0debf9f))
|
|
36
|
+
* **vscode:** add metrics command and enhanced workflow integration ([24a3cd0](https://github.com/Dylan-Natter/ripp-protocol/commit/24a3cd02f3657958321cc8f04ca55f487853205c))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Documentation
|
|
40
|
+
|
|
41
|
+
* upgrade reference implementation to Level 2 ([a4b18e3](https://github.com/Dylan-Natter/ripp-protocol/commit/a4b18e320d95fbb2754d4eb07dafc8da00eef673))
|
|
42
|
+
|
|
8
43
|
## [1.0.1] - 2025-12-22
|
|
9
44
|
|
|
10
45
|
### Changed
|
package/index.js
CHANGED
|
@@ -17,6 +17,14 @@ const { discoverIntent } = require('./lib/discovery');
|
|
|
17
17
|
const { confirmIntent } = require('./lib/confirmation');
|
|
18
18
|
const { buildCanonicalArtifacts } = require('./lib/build');
|
|
19
19
|
const { migrateDirectoryStructure } = require('./lib/migrate');
|
|
20
|
+
const {
|
|
21
|
+
gatherMetrics,
|
|
22
|
+
formatMetricsText,
|
|
23
|
+
loadMetricsHistory,
|
|
24
|
+
saveMetricsHistory,
|
|
25
|
+
formatMetricsHistory
|
|
26
|
+
} = require('./lib/metrics');
|
|
27
|
+
const { runHealthChecks, formatHealthCheckText } = require('./lib/doctor');
|
|
20
28
|
|
|
21
29
|
// ANSI color codes
|
|
22
30
|
const colors = {
|
|
@@ -38,7 +46,7 @@ function log(color, symbol, message) {
|
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
function loadSchema() {
|
|
41
|
-
const schemaPath = path.join(__dirname, '
|
|
49
|
+
const schemaPath = path.join(__dirname, 'schema/ripp-1.0.schema.json');
|
|
42
50
|
try {
|
|
43
51
|
return JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
|
44
52
|
} catch (error) {
|
|
@@ -277,6 +285,8 @@ ${colors.blue}vNext - Intent Discovery Mode:${colors.reset}
|
|
|
277
285
|
ripp discover Infer candidate intent (requires AI enabled)
|
|
278
286
|
ripp confirm Confirm candidate intent (interactive)
|
|
279
287
|
ripp build Build canonical RIPP artifacts from confirmed intent
|
|
288
|
+
ripp metrics Display workflow analytics and health metrics
|
|
289
|
+
ripp doctor Run health checks and diagnostics
|
|
280
290
|
|
|
281
291
|
ripp --help Show this help message
|
|
282
292
|
ripp --version Show version
|
|
@@ -300,9 +310,15 @@ ${colors.green}Confirm Options:${colors.reset}
|
|
|
300
310
|
--user <id> User identifier for confirmation
|
|
301
311
|
|
|
302
312
|
${colors.green}Build Options:${colors.reset}
|
|
313
|
+
--from-checklist Build from intent.checklist.md (after manual review)
|
|
303
314
|
--packet-id <id> Packet ID for generated RIPP (default: discovered-intent)
|
|
304
315
|
--title <title> Title for generated RIPP packet
|
|
305
316
|
--output-name <file> Output file name (default: handoff.ripp.yaml)
|
|
317
|
+
--user <id> User identifier for confirmation tracking
|
|
318
|
+
|
|
319
|
+
${colors.green}Metrics Options:${colors.reset}
|
|
320
|
+
--report Write metrics to .ripp/metrics.json
|
|
321
|
+
--history Show metrics trends from previous runs
|
|
306
322
|
|
|
307
323
|
${colors.green}Validate Options:${colors.reset}
|
|
308
324
|
--min-level <1|2|3> Enforce minimum RIPP level
|
|
@@ -317,6 +333,7 @@ ${colors.green}Package Options:${colors.reset}
|
|
|
317
333
|
--in <file> Input RIPP packet file (required)
|
|
318
334
|
--out <file> Output file path (required)
|
|
319
335
|
--format <json|yaml|md> Output format (auto-detected from extension)
|
|
336
|
+
--single Generate consolidated single-file markdown
|
|
320
337
|
--package-version <version> Version string for the package (e.g., 1.0.0)
|
|
321
338
|
--force Overwrite existing output file without versioning
|
|
322
339
|
--skip-validation Skip validation entirely
|
|
@@ -339,6 +356,7 @@ ${colors.green}Examples:${colors.reset}
|
|
|
339
356
|
ripp lint ripp/intent/
|
|
340
357
|
ripp lint ripp/intent/ --strict
|
|
341
358
|
ripp package --in feature.ripp.yaml --out handoff.md
|
|
359
|
+
ripp package --in feature.ripp.yaml --out handoff.md --single
|
|
342
360
|
ripp package --in feature.ripp.yaml --out handoff.md --package-version 1.0.0
|
|
343
361
|
ripp package --in feature.ripp.yaml --out handoff.md --force
|
|
344
362
|
ripp package --in feature.ripp.yaml --out handoff.md --warn-on-invalid
|
|
@@ -351,7 +369,10 @@ ${colors.blue}Intent Discovery Examples:${colors.reset}
|
|
|
351
369
|
ripp evidence build
|
|
352
370
|
RIPP_AI_ENABLED=true ripp discover --target-level 2
|
|
353
371
|
ripp confirm --interactive
|
|
354
|
-
ripp
|
|
372
|
+
ripp confirm --checklist
|
|
373
|
+
ripp build
|
|
374
|
+
ripp build --from-checklist
|
|
375
|
+
ripp build --from-checklist --packet-id my-feature --title "My Feature"
|
|
355
376
|
|
|
356
377
|
${colors.gray}Note: Legacy paths (features/, handoffs/, packages/) are supported for backward compatibility.${colors.reset}
|
|
357
378
|
|
|
@@ -430,7 +451,7 @@ function getGitInfo() {
|
|
|
430
451
|
commit,
|
|
431
452
|
branch
|
|
432
453
|
};
|
|
433
|
-
} catch
|
|
454
|
+
} catch {
|
|
434
455
|
// Not in a git repo or git not available
|
|
435
456
|
return null;
|
|
436
457
|
}
|
|
@@ -565,6 +586,10 @@ async function main() {
|
|
|
565
586
|
await handleConfirmCommand(args);
|
|
566
587
|
} else if (command === 'build') {
|
|
567
588
|
await handleBuildCommand(args);
|
|
589
|
+
} else if (command === 'metrics') {
|
|
590
|
+
await handleMetricsCommand(args);
|
|
591
|
+
} else if (command === 'doctor') {
|
|
592
|
+
handleDoctorCommand(args);
|
|
568
593
|
} else {
|
|
569
594
|
console.error(`${colors.red}Error: Unknown command '${command}'${colors.reset}`);
|
|
570
595
|
console.error("Run 'ripp --help' for usage information.");
|
|
@@ -860,7 +885,8 @@ async function handlePackageCommand(args) {
|
|
|
860
885
|
version: null,
|
|
861
886
|
force: args.includes('--force'),
|
|
862
887
|
skipValidation: args.includes('--skip-validation'),
|
|
863
|
-
warnOnInvalid: args.includes('--warn-on-invalid')
|
|
888
|
+
warnOnInvalid: args.includes('--warn-on-invalid'),
|
|
889
|
+
single: args.includes('--single')
|
|
864
890
|
};
|
|
865
891
|
|
|
866
892
|
const inIndex = args.indexOf('--in');
|
|
@@ -1011,7 +1037,7 @@ async function handlePackageCommand(args) {
|
|
|
1011
1037
|
} else if (options.format === 'yaml') {
|
|
1012
1038
|
output = formatAsYaml(packaged);
|
|
1013
1039
|
} else if (options.format === 'md') {
|
|
1014
|
-
output = formatAsMarkdown(packaged);
|
|
1040
|
+
output = formatAsMarkdown(packaged, { single: options.single });
|
|
1015
1041
|
}
|
|
1016
1042
|
|
|
1017
1043
|
// Write to output file
|
|
@@ -1272,7 +1298,7 @@ async function handleConfirmCommand(args) {
|
|
|
1272
1298
|
console.log(' 1. Review and edit the checklist');
|
|
1273
1299
|
console.log(' 2. Mark accepted candidates with [x]');
|
|
1274
1300
|
console.log(' 3. Save the file');
|
|
1275
|
-
console.log(' 4. Run "ripp build" to compile confirmed intent');
|
|
1301
|
+
console.log(' 4. Run "ripp build --from-checklist" to compile confirmed intent');
|
|
1276
1302
|
console.log('');
|
|
1277
1303
|
} else {
|
|
1278
1304
|
log(colors.green, '✓', 'Intent confirmation complete');
|
|
@@ -1302,7 +1328,8 @@ async function handleBuildCommand(args) {
|
|
|
1302
1328
|
const options = {
|
|
1303
1329
|
packetId: null,
|
|
1304
1330
|
title: null,
|
|
1305
|
-
outputName: null
|
|
1331
|
+
outputName: null,
|
|
1332
|
+
fromChecklist: args.includes('--from-checklist')
|
|
1306
1333
|
};
|
|
1307
1334
|
|
|
1308
1335
|
const packetIdIndex = args.indexOf('--packet-id');
|
|
@@ -1320,11 +1347,32 @@ async function handleBuildCommand(args) {
|
|
|
1320
1347
|
options.outputName = args[outputNameIndex + 1];
|
|
1321
1348
|
}
|
|
1322
1349
|
|
|
1323
|
-
|
|
1350
|
+
const userIndex = args.indexOf('--user');
|
|
1351
|
+
if (userIndex !== -1 && args[userIndex + 1]) {
|
|
1352
|
+
options.user = args[userIndex + 1];
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
if (options.fromChecklist) {
|
|
1356
|
+
console.log(`${colors.blue}Building from checklist...${colors.reset}\n`);
|
|
1357
|
+
} else {
|
|
1358
|
+
console.log(`${colors.blue}Building canonical RIPP artifacts...${colors.reset}\n`);
|
|
1359
|
+
}
|
|
1324
1360
|
|
|
1325
1361
|
try {
|
|
1326
1362
|
const result = buildCanonicalArtifacts(cwd, options);
|
|
1327
1363
|
|
|
1364
|
+
// Display summary of checklist processing if applicable
|
|
1365
|
+
if (options.fromChecklist && options._validationResults) {
|
|
1366
|
+
const vr = options._validationResults;
|
|
1367
|
+
console.log(`${colors.blue}Checklist Summary:${colors.reset}`);
|
|
1368
|
+
console.log(` ${colors.gray}Total checked: ${vr.totalChecked}${colors.reset}`);
|
|
1369
|
+
console.log(` ${colors.green}✓ Accepted: ${vr.accepted}${colors.reset}`);
|
|
1370
|
+
if (vr.rejected > 0) {
|
|
1371
|
+
console.log(` ${colors.yellow}⚠ Rejected: ${vr.rejected}${colors.reset}`);
|
|
1372
|
+
}
|
|
1373
|
+
console.log('');
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1328
1376
|
log(colors.green, '✓', 'Build complete');
|
|
1329
1377
|
console.log(` ${colors.gray}RIPP Packet: ${result.packetPath}${colors.reset}`);
|
|
1330
1378
|
console.log(` ${colors.gray}Handoff MD: ${result.markdownPath}${colors.reset}`);
|
|
@@ -1340,6 +1388,88 @@ async function handleBuildCommand(args) {
|
|
|
1340
1388
|
process.exit(0);
|
|
1341
1389
|
} catch (error) {
|
|
1342
1390
|
console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
|
|
1391
|
+
if (options.fromChecklist) {
|
|
1392
|
+
console.log('');
|
|
1393
|
+
console.log(`${colors.blue}Troubleshooting:${colors.reset}`);
|
|
1394
|
+
console.log(' 1. Verify checklist file exists: .ripp/intent.checklist.md');
|
|
1395
|
+
console.log(' 2. Ensure at least one candidate is marked with [x]');
|
|
1396
|
+
console.log(' 3. Check YAML blocks for syntax errors');
|
|
1397
|
+
console.log(' 4. Run "ripp confirm --checklist" to regenerate checklist');
|
|
1398
|
+
}
|
|
1399
|
+
console.log('');
|
|
1400
|
+
process.exit(1);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
async function handleMetricsCommand(args) {
|
|
1405
|
+
const cwd = process.cwd();
|
|
1406
|
+
const rippDir = path.join(cwd, '.ripp');
|
|
1407
|
+
|
|
1408
|
+
// Parse options
|
|
1409
|
+
const options = {
|
|
1410
|
+
report: args.includes('--report'),
|
|
1411
|
+
history: args.includes('--history')
|
|
1412
|
+
};
|
|
1413
|
+
|
|
1414
|
+
// Check if .ripp directory exists
|
|
1415
|
+
if (!fs.existsSync(rippDir)) {
|
|
1416
|
+
console.error(`${colors.red}Error: RIPP directory not found${colors.reset}`);
|
|
1417
|
+
console.error('Run "ripp init" to initialize RIPP in this repository');
|
|
1418
|
+
process.exit(1);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
try {
|
|
1422
|
+
if (options.history) {
|
|
1423
|
+
// Show metrics history
|
|
1424
|
+
const history = loadMetricsHistory(rippDir);
|
|
1425
|
+
console.log(formatMetricsHistory(history));
|
|
1426
|
+
process.exit(0);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// Gather current metrics
|
|
1430
|
+
const metrics = gatherMetrics(rippDir);
|
|
1431
|
+
|
|
1432
|
+
// Display metrics
|
|
1433
|
+
console.log(formatMetricsText(metrics));
|
|
1434
|
+
|
|
1435
|
+
// Write report if requested
|
|
1436
|
+
if (options.report) {
|
|
1437
|
+
const reportPath = path.join(rippDir, 'metrics.json');
|
|
1438
|
+
fs.writeFileSync(reportPath, JSON.stringify(metrics, null, 2), 'utf8');
|
|
1439
|
+
|
|
1440
|
+
// Save to history
|
|
1441
|
+
saveMetricsHistory(rippDir, metrics);
|
|
1442
|
+
|
|
1443
|
+
console.log('');
|
|
1444
|
+
log(colors.green, '✓', `Metrics report saved to ${reportPath}`);
|
|
1445
|
+
console.log('');
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
process.exit(0);
|
|
1449
|
+
} catch (error) {
|
|
1450
|
+
console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
|
|
1451
|
+
process.exit(1);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function handleDoctorCommand() {
|
|
1456
|
+
const cwd = process.cwd();
|
|
1457
|
+
|
|
1458
|
+
console.log(`${colors.blue}Running RIPP health checks...${colors.reset}`);
|
|
1459
|
+
console.log('');
|
|
1460
|
+
|
|
1461
|
+
try {
|
|
1462
|
+
const results = runHealthChecks(cwd);
|
|
1463
|
+
console.log(formatHealthCheckText(results));
|
|
1464
|
+
|
|
1465
|
+
// Exit with non-zero if there are critical failures
|
|
1466
|
+
const hasCriticalFailures = Object.values(results.checks).some(
|
|
1467
|
+
check => check.status === 'fail'
|
|
1468
|
+
);
|
|
1469
|
+
|
|
1470
|
+
process.exit(hasCriticalFailures ? 1 : 0);
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
console.error(`${colors.red}Error running health checks: ${error.message}${colors.reset}`);
|
|
1343
1473
|
process.exit(1);
|
|
1344
1474
|
}
|
|
1345
1475
|
}
|
package/lib/build.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const yaml = require('js-yaml');
|
|
4
|
+
const {
|
|
5
|
+
parseChecklist,
|
|
6
|
+
buildConfirmedIntent,
|
|
7
|
+
validateConfirmedBlocks
|
|
8
|
+
} = require('./checklist-parser');
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
11
|
* RIPP Build - Canonical Artifact Compilation
|
|
@@ -11,17 +16,26 @@ const yaml = require('js-yaml');
|
|
|
11
16
|
* Build canonical RIPP artifacts from confirmed intent
|
|
12
17
|
*/
|
|
13
18
|
function buildCanonicalArtifacts(cwd, options = {}) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
let confirmed;
|
|
20
|
+
|
|
21
|
+
// Check if building from checklist
|
|
22
|
+
if (options.fromChecklist) {
|
|
23
|
+
confirmed = buildFromChecklist(cwd, options);
|
|
24
|
+
} else {
|
|
25
|
+
const confirmedPath = path.join(cwd, '.ripp', 'intent.confirmed.yaml');
|
|
26
|
+
|
|
27
|
+
if (!fs.existsSync(confirmedPath)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'No confirmed intent found. Run "ripp confirm" first, or use "ripp build --from-checklist" to build from the checklist.'
|
|
30
|
+
);
|
|
31
|
+
}
|
|
19
32
|
|
|
20
|
-
|
|
21
|
-
|
|
33
|
+
const confirmedContent = fs.readFileSync(confirmedPath, 'utf8');
|
|
34
|
+
confirmed = yaml.load(confirmedContent);
|
|
22
35
|
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
if (!confirmed.confirmed || confirmed.confirmed.length === 0) {
|
|
37
|
+
throw new Error('No confirmed intent blocks found');
|
|
38
|
+
}
|
|
25
39
|
}
|
|
26
40
|
|
|
27
41
|
// Build RIPP packet from confirmed intent
|
|
@@ -50,6 +64,97 @@ function buildCanonicalArtifacts(cwd, options = {}) {
|
|
|
50
64
|
};
|
|
51
65
|
}
|
|
52
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Build from checklist markdown file
|
|
69
|
+
* Parses checklist, validates checked items, and generates confirmed intent
|
|
70
|
+
*/
|
|
71
|
+
function buildFromChecklist(cwd, options = {}) {
|
|
72
|
+
const checklistPath = path.join(cwd, '.ripp', 'intent.checklist.md');
|
|
73
|
+
|
|
74
|
+
// Check if checklist exists
|
|
75
|
+
if (!fs.existsSync(checklistPath)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Checklist not found at ${checklistPath}. Run "ripp confirm --checklist" to generate it.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Read and parse checklist
|
|
82
|
+
const checklistContent = fs.readFileSync(checklistPath, 'utf8');
|
|
83
|
+
const parseResult = parseChecklist(checklistContent);
|
|
84
|
+
|
|
85
|
+
// Check for parsing errors
|
|
86
|
+
if (parseResult.errors.length > 0) {
|
|
87
|
+
const errorMsg = [
|
|
88
|
+
'Failed to parse checklist:',
|
|
89
|
+
...parseResult.errors.map(e => ` - ${e}`)
|
|
90
|
+
].join('\n');
|
|
91
|
+
throw new Error(errorMsg);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check if any candidates were checked
|
|
95
|
+
if (parseResult.candidates.length === 0) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
'No candidates selected in checklist. Mark candidates with [x] and save the file, then run this command again.'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Display warnings if any
|
|
102
|
+
if (parseResult.warnings.length > 0 && options.showWarnings !== false) {
|
|
103
|
+
console.warn('\n⚠️ Warnings:');
|
|
104
|
+
parseResult.warnings.forEach(w => console.warn(` - ${w}`));
|
|
105
|
+
console.warn('');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Build confirmed intent structure
|
|
109
|
+
const confirmed = buildConfirmedIntent(parseResult.candidates, {
|
|
110
|
+
user: options.user || 'checklist',
|
|
111
|
+
timestamp: new Date().toISOString()
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Validate confirmed blocks
|
|
115
|
+
const validation = validateConfirmedBlocks(confirmed.confirmed);
|
|
116
|
+
|
|
117
|
+
// Store validation results for reporting
|
|
118
|
+
const validationResults = {
|
|
119
|
+
totalChecked: parseResult.candidates.length,
|
|
120
|
+
accepted: validation.accepted.length,
|
|
121
|
+
rejected: validation.rejected.length,
|
|
122
|
+
reasons: validation.reasons
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// If there are rejected blocks, report them
|
|
126
|
+
if (validation.rejected.length > 0) {
|
|
127
|
+
console.warn('\n⚠️ Some candidates were rejected:');
|
|
128
|
+
validation.rejected.forEach(block => {
|
|
129
|
+
const reasons = validation.reasons[block.section] || [];
|
|
130
|
+
console.warn(` - ${block.section}: ${reasons.join(', ')}`);
|
|
131
|
+
});
|
|
132
|
+
console.warn('');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if we have any accepted blocks
|
|
136
|
+
if (validation.accepted.length === 0) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
'No valid candidates found. All selected candidates failed validation. Please review and fix the checklist.'
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Use only accepted blocks
|
|
143
|
+
const finalConfirmed = {
|
|
144
|
+
version: confirmed.version,
|
|
145
|
+
confirmed: validation.accepted
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Save confirmed intent for traceability
|
|
149
|
+
const confirmedPath = path.join(cwd, '.ripp', 'intent.confirmed.yaml');
|
|
150
|
+
fs.writeFileSync(confirmedPath, yaml.dump(finalConfirmed, { indent: 2 }), 'utf8');
|
|
151
|
+
|
|
152
|
+
// Store validation results on options for reporting in handleBuildCommand
|
|
153
|
+
options._validationResults = validationResults;
|
|
154
|
+
|
|
155
|
+
return finalConfirmed;
|
|
156
|
+
}
|
|
157
|
+
|
|
53
158
|
/**
|
|
54
159
|
* Build RIPP packet from confirmed intent blocks
|
|
55
160
|
*/
|
|
@@ -334,5 +439,6 @@ function generateHandoffMarkdown(packet, confirmed) {
|
|
|
334
439
|
module.exports = {
|
|
335
440
|
buildCanonicalArtifacts,
|
|
336
441
|
buildRippPacket,
|
|
337
|
-
generateHandoffMarkdown
|
|
442
|
+
generateHandoffMarkdown,
|
|
443
|
+
buildFromChecklist
|
|
338
444
|
};
|
|
@@ -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}`);
|