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 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, '../../schema/ripp-1.0.schema.json');
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 build --packet-id my-feature --title "My Feature"
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 (error) {
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
- console.log(`${colors.blue}Building canonical RIPP artifacts...${colors.reset}\n`);
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
- const confirmedPath = path.join(cwd, '.ripp', 'intent.confirmed.yaml');
15
-
16
- if (!fs.existsSync(confirmedPath)) {
17
- throw new Error('No confirmed intent found. Run "ripp confirm" first.');
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
- const confirmedContent = fs.readFileSync(confirmedPath, 'utf8');
21
- const confirmed = yaml.load(confirmedContent);
33
+ const confirmedContent = fs.readFileSync(confirmedPath, 'utf8');
34
+ confirmed = yaml.load(confirmedContent);
22
35
 
23
- if (!confirmed.confirmed || confirmed.confirmed.length === 0) {
24
- throw new Error('No confirmed intent blocks found');
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 project root (3 levels up from lib/)
62
- const projectRoot = path.join(__dirname, '../../..');
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}`);