New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details
Socket
Book a DemoSign in
Socket

ripp-cli

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ripp-cli - npm Package Compare versions

Comparing version
1.0.1
to
1.2.1
+224
lib/checklist-parser.js
const yaml = require('js-yaml');
/**
* RIPP Checklist Parser
* Parses markdown checklist files generated by `ripp confirm --checklist`
* and extracts checked candidates with their YAML content.
*
* Handles edge cases:
* - Missing or empty files
* - Malformed YAML blocks
* - Partial/truncated blocks
* - Windows line endings
* - No items checked
* - Duplicate entries
*/
/**
* Parse a checklist markdown file and extract checked candidates
*
* @param {string} checklistContent - Raw markdown content
* @returns {Object} - { candidates: Array, errors: Array, warnings: Array }
*/
function parseChecklist(checklistContent) {
if (!checklistContent || checklistContent.trim().length === 0) {
return {
candidates: [],
errors: ['Checklist file is empty'],
warnings: []
};
}
// Normalize line endings (handle Windows CRLF)
const normalizedContent = checklistContent.replace(/\r\n/g, '\n');
const candidates = [];
const errors = [];
const warnings = [];
const seenSections = new Set(); // Track duplicates
// Split by candidate sections (## Candidate N: section_name)
const candidatePattern = /^## Candidate (\d+): (.+)$/gm;
const matches = [...normalizedContent.matchAll(candidatePattern)];
if (matches.length === 0) {
errors.push('No candidate sections found in checklist');
return { candidates, errors, warnings };
}
for (let i = 0; i < matches.length; i++) {
const match = matches[i];
const candidateNum = match[1];
const section = match[2].trim();
const startIndex = match.index;
const endIndex = i < matches.length - 1 ? matches[i + 1].index : normalizedContent.length;
// Extract the content between this candidate and the next
const candidateBlock = normalizedContent.substring(startIndex, endIndex);
// Check if this candidate is accepted (has [x] checkbox)
const acceptPattern = /^- \[x\] Accept this candidate$/im;
const isAccepted = acceptPattern.test(candidateBlock);
if (!isAccepted) {
continue; // Skip unchecked candidates
}
// Extract confidence (optional, for metadata)
const confidenceMatch = candidateBlock.match(/\*\*Confidence\*\*: ([\d.]+)%/);
const confidence = confidenceMatch ? parseFloat(confidenceMatch[1]) / 100 : 0.8;
// Extract evidence count (optional, for metadata)
const evidenceMatch = candidateBlock.match(/\*\*Evidence\*\*: (\d+) reference/);
const evidenceCount = evidenceMatch ? parseInt(evidenceMatch[1], 10) : 0;
// Extract YAML content from code block
const yamlPattern = /```yaml\n([\s\S]*?)\n```/;
const yamlMatch = candidateBlock.match(yamlPattern);
if (!yamlMatch) {
errors.push(`Candidate ${candidateNum} (${section}): No YAML content block found`);
continue;
}
const yamlContent = yamlMatch[1];
// Validate YAML can be parsed
let parsedContent;
try {
parsedContent = yaml.load(yamlContent);
} catch (yamlError) {
errors.push(`Candidate ${candidateNum} (${section}): Invalid YAML - ${yamlError.message}`);
continue;
}
// Check for duplicate sections
if (seenSections.has(section)) {
warnings.push(
`Candidate ${candidateNum} (${section}): Duplicate section detected, using first occurrence`
);
continue;
}
seenSections.add(section);
// Build candidate object
candidates.push({
candidateNum,
section,
confidence,
evidenceCount,
content: parsedContent,
rawYaml: yamlContent
});
}
return {
candidates,
errors,
warnings
};
}
/**
* Convert parsed checklist candidates into confirmed intent format
*
* @param {Array} candidates - Parsed candidates from parseChecklist
* @param {Object} metadata - Optional metadata (user, timestamp)
* @returns {Object} - Confirmed intent data structure
*/
function buildConfirmedIntent(candidates, metadata = {}) {
const confirmed = candidates.map(candidate => ({
section: candidate.section,
source: 'confirmed',
confirmed_at: metadata.timestamp || new Date().toISOString(),
confirmed_by: metadata.user || 'checklist',
original_confidence: candidate.confidence,
evidence: [], // Evidence references not preserved in checklist format
content: candidate.content
}));
return {
version: '1.0',
confirmed
};
}
/**
* Validate confirmed intent blocks against quality rules
*
* @param {Array} confirmed - Array of confirmed intent blocks
* @returns {Object} - { accepted: Array, rejected: Array, reasons: Object }
*/
function validateConfirmedBlocks(confirmed) {
const accepted = [];
const rejected = [];
const reasons = {};
for (const block of confirmed) {
const blockErrors = [];
const section = block.section;
// Rule 1: Section must be a known RIPP section or full-packet
const knownSections = [
'purpose',
'ux_flow',
'data_contracts',
'api_contracts',
'permissions',
'failure_modes',
'audit_events',
'nfrs',
'acceptance_tests',
'design_philosophy',
'design_decisions',
'constraints',
'success_criteria',
'full-packet' // Allow full packet candidates
];
if (!knownSections.includes(section)) {
blockErrors.push(`Unknown section type: ${section}`);
}
// Rule 2: Content must not be empty
if (!block.content || Object.keys(block.content).length === 0) {
blockErrors.push('Content is empty');
}
// Rule 3: Check for placeholder values (based on linter rules)
const contentStr = JSON.stringify(block.content).toLowerCase();
const placeholders = ['unknown', 'todo', 'tbd', 'fixme', 'placeholder', 'xxx'];
for (const placeholder of placeholders) {
if (contentStr.includes(placeholder)) {
blockErrors.push(`Contains placeholder value: ${placeholder}`);
break; // Only report once per block
}
}
// Rule 4: Confidence threshold (if available and low)
if (block.original_confidence && block.original_confidence < 0.5) {
blockErrors.push(`Low confidence: ${(block.original_confidence * 100).toFixed(1)}%`);
}
if (blockErrors.length > 0) {
rejected.push(block);
reasons[section] = blockErrors;
} else {
accepted.push(block);
}
}
return {
accepted,
rejected,
reasons
};
}
module.exports = {
parseChecklist,
buildConfirmedIntent,
validateConfirmedBlocks
};
/**
* RIPP Doctor - Health Check and Diagnostics
*
* Checks repository health and provides actionable fix-it commands
* for common RIPP setup issues.
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
/**
* Run all health checks
* @param {string} cwd - Current working directory
* @returns {Object} Health check results
*/
function runHealthChecks(cwd = process.cwd()) {
const checks = {
nodeVersion: checkNodeVersion(),
gitRepository: checkGitRepository(cwd),
rippDirectory: checkRippDirectory(cwd),
configFile: checkConfigFile(cwd),
evidencePack: checkEvidencePack(cwd),
candidates: checkCandidates(cwd),
confirmedIntent: checkConfirmedIntent(cwd),
schema: checkSchema(),
cliVersion: checkCliVersion()
};
// Calculate overall health
const total = Object.keys(checks).length;
const passed = Object.values(checks).filter(c => c.status === 'pass').length;
const warnings = Object.values(checks).filter(c => c.status === 'warning').length;
return {
checks,
summary: {
total,
passed,
warnings,
failed: total - passed - warnings,
healthy: passed === total
}
};
}
/**
* Check Node.js version (>= 20.0.0)
*/
function checkNodeVersion() {
const version = process.version;
const major = parseInt(version.slice(1).split('.')[0]);
if (major >= 20) {
return {
status: 'pass',
message: `Node.js ${version}`,
fix: null
};
} else {
return {
status: 'fail',
message: `Node.js ${version} is too old`,
fix: 'Install Node.js 20 or later: https://nodejs.org/',
docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html#prerequisites'
};
}
}
/**
* Check if current directory is a Git repository
*/
function checkGitRepository(cwd) {
try {
const gitDir = path.join(cwd, '.git');
if (fs.existsSync(gitDir)) {
return {
status: 'pass',
message: 'Git repository detected',
fix: null
};
} else {
return {
status: 'fail',
message: 'Not a Git repository',
fix: 'Initialize Git: git init',
docs: 'https://git-scm.com/docs/git-init'
};
}
} catch (error) {
return {
status: 'fail',
message: 'Unable to check Git repository',
fix: 'Ensure you are in a valid directory',
docs: null
};
}
}
/**
* Check if .ripp directory exists
*/
function checkRippDirectory(cwd) {
const rippDir = path.join(cwd, '.ripp');
if (fs.existsSync(rippDir) && fs.statSync(rippDir).isDirectory()) {
return {
status: 'pass',
message: '.ripp directory exists',
fix: null
};
} else {
return {
status: 'fail',
message: '.ripp directory not found',
fix: 'Initialize RIPP: ripp init',
docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html'
};
}
}
/**
* Check if config.yaml exists
*/
function checkConfigFile(cwd) {
const configPath = path.join(cwd, '.ripp', 'config.yaml');
if (fs.existsSync(configPath)) {
return {
status: 'pass',
message: 'config.yaml present',
fix: null
};
} else {
return {
status: 'warning',
message: 'config.yaml not found (using defaults)',
fix: 'Initialize RIPP: ripp init',
docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html'
};
}
}
/**
* Check if evidence pack exists
*/
function checkEvidencePack(cwd) {
const evidenceIndex = path.join(cwd, '.ripp', 'evidence', 'index.yaml');
if (fs.existsSync(evidenceIndex)) {
return {
status: 'pass',
message: 'Evidence pack built',
fix: null
};
} else {
return {
status: 'warning',
message: 'Evidence pack not built',
fix: 'Build evidence: ripp evidence build',
docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html#step-2-build-evidence'
};
}
}
/**
* Check if candidates exist (discovery has run)
*/
function checkCandidates(cwd) {
const candidatesDir = path.join(cwd, '.ripp', 'candidates');
if (fs.existsSync(candidatesDir)) {
const files = fs.readdirSync(candidatesDir);
const yamlFiles = files.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
if (yamlFiles.length > 0) {
return {
status: 'pass',
message: `${yamlFiles.length} candidate(s) found`,
fix: null
};
} else {
return {
status: 'warning',
message: 'No candidate files in candidates directory',
fix: 'Run discovery: ripp discover (requires AI enabled)',
docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html#step-3-discover-intent'
};
}
} else {
return {
status: 'warning',
message: 'Discovery not run',
fix: 'Run discovery: ripp discover (requires AI enabled)',
docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html#step-3-discover-intent'
};
}
}
/**
* Check if confirmed intent exists
*/
function checkConfirmedIntent(cwd) {
const intentPath = path.join(cwd, '.ripp', 'intent.confirmed.yaml');
if (fs.existsSync(intentPath)) {
return {
status: 'pass',
message: 'Intent confirmed',
fix: null
};
} else {
return {
status: 'warning',
message: 'Intent not confirmed',
fix: 'Confirm intent: ripp confirm --checklist (then ripp build --from-checklist)',
docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html#step-4-confirm-intent'
};
}
}
/**
* Check if RIPP schema is accessible
*/
function checkSchema() {
try {
// First check bundled schema (always available when CLI is installed)
const bundledSchemaPath = path.join(__dirname, '../schema', 'ripp-1.0.schema.json');
if (fs.existsSync(bundledSchemaPath)) {
return {
status: 'pass',
message: 'RIPP schema accessible',
fix: null
};
}
// Fallback: check if we're in project root (for development)
const projectRoot = path.resolve(__dirname, '../../..');
const schemaPath = path.join(projectRoot, 'schema', 'ripp-1.0.schema.json');
if (fs.existsSync(schemaPath)) {
return {
status: 'pass',
message: 'RIPP schema accessible',
fix: null
};
} else {
return {
status: 'warning',
message: 'RIPP schema not found locally',
fix: 'Schema will be loaded from repository when needed',
docs: 'https://dylan-natter.github.io/ripp-protocol/schema/ripp-1.0.schema.json'
};
}
} catch (error) {
return {
status: 'warning',
message: 'Unable to check schema',
fix: null,
docs: null
};
}
}
/**
* Check CLI version
*/
function checkCliVersion() {
try {
const pkg = require('../package.json');
return {
status: 'pass',
message: `ripp-cli v${pkg.version}`,
fix: null
};
} catch (error) {
return {
status: 'warning',
message: 'Unable to determine CLI version',
fix: null,
docs: null
};
}
}
/**
* Format health check results as text
*/
function formatHealthCheckText(results) {
const { checks, summary } = results;
let output = '\n';
output += '🔍 RIPP Health Check\n';
output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n';
// Overall summary
if (summary.healthy) {
output += '✅ All checks passed!\n\n';
} else {
output += `📊 Summary: ${summary.passed}/${summary.total} checks passed`;
if (summary.warnings > 0) {
output += `, ${summary.warnings} warnings`;
}
if (summary.failed > 0) {
output += `, ${summary.failed} failed`;
}
output += '\n\n';
}
// Individual checks
for (const [name, check] of Object.entries(checks)) {
const icon = check.status === 'pass' ? '✓' : check.status === 'warning' ? '⚠' : '✗';
const statusColor = check.status === 'pass' ? '' : check.status === 'warning' ? '⚠ ' : '✗ ';
output += `${icon} ${formatCheckName(name)}: ${check.message}\n`;
if (check.fix) {
output += ` → Fix: ${check.fix}\n`;
}
if (check.docs) {
output += ` → Docs: ${check.docs}\n`;
}
output += '\n';
}
// Next steps
if (!summary.healthy) {
output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
output += '💡 Next Steps:\n\n';
const failedChecks = Object.entries(checks)
.filter(([_, check]) => check.status === 'fail')
.map(([_, check]) => check.fix)
.filter(fix => fix !== null);
if (failedChecks.length > 0) {
failedChecks.forEach((fix, idx) => {
output += ` ${idx + 1}. ${fix}\n`;
});
} else {
output += ' All critical checks passed. Address warnings to improve workflow.\n';
}
output += '\n';
}
output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
output += 'For more help: https://dylan-natter.github.io/ripp-protocol/getting-started.html\n';
return output;
}
/**
* Format check name for display
*/
function formatCheckName(name) {
const names = {
nodeVersion: 'Node.js Version',
gitRepository: 'Git Repository',
rippDirectory: 'RIPP Directory',
configFile: 'Configuration',
evidencePack: 'Evidence Pack',
candidates: 'Intent Candidates',
confirmedIntent: 'Confirmed Intent',
schema: 'RIPP Schema',
cliVersion: 'CLI Version'
};
return names[name] || name;
}
module.exports = {
runHealthChecks,
formatHealthCheckText
};
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
/**
* Gather metrics about the RIPP workflow in the current repository.
* Metrics are best-effort and never fabricated - if data is unavailable, it is marked as N/A.
*
* @param {string} rippDir - Path to .ripp directory (default: ./.ripp)
* @returns {object} Metrics object with evidence, discovery, validation, and workflow stats
*/
function gatherMetrics(rippDir = './.ripp') {
const metrics = {
timestamp: new Date().toISOString(),
evidence: gatherEvidenceMetrics(rippDir),
discovery: gatherDiscoveryMetrics(rippDir),
validation: gatherValidationMetrics(rippDir),
workflow: gatherWorkflowMetrics(rippDir)
};
return metrics;
}
/**
* Gather evidence pack metrics
*/
function gatherEvidenceMetrics(rippDir) {
const evidenceIndexPath = path.join(rippDir, 'evidence', 'evidence.index.json');
if (!fs.existsSync(evidenceIndexPath)) {
return {
status: 'not_built',
file_count: 0,
total_size: 0,
coverage_percent: 0
};
}
try {
const evidenceIndex = JSON.parse(fs.readFileSync(evidenceIndexPath, 'utf8'));
const fileCount = evidenceIndex.total_files || evidenceIndex.files?.length || 0;
const totalSize = evidenceIndex.total_size || 0;
// Calculate coverage: evidence files vs total git-tracked files
let gitFileCount = 0;
try {
const gitFiles = execSync('git ls-files --exclude-standard', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
cwd: path.dirname(rippDir)
});
gitFileCount = gitFiles
.trim()
.split('\n')
.filter(f => f.length > 0).length;
} catch (error) {
// Not a git repo or git command failed, coverage unknown
gitFileCount = fileCount; // Assume 100% if git unavailable
}
const coveragePercent = gitFileCount > 0 ? Math.round((fileCount / gitFileCount) * 100) : 0;
return {
status: 'built',
file_count: fileCount,
total_size: totalSize,
coverage_percent: coveragePercent,
last_build: evidenceIndex.timestamp || 'unknown'
};
} catch (error) {
return {
status: 'error',
file_count: 0,
total_size: 0,
coverage_percent: 0,
error: error.message
};
}
}
/**
* Gather discovery metrics (AI-generated candidates)
*/
function gatherDiscoveryMetrics(rippDir) {
const candidatesPath = path.join(rippDir, 'intent.candidates.yaml');
if (!fs.existsSync(candidatesPath)) {
return {
status: 'not_run',
candidate_count: 0
};
}
try {
const yaml = require('js-yaml');
const candidatesContent = fs.readFileSync(candidatesPath, 'utf8');
const candidates = yaml.load(candidatesContent);
const candidateCount = candidates.candidates?.length || 0;
// Calculate average confidence if available
let avgConfidence = null;
if (candidates.candidates && candidateCount > 0) {
const confidences = candidates.candidates
.map(c => c.confidence)
.filter(conf => typeof conf === 'number' && conf >= 0 && conf <= 1);
if (confidences.length > 0) {
avgConfidence = confidences.reduce((sum, c) => sum + c, 0) / confidences.length;
}
}
// Quality score (simple heuristic: avg confidence * candidate count normalization)
const qualityScore =
avgConfidence !== null
? Math.round(avgConfidence * Math.min(candidateCount / 3, 1) * 100)
: null;
return {
status: 'completed',
candidate_count: candidateCount,
avg_confidence: avgConfidence !== null ? Math.round(avgConfidence * 100) / 100 : null,
quality_score: qualityScore,
model: candidates.metadata?.model || 'unknown'
};
} catch (error) {
return {
status: 'error',
candidate_count: 0,
error: error.message
};
}
}
/**
* Gather validation metrics
*/
function gatherValidationMetrics(rippDir) {
// Look for canonical handoff packet
const handoffPath = path.join(rippDir, 'handoff.ripp.yaml');
if (!fs.existsSync(handoffPath)) {
return {
status: 'not_validated',
last_run: null
};
}
try {
// Get file mtime as proxy for last validation
const stats = fs.statSync(handoffPath);
const lastRun = stats.mtime.toISOString();
// Attempt basic validation check (packet must be parseable YAML with ripp_version)
const yaml = require('js-yaml');
const packetContent = fs.readFileSync(handoffPath, 'utf8');
const packet = yaml.load(packetContent);
const isValid = packet && packet.ripp_version === '1.0' && packet.packet_id && packet.level;
return {
status: isValid ? 'pass' : 'fail',
last_run: lastRun,
level: packet.level || null
};
} catch (error) {
return {
status: 'fail',
last_run: null,
error: error.message
};
}
}
/**
* Gather workflow completion metrics
*/
function gatherWorkflowMetrics(rippDir) {
// Define expected artifacts for each workflow step
const steps = {
initialized: fs.existsSync(path.join(rippDir, 'config.yaml')),
evidence_built: fs.existsSync(path.join(rippDir, 'evidence', 'evidence.index.json')),
discovery_run: fs.existsSync(path.join(rippDir, 'intent.candidates.yaml')),
checklist_generated: fs.existsSync(path.join(rippDir, 'intent.checklist.md')),
artifacts_built: fs.existsSync(path.join(rippDir, 'handoff.ripp.yaml'))
};
const completedSteps = Object.values(steps).filter(Boolean).length;
const totalSteps = Object.keys(steps).length;
const completionPercent = Math.round((completedSteps / totalSteps) * 100);
return {
completion_percent: completionPercent,
steps_completed: completedSteps,
steps_total: totalSteps,
steps: steps
};
}
/**
* Format metrics as human-readable text
*/
function formatMetricsText(metrics) {
const lines = [];
lines.push('RIPP Workflow Metrics');
lines.push('='.repeat(60));
lines.push('');
// Evidence metrics
lines.push('Evidence Pack:');
if (metrics.evidence.status === 'built') {
lines.push(` Status: ✓ Built`);
lines.push(` Files: ${metrics.evidence.file_count}`);
lines.push(` Size: ${formatBytes(metrics.evidence.total_size)}`);
lines.push(` Coverage: ${metrics.evidence.coverage_percent}% of git-tracked files`);
lines.push(` Last Build: ${formatTimestamp(metrics.evidence.last_build)}`);
} else if (metrics.evidence.status === 'not_built') {
lines.push(` Status: ✗ Not built`);
lines.push(` Next Step: Run 'ripp evidence build'`);
} else {
lines.push(` Status: ✗ Error`);
lines.push(` Error: ${metrics.evidence.error || 'Unknown'}`);
}
lines.push('');
// Discovery metrics
lines.push('Intent Discovery:');
if (metrics.discovery.status === 'completed') {
lines.push(` Status: ✓ Completed`);
lines.push(` Candidates: ${metrics.discovery.candidate_count}`);
if (metrics.discovery.avg_confidence !== null) {
lines.push(` Avg Confidence: ${(metrics.discovery.avg_confidence * 100).toFixed(0)}%`);
}
if (metrics.discovery.quality_score !== null) {
lines.push(` Quality Score: ${metrics.discovery.quality_score}/100`);
}
lines.push(` Model: ${metrics.discovery.model}`);
} else if (metrics.discovery.status === 'not_run') {
lines.push(` Status: ✗ Not run`);
lines.push(` Next Step: Run 'ripp discover'`);
} else {
lines.push(` Status: ✗ Error`);
lines.push(` Error: ${metrics.discovery.error || 'Unknown'}`);
}
lines.push('');
// Validation metrics
lines.push('Validation:');
if (metrics.validation.status === 'pass') {
lines.push(` Status: ✓ Pass`);
lines.push(` Level: ${metrics.validation.level}`);
lines.push(` Last Run: ${formatTimestamp(metrics.validation.last_run)}`);
} else if (metrics.validation.status === 'fail') {
lines.push(` Status: ✗ Fail`);
if (metrics.validation.error) {
lines.push(` Error: ${metrics.validation.error}`);
}
} else {
lines.push(` Status: - Not validated`);
lines.push(` Next Step: Run 'ripp build' to create handoff packet`);
}
lines.push('');
// Workflow completion
lines.push('Workflow Progress:');
lines.push(
` Completion: ${metrics.workflow.completion_percent}% (${metrics.workflow.steps_completed}/${metrics.workflow.steps_total} steps)`
);
lines.push(` Steps:`);
lines.push(
` ${metrics.workflow.steps.initialized ? '✓' : '✗'} Initialized (.ripp/config.yaml)`
);
lines.push(` ${metrics.workflow.steps.evidence_built ? '✓' : '✗'} Evidence Built`);
lines.push(` ${metrics.workflow.steps.discovery_run ? '✓' : '✗'} Discovery Run`);
lines.push(` ${metrics.workflow.steps.checklist_generated ? '✓' : '✗'} Checklist Generated`);
lines.push(` ${metrics.workflow.steps.artifacts_built ? '✓' : '✗'} Artifacts Built`);
lines.push('');
lines.push('='.repeat(60));
lines.push(`Generated: ${formatTimestamp(metrics.timestamp)}`);
return lines.join('\n');
}
/**
* Format bytes as human-readable size
*/
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Format ISO timestamp as human-readable
*/
function formatTimestamp(timestamp) {
if (!timestamp || timestamp === 'unknown') return 'Unknown';
try {
const date = new Date(timestamp);
return date.toLocaleString();
} catch (error) {
return timestamp;
}
}
/**
* Load metrics history from .ripp/metrics-history.json
*/
function loadMetricsHistory(rippDir) {
const historyPath = path.join(rippDir, 'metrics-history.json');
if (!fs.existsSync(historyPath)) {
return [];
}
try {
const historyContent = fs.readFileSync(historyPath, 'utf8');
return JSON.parse(historyContent);
} catch (error) {
console.error(`Warning: Could not load metrics history: ${error.message}`);
return [];
}
}
/**
* Save metrics to history
*/
function saveMetricsHistory(rippDir, metrics) {
const historyPath = path.join(rippDir, 'metrics-history.json');
try {
const history = loadMetricsHistory(rippDir);
// Add current metrics to history (keep last 50 entries)
history.push({
timestamp: metrics.timestamp,
evidence: metrics.evidence,
discovery: metrics.discovery,
validation: metrics.validation,
workflow: metrics.workflow
});
const trimmedHistory = history.slice(-50);
fs.writeFileSync(historyPath, JSON.stringify(trimmedHistory, null, 2), 'utf8');
} catch (error) {
console.error(`Warning: Could not save metrics history: ${error.message}`);
}
}
/**
* Format metrics history as text
*/
function formatMetricsHistory(history) {
if (history.length === 0) {
return 'No metrics history available. Run `ripp metrics --report` multiple times to build history.';
}
const lines = [];
lines.push('RIPP Metrics History');
lines.push('='.repeat(60));
lines.push('');
// Show trends for key metrics
const recent = history.slice(-10); // Last 10 entries
lines.push('Recent Trends (last 10 runs):');
lines.push('');
// Evidence coverage trend
lines.push('Evidence Coverage:');
recent.forEach((entry, idx) => {
const coverage = entry.evidence?.coverage_percent || 0;
const bar = '█'.repeat(Math.round(coverage / 5));
lines.push(` ${formatTimestamp(entry.timestamp).padEnd(25)} ${bar} ${coverage}%`);
});
lines.push('');
// Discovery quality trend
lines.push('Discovery Quality Score:');
recent.forEach((entry, idx) => {
const quality = entry.discovery?.quality_score || 0;
const bar = '█'.repeat(Math.round(quality / 5));
lines.push(` ${formatTimestamp(entry.timestamp).padEnd(25)} ${bar} ${quality}/100`);
});
lines.push('');
// Workflow completion trend
lines.push('Workflow Completion:');
recent.forEach((entry, idx) => {
const completion = entry.workflow?.completion_percent || 0;
const bar = '█'.repeat(Math.round(completion / 5));
lines.push(` ${formatTimestamp(entry.timestamp).padEnd(25)} ${bar} ${completion}%`);
});
lines.push('');
return lines.join('\n');
}
module.exports = {
gatherMetrics,
formatMetricsText,
loadMetricsHistory,
saveMetricsHistory,
formatMetricsHistory
};
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://dylan-natter.github.io/ripp-protocol/schema/evidence-pack.schema.json",
"title": "RIPP Evidence Pack Schema",
"description": "Schema for RIPP evidence pack index",
"type": "object",
"required": ["version", "created", "stats", "files"],
"properties": {
"version": {
"type": "string",
"const": "1.0",
"description": "Evidence pack schema version"
},
"created": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of evidence pack creation"
},
"stats": {
"type": "object",
"required": ["totalFiles", "totalSize", "includedFiles", "excludedFiles"],
"properties": {
"totalFiles": {
"type": "integer",
"minimum": 0,
"description": "Total number of files scanned"
},
"totalSize": {
"type": "integer",
"minimum": 0,
"description": "Total size of included files in bytes"
},
"includedFiles": {
"type": "integer",
"minimum": 0,
"description": "Number of files included in evidence"
},
"excludedFiles": {
"type": "integer",
"minimum": 0,
"description": "Number of files excluded from evidence"
}
}
},
"includePatterns": {
"type": "array",
"items": {
"type": "string"
},
"description": "Glob patterns used for inclusion"
},
"excludePatterns": {
"type": "array",
"items": {
"type": "string"
},
"description": "Glob patterns used for exclusion"
},
"files": {
"type": "array",
"items": {
"type": "object",
"required": ["path", "hash", "size"],
"properties": {
"path": {
"type": "string",
"description": "Relative path from repository root"
},
"hash": {
"type": "string",
"description": "SHA-256 hash of file content"
},
"size": {
"type": "integer",
"minimum": 0,
"description": "File size in bytes"
},
"type": {
"type": "string",
"enum": ["source", "config", "schema", "workflow", "other"],
"description": "Detected file type"
}
}
},
"description": "List of files included in evidence pack"
},
"evidence": {
"type": "object",
"properties": {
"dependencies": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["runtime", "dev", "peer"]
},
"source": {
"type": "string",
"description": "File path where dependency was found"
}
}
},
"description": "Extracted dependencies"
},
"routes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"method": {
"type": "string"
},
"path": {
"type": "string"
},
"source": {
"type": "string",
"description": "File and line where route was found"
},
"snippet": {
"type": "string",
"description": "Code snippet"
}
}
},
"description": "Detected API routes"
},
"schemas": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["model", "migration", "validation"]
},
"source": {
"type": "string"
},
"snippet": {
"type": "string"
}
}
},
"description": "Detected data schemas"
},
"auth": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["middleware", "guard", "config"]
},
"source": {
"type": "string"
},
"snippet": {
"type": "string"
}
}
},
"description": "Authentication/authorization signals"
},
"workflows": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"triggers": {
"type": "array",
"items": {
"type": "string"
}
},
"source": {
"type": "string"
}
}
},
"description": "CI/CD workflow configurations"
}
}
}
}
}
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://dylan-natter.github.io/ripp-protocol/schema/intent-candidates.schema.json",
"title": "RIPP Intent Candidates Schema",
"description": "Schema for AI-inferred candidate intent requiring human confirmation",
"type": "object",
"required": ["version", "created", "candidates"],
"properties": {
"version": {
"type": "string",
"const": "1.0",
"description": "Intent candidates schema version"
},
"created": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of generation"
},
"generatedBy": {
"type": "object",
"properties": {
"provider": {
"type": "string",
"description": "AI provider used"
},
"model": {
"type": "string",
"description": "Model identifier"
},
"evidencePackHash": {
"type": "string",
"description": "Hash of evidence pack used"
}
}
},
"candidates": {
"type": "array",
"items": {
"type": "object",
"required": ["source", "confidence", "evidence", "requires_human_confirmation"],
"properties": {
"section": {
"type": "string",
"enum": [
"purpose",
"ux_flow",
"data_contracts",
"api_contracts",
"permissions",
"failure_modes",
"audit_events",
"nfrs",
"acceptance_tests"
],
"description": "RIPP section this candidate belongs to"
},
"source": {
"type": "string",
"const": "inferred",
"description": "Must always be 'inferred' for candidates"
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "Confidence score (0.0 to 1.0)"
},
"evidence": {
"type": "array",
"items": {
"type": "object",
"required": ["file", "line"],
"properties": {
"file": {
"type": "string",
"description": "Source file path"
},
"line": {
"type": "integer",
"minimum": 1,
"description": "Line number (or start line)"
},
"endLine": {
"type": "integer",
"minimum": 1,
"description": "End line (for multi-line snippets)"
},
"snippet": {
"type": "string",
"description": "Relevant code snippet"
}
}
},
"minItems": 1,
"description": "Evidence references supporting this inference"
},
"requires_human_confirmation": {
"type": "boolean",
"const": true,
"description": "Must always be true for candidates"
},
"content": {
"description": "The inferred content (structure depends on section)"
}
}
}
}
}
}
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://dylan-natter.github.io/ripp-protocol/schema/intent-confirmed.schema.json",
"title": "RIPP Confirmed Intent Schema",
"description": "Schema for human-confirmed intent ready for compilation",
"type": "object",
"required": ["version", "confirmed"],
"properties": {
"version": {
"type": "string",
"const": "1.0",
"description": "Confirmed intent schema version"
},
"confirmed": {
"type": "array",
"items": {
"type": "object",
"required": ["source", "confirmed_at", "content"],
"properties": {
"section": {
"type": "string",
"enum": [
"purpose",
"ux_flow",
"data_contracts",
"api_contracts",
"permissions",
"failure_modes",
"audit_events",
"nfrs",
"acceptance_tests"
],
"description": "RIPP section this confirmed block belongs to"
},
"source": {
"type": "string",
"const": "confirmed",
"description": "Must be 'confirmed' after human review"
},
"confirmed_at": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of confirmation"
},
"confirmed_by": {
"type": "string",
"description": "Optional user identifier"
},
"original_confidence": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "Original AI confidence score (for traceability)"
},
"evidence": {
"type": "array",
"items": {
"type": "object",
"properties": {
"file": {
"type": "string"
},
"line": {
"type": "integer",
"minimum": 1
},
"endLine": {
"type": "integer",
"minimum": 1
},
"snippet": {
"type": "string"
}
}
},
"description": "Original evidence references (preserved for traceability)"
},
"content": {
"description": "The confirmed content (structure depends on section)"
}
}
}
}
}
}
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://dylan-natter.github.io/ripp-protocol/schema/ripp-1.0.schema.json",
"title": "RIPP v1.0 Packet Schema",
"description": "JSON Schema for Regenerative Intent Prompting Protocol v1.0 packets",
"type": "object",
"required": [
"ripp_version",
"packet_id",
"title",
"created",
"updated",
"status",
"level",
"purpose",
"ux_flow",
"data_contracts"
],
"properties": {
"ripp_version": {
"type": "string",
"const": "1.0",
"description": "RIPP specification version"
},
"packet_id": {
"type": "string",
"pattern": "^[a-z0-9-]+$",
"description": "Unique identifier for this packet (kebab-case)"
},
"title": {
"type": "string",
"minLength": 1,
"description": "Human-readable feature title"
},
"created": {
"type": "string",
"format": "date",
"description": "ISO 8601 date when packet was created"
},
"updated": {
"type": "string",
"format": "date",
"description": "ISO 8601 date of last update"
},
"status": {
"type": "string",
"enum": ["draft", "approved", "implemented", "deprecated"],
"description": "Lifecycle stage of the feature"
},
"level": {
"type": "integer",
"minimum": 1,
"maximum": 3,
"description": "RIPP conformance level"
},
"version": {
"type": "string",
"description": "Optional packet version (semver recommended)"
},
"purpose": {
"type": "object",
"required": ["problem", "solution", "value"],
"properties": {
"problem": {
"type": "string",
"minLength": 1,
"description": "Clear statement of the problem being solved"
},
"solution": {
"type": "string",
"minLength": 1,
"description": "High-level approach to solving it"
},
"value": {
"type": "string",
"minLength": 1,
"description": "Business or user value delivered"
},
"out_of_scope": {
"type": "string",
"description": "What this feature explicitly does NOT do"
},
"assumptions": {
"type": "array",
"items": {
"type": "string"
},
"description": "Known assumptions or constraints"
},
"references": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": { "type": "string" },
"url": { "type": "string", "format": "uri" }
},
"required": ["title", "url"]
},
"description": "Links to related docs, issues, or designs"
}
},
"additionalProperties": false
},
"ux_flow": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["step", "actor", "action"],
"properties": {
"step": {
"type": "integer",
"minimum": 1,
"description": "Numeric order"
},
"actor": {
"type": "string",
"minLength": 1,
"description": "Who or what performs the action"
},
"action": {
"type": "string",
"minLength": 1,
"description": "What happens in this step"
},
"trigger": {
"type": "string",
"description": "What initiates this step"
},
"result": {
"type": "string",
"description": "What the user sees or receives"
},
"condition": {
"type": "string",
"description": "Conditional logic for this step"
}
},
"anyOf": [
{ "required": ["trigger"] },
{ "required": ["result"] },
{ "required": ["condition"] }
],
"additionalProperties": false
},
"description": "User or system interaction flow"
},
"data_contracts": {
"type": "object",
"anyOf": [{ "required": ["inputs"] }, { "required": ["outputs"] }],
"properties": {
"inputs": {
"type": "array",
"items": {
"$ref": "#/definitions/entity"
},
"description": "Data structures consumed by the feature"
},
"outputs": {
"type": "array",
"items": {
"$ref": "#/definitions/entity"
},
"description": "Data structures produced by the feature"
}
},
"additionalProperties": false
},
"api_contracts": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["endpoint", "method", "purpose", "response"],
"properties": {
"endpoint": {
"type": "string",
"minLength": 1,
"description": "URL path or RPC method name"
},
"method": {
"type": "string",
"enum": ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
"description": "HTTP method or protocol operation"
},
"purpose": {
"type": "string",
"minLength": 1,
"description": "What this endpoint does"
},
"request": {
"type": "object",
"properties": {
"content_type": { "type": "string" },
"schema_ref": { "type": "string" },
"parameters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"in": { "type": "string", "enum": ["path", "query", "header", "cookie"] },
"required": { "type": "boolean" },
"type": { "type": "string" },
"description": { "type": "string" }
},
"required": ["name", "in", "required", "type"]
}
}
}
},
"response": {
"type": "object",
"required": ["success", "errors"],
"properties": {
"success": {
"type": "object",
"required": ["status"],
"properties": {
"status": { "type": "integer", "minimum": 200, "maximum": 299 },
"schema_ref": { "type": "string" },
"content_type": { "type": "string" }
}
},
"errors": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["status", "description"],
"properties": {
"status": { "type": "integer", "minimum": 400, "maximum": 599 },
"description": { "type": "string" }
}
}
}
}
}
},
"additionalProperties": false
},
"description": "API endpoints or service interfaces"
},
"permissions": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["action", "required_roles", "description"],
"properties": {
"action": {
"type": "string",
"minLength": 1,
"description": "The permission being checked"
},
"required_roles": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
},
"description": "Roles that can perform this action"
},
"resource_scope": {
"type": "string",
"description": "Scope of the resource"
},
"description": {
"type": "string",
"minLength": 1,
"description": "When and why this permission is checked"
}
},
"additionalProperties": false
},
"description": "Permission requirements for the feature"
},
"failure_modes": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["scenario", "impact", "handling", "user_message"],
"properties": {
"scenario": {
"type": "string",
"minLength": 1,
"description": "What goes wrong"
},
"impact": {
"type": "string",
"minLength": 1,
"description": "Effect on users or system"
},
"handling": {
"type": "string",
"minLength": 1,
"description": "How the system responds"
},
"user_message": {
"type": "string",
"minLength": 1,
"description": "What users see or are told"
}
},
"additionalProperties": false
},
"description": "What can go wrong and how to handle it"
},
"audit_events": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["event", "severity", "includes", "purpose"],
"properties": {
"event": {
"type": "string",
"minLength": 1,
"description": "Event name"
},
"severity": {
"type": "string",
"enum": ["debug", "info", "warn", "error", "critical"],
"description": "Log level"
},
"includes": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
},
"description": "Data fields captured in the event"
},
"retention": {
"type": "string",
"description": "How long to keep this event"
},
"purpose": {
"type": "string",
"minLength": 1,
"description": "Why this event is logged"
}
},
"additionalProperties": false
},
"description": "Events that must be logged"
},
"nfrs": {
"type": "object",
"minProperties": 1,
"properties": {
"performance": {
"type": "object",
"properties": {
"response_time_p50": { "type": "string" },
"response_time_p95": { "type": "string" },
"response_time_p99": { "type": "string" },
"throughput": { "type": "string" }
}
},
"scalability": {
"type": "object",
"properties": {
"max_concurrent_users": { "type": "integer" },
"data_growth": { "type": "string" },
"horizontal_scaling": { "type": "boolean" }
}
},
"availability": {
"type": "object",
"properties": {
"uptime_target": { "type": "string" },
"rpo": { "type": "string" },
"rto": { "type": "string" }
}
},
"security": {
"type": "object",
"properties": {
"encryption_at_rest": { "type": "boolean" },
"encryption_in_transit": { "type": "boolean" },
"authentication_required": { "type": "boolean" },
"authorization_model": { "type": "string" }
}
},
"compliance": {
"type": "object",
"properties": {
"standards": {
"type": "array",
"items": { "type": "string" }
},
"data_residency": { "type": "string" },
"audit_requirements": { "type": "string" }
}
}
},
"additionalProperties": true,
"description": "Non-functional requirements"
},
"acceptance_tests": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["test_id", "title", "given", "when", "then", "verification"],
"properties": {
"test_id": {
"type": "string",
"minLength": 1,
"description": "Unique test identifier"
},
"title": {
"type": "string",
"minLength": 1,
"description": "What is being tested"
},
"given": {
"type": "string",
"minLength": 1,
"description": "Preconditions"
},
"when": {
"type": "string",
"minLength": 1,
"description": "Action taken"
},
"then": {
"type": "string",
"minLength": 1,
"description": "Expected outcome"
},
"verification": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
},
"description": "Specific checks to perform"
}
},
"additionalProperties": false
},
"description": "How to verify the feature works correctly"
}
},
"allOf": [
{
"if": {
"properties": { "level": { "const": 2 } }
},
"then": {
"required": ["api_contracts", "permissions", "failure_modes"]
}
},
{
"if": {
"properties": { "level": { "const": 3 } }
},
"then": {
"required": [
"api_contracts",
"permissions",
"failure_modes",
"audit_events",
"nfrs",
"acceptance_tests"
]
}
}
],
"additionalProperties": true,
"definitions": {
"entity": {
"type": "object",
"required": ["name", "fields"],
"properties": {
"name": {
"type": "string",
"minLength": 1,
"description": "Entity name"
},
"description": {
"type": "string",
"description": "What this entity represents"
},
"fields": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["name", "type", "required", "description"],
"properties": {
"name": {
"type": "string",
"minLength": 1,
"description": "Field name"
},
"type": {
"type": "string",
"enum": ["string", "number", "integer", "boolean", "object", "array", "null"],
"description": "Field data type"
},
"required": {
"type": "boolean",
"description": "Whether this field is required"
},
"description": {
"type": "string",
"minLength": 1,
"description": "What this field represents"
},
"format": {
"type": "string",
"description": "Format hint (e.g., email, uuid, date-time)"
},
"min": {
"type": "number",
"description": "Minimum value or length"
},
"max": {
"type": "number",
"description": "Maximum value or length"
},
"pattern": {
"type": "string",
"description": "Regex pattern for validation"
},
"enum": {
"type": "array",
"description": "Allowed values"
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}
}
}
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://dylan-natter.github.io/ripp-protocol/schema/ripp-config.schema.json",
"title": "RIPP Configuration Schema",
"description": "Configuration schema for RIPP vNext Intent Discovery Mode",
"type": "object",
"required": ["rippVersion"],
"properties": {
"rippVersion": {
"type": "string",
"pattern": "^\\d+\\.\\d+$",
"description": "RIPP specification version (e.g., '1.0')"
},
"ai": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": false,
"description": "Master switch for AI features (disabled by default)"
},
"provider": {
"type": "string",
"enum": ["openai", "azure-openai", "ollama", "custom"],
"default": "openai",
"description": "AI provider to use for intent inference"
},
"model": {
"type": "string",
"description": "Model identifier (e.g., 'gpt-4', 'gpt-3.5-turbo')"
},
"maxRetries": {
"type": "integer",
"minimum": 1,
"maximum": 10,
"default": 3,
"description": "Maximum retries for AI inference with schema validation"
},
"timeout": {
"type": "integer",
"minimum": 1000,
"description": "Request timeout in milliseconds"
},
"customEndpoint": {
"type": "string",
"format": "uri",
"description": "Custom API endpoint (for custom provider)"
}
},
"required": ["enabled"]
},
"evidencePack": {
"type": "object",
"properties": {
"includeGlobs": {
"type": "array",
"items": {
"type": "string"
},
"default": ["src/**", "app/**", "api/**", "db/**", ".github/workflows/**"],
"description": "Glob patterns for files to include in evidence"
},
"excludeGlobs": {
"type": "array",
"items": {
"type": "string"
},
"default": ["**/node_modules/**", "**/dist/**", "**/build/**", "**/*.lock", "**/.git/**"],
"description": "Glob patterns for files to exclude from evidence"
},
"maxFileSize": {
"type": "integer",
"minimum": 1024,
"default": 1048576,
"description": "Maximum file size in bytes (default: 1MB)"
},
"secretPatterns": {
"type": "array",
"items": {
"type": "string"
},
"description": "Custom regex patterns for secret detection"
}
}
},
"discovery": {
"type": "object",
"properties": {
"minConfidence": {
"type": "number",
"minimum": 0,
"maximum": 1,
"default": 0.5,
"description": "Minimum confidence threshold for AI-inferred intent"
},
"includeEvidence": {
"type": "boolean",
"default": true,
"description": "Include evidence references in candidate intent"
}
}
}
}
}
+35
-0

@@ -8,2 +8,37 @@ # Changelog

## [1.2.1] (2025-12-24)
### Bug Fixes
* **cli:** bundle schema files in npm package to fix validation when installed globally ([ed94409](https://github.com/Dylan-Natter/ripp-protocol/commit/ed94409))
- Schema files are now included in the npm package under `schema/` directory
- Updated schema loading paths to use bundled schemas instead of parent directory
- Fixes post-publish smoke test failures from version 1.2.0
* **cli:** fix checklist generation and parsing bugs in `ripp confirm` command
- Fixed extraction of content fields (purpose, ux_flow, data_contracts, etc.) from candidates
- Use 'purpose' or 'full-packet' as section name instead of 'unknown'
- Add 'full-packet' to valid section types in checklist parser
- Fixes empty YAML blocks in generated checklists
- Fixes 'Unknown section type' error when building from checklist
## [1.2.0](https://github.com/Dylan-Natter/ripp-protocol/compare/ripp-cli-v1.1.0...ripp-cli-v1.2.0) (2025-12-23)
### Features
* **cli:** enhance CLI description to include tooling capabilities ([8f97965](https://github.com/Dylan-Natter/ripp-protocol/commit/8f97965379bbb24287b8d69bb9d4e5af16bca1df))
## [1.1.0](https://github.com/Dylan-Natter/ripp-protocol/compare/ripp-cli-v1.0.1...ripp-cli-v1.1.0) (2025-12-23)
### Features
* **cli:** add metrics, doctor, and enhanced workflow commands ([b5c413d](https://github.com/Dylan-Natter/ripp-protocol/commit/b5c413d335088350527e6e9aa8cd6fa1f0debf9f))
* **vscode:** add metrics command and enhanced workflow integration ([24a3cd0](https://github.com/Dylan-Natter/ripp-protocol/commit/24a3cd02f3657958321cc8f04ca55f487853205c))
### Documentation
* upgrade reference implementation to Level 2 ([a4b18e3](https://github.com/Dylan-Natter/ripp-protocol/commit/a4b18e320d95fbb2754d4eb07dafc8da00eef673))
## [1.0.1] - 2025-12-22

@@ -10,0 +45,0 @@

+138
-8

@@ -20,2 +20,10 @@ #!/usr/bin/env node

const { migrateDirectoryStructure } = require('./lib/migrate');
const {
gatherMetrics,
formatMetricsText,
loadMetricsHistory,
saveMetricsHistory,
formatMetricsHistory
} = require('./lib/metrics');
const { runHealthChecks, formatHealthCheckText } = require('./lib/doctor');

@@ -42,3 +50,3 @@ // ANSI color codes

function loadSchema() {
const schemaPath = path.join(__dirname, '../../schema/ripp-1.0.schema.json');
const schemaPath = path.join(__dirname, 'schema/ripp-1.0.schema.json');
try {

@@ -282,2 +290,4 @@ return JSON.parse(fs.readFileSync(schemaPath, 'utf8'));

ripp build Build canonical RIPP artifacts from confirmed intent
ripp metrics Display workflow analytics and health metrics
ripp doctor Run health checks and diagnostics

@@ -306,6 +316,12 @@ ripp --help Show this help message

${colors.green}Build Options:${colors.reset}
--from-checklist Build from intent.checklist.md (after manual review)
--packet-id <id> Packet ID for generated RIPP (default: discovered-intent)
--title <title> Title for generated RIPP packet
--output-name <file> Output file name (default: handoff.ripp.yaml)
--user <id> User identifier for confirmation tracking
${colors.green}Metrics Options:${colors.reset}
--report Write metrics to .ripp/metrics.json
--history Show metrics trends from previous runs
${colors.green}Validate Options:${colors.reset}

@@ -324,2 +340,3 @@ --min-level <1|2|3> Enforce minimum RIPP level

--format <json|yaml|md> Output format (auto-detected from extension)
--single Generate consolidated single-file markdown
--package-version <version> Version string for the package (e.g., 1.0.0)

@@ -347,2 +364,3 @@ --force Overwrite existing output file without versioning

ripp package --in feature.ripp.yaml --out handoff.md
ripp package --in feature.ripp.yaml --out handoff.md --single
ripp package --in feature.ripp.yaml --out handoff.md --package-version 1.0.0

@@ -360,3 +378,6 @@ ripp package --in feature.ripp.yaml --out handoff.md --force

ripp confirm --interactive
ripp build --packet-id my-feature --title "My Feature"
ripp confirm --checklist
ripp build
ripp build --from-checklist
ripp build --from-checklist --packet-id my-feature --title "My Feature"

@@ -440,3 +461,3 @@ ${colors.gray}Note: Legacy paths (features/, handoffs/, packages/) are supported for backward compatibility.${colors.reset}

};
} catch (error) {
} catch {
// Not in a git repo or git not available

@@ -576,2 +597,6 @@ return null;

await handleBuildCommand(args);
} else if (command === 'metrics') {
await handleMetricsCommand(args);
} else if (command === 'doctor') {
handleDoctorCommand(args);
} else {

@@ -872,3 +897,4 @@ console.error(`${colors.red}Error: Unknown command '${command}'${colors.reset}`);

skipValidation: args.includes('--skip-validation'),
warnOnInvalid: args.includes('--warn-on-invalid')
warnOnInvalid: args.includes('--warn-on-invalid'),
single: args.includes('--single')
};

@@ -1024,3 +1050,3 @@

} else if (options.format === 'md') {
output = formatAsMarkdown(packaged);
output = formatAsMarkdown(packaged, { single: options.single });
}

@@ -1286,3 +1312,3 @@

console.log(' 3. Save the file');
console.log(' 4. Run "ripp build" to compile confirmed intent');
console.log(' 4. Run "ripp build --from-checklist" to compile confirmed intent');
console.log('');

@@ -1317,3 +1343,4 @@ } else {

title: null,
outputName: null
outputName: null,
fromChecklist: args.includes('--from-checklist')
};

@@ -1336,7 +1363,28 @@

console.log(`${colors.blue}Building canonical RIPP artifacts...${colors.reset}\n`);
const userIndex = args.indexOf('--user');
if (userIndex !== -1 && args[userIndex + 1]) {
options.user = args[userIndex + 1];
}
if (options.fromChecklist) {
console.log(`${colors.blue}Building from checklist...${colors.reset}\n`);
} else {
console.log(`${colors.blue}Building canonical RIPP artifacts...${colors.reset}\n`);
}
try {
const result = buildCanonicalArtifacts(cwd, options);
// Display summary of checklist processing if applicable
if (options.fromChecklist && options._validationResults) {
const vr = options._validationResults;
console.log(`${colors.blue}Checklist Summary:${colors.reset}`);
console.log(` ${colors.gray}Total checked: ${vr.totalChecked}${colors.reset}`);
console.log(` ${colors.green}✓ Accepted: ${vr.accepted}${colors.reset}`);
if (vr.rejected > 0) {
console.log(` ${colors.yellow}⚠ Rejected: ${vr.rejected}${colors.reset}`);
}
console.log('');
}
log(colors.green, '✓', 'Build complete');

@@ -1357,2 +1405,11 @@ console.log(` ${colors.gray}RIPP Packet: ${result.packetPath}${colors.reset}`);

console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
if (options.fromChecklist) {
console.log('');
console.log(`${colors.blue}Troubleshooting:${colors.reset}`);
console.log(' 1. Verify checklist file exists: .ripp/intent.checklist.md');
console.log(' 2. Ensure at least one candidate is marked with [x]');
console.log(' 3. Check YAML blocks for syntax errors');
console.log(' 4. Run "ripp confirm --checklist" to regenerate checklist');
}
console.log('');
process.exit(1);

@@ -1362,2 +1419,75 @@ }

async function handleMetricsCommand(args) {
const cwd = process.cwd();
const rippDir = path.join(cwd, '.ripp');
// Parse options
const options = {
report: args.includes('--report'),
history: args.includes('--history')
};
// Check if .ripp directory exists
if (!fs.existsSync(rippDir)) {
console.error(`${colors.red}Error: RIPP directory not found${colors.reset}`);
console.error('Run "ripp init" to initialize RIPP in this repository');
process.exit(1);
}
try {
if (options.history) {
// Show metrics history
const history = loadMetricsHistory(rippDir);
console.log(formatMetricsHistory(history));
process.exit(0);
}
// Gather current metrics
const metrics = gatherMetrics(rippDir);
// Display metrics
console.log(formatMetricsText(metrics));
// Write report if requested
if (options.report) {
const reportPath = path.join(rippDir, 'metrics.json');
fs.writeFileSync(reportPath, JSON.stringify(metrics, null, 2), 'utf8');
// Save to history
saveMetricsHistory(rippDir, metrics);
console.log('');
log(colors.green, '✓', `Metrics report saved to ${reportPath}`);
console.log('');
}
process.exit(0);
} catch (error) {
console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
process.exit(1);
}
}
function handleDoctorCommand() {
const cwd = process.cwd();
console.log(`${colors.blue}Running RIPP health checks...${colors.reset}`);
console.log('');
try {
const results = runHealthChecks(cwd);
console.log(formatHealthCheckText(results));
// Exit with non-zero if there are critical failures
const hasCriticalFailures = Object.values(results.checks).some(
check => check.status === 'fail'
);
process.exit(hasCriticalFailures ? 1 : 0);
} catch (error) {
console.error(`${colors.red}Error running health checks: ${error.message}${colors.reset}`);
process.exit(1);
}
}
main().catch(error => {

@@ -1364,0 +1494,0 @@ console.error(`${colors.red}Fatal error: ${error.message}${colors.reset}`);

const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const {
parseChecklist,
buildConfirmedIntent,
validateConfirmedBlocks
} = require('./checklist-parser');

@@ -14,13 +19,22 @@ /**

function buildCanonicalArtifacts(cwd, options = {}) {
const confirmedPath = path.join(cwd, '.ripp', 'intent.confirmed.yaml');
let confirmed;
if (!fs.existsSync(confirmedPath)) {
throw new Error('No confirmed intent found. Run "ripp confirm" first.');
}
// Check if building from checklist
if (options.fromChecklist) {
confirmed = buildFromChecklist(cwd, options);
} else {
const confirmedPath = path.join(cwd, '.ripp', 'intent.confirmed.yaml');
const confirmedContent = fs.readFileSync(confirmedPath, 'utf8');
const confirmed = yaml.load(confirmedContent);
if (!fs.existsSync(confirmedPath)) {
throw new Error(
'No confirmed intent found. Run "ripp confirm" first, or use "ripp build --from-checklist" to build from the checklist.'
);
}
if (!confirmed.confirmed || confirmed.confirmed.length === 0) {
throw new Error('No confirmed intent blocks found');
const confirmedContent = fs.readFileSync(confirmedPath, 'utf8');
confirmed = yaml.load(confirmedContent);
if (!confirmed.confirmed || confirmed.confirmed.length === 0) {
throw new Error('No confirmed intent blocks found');
}
}

@@ -55,2 +69,93 @@

/**
* Build from checklist markdown file
* Parses checklist, validates checked items, and generates confirmed intent
*/
function buildFromChecklist(cwd, options = {}) {
const checklistPath = path.join(cwd, '.ripp', 'intent.checklist.md');
// Check if checklist exists
if (!fs.existsSync(checklistPath)) {
throw new Error(
`Checklist not found at ${checklistPath}. Run "ripp confirm --checklist" to generate it.`
);
}
// Read and parse checklist
const checklistContent = fs.readFileSync(checklistPath, 'utf8');
const parseResult = parseChecklist(checklistContent);
// Check for parsing errors
if (parseResult.errors.length > 0) {
const errorMsg = [
'Failed to parse checklist:',
...parseResult.errors.map(e => ` - ${e}`)
].join('\n');
throw new Error(errorMsg);
}
// Check if any candidates were checked
if (parseResult.candidates.length === 0) {
throw new Error(
'No candidates selected in checklist. Mark candidates with [x] and save the file, then run this command again.'
);
}
// Display warnings if any
if (parseResult.warnings.length > 0 && options.showWarnings !== false) {
console.warn('\n⚠️ Warnings:');
parseResult.warnings.forEach(w => console.warn(` - ${w}`));
console.warn('');
}
// Build confirmed intent structure
const confirmed = buildConfirmedIntent(parseResult.candidates, {
user: options.user || 'checklist',
timestamp: new Date().toISOString()
});
// Validate confirmed blocks
const validation = validateConfirmedBlocks(confirmed.confirmed);
// Store validation results for reporting
const validationResults = {
totalChecked: parseResult.candidates.length,
accepted: validation.accepted.length,
rejected: validation.rejected.length,
reasons: validation.reasons
};
// If there are rejected blocks, report them
if (validation.rejected.length > 0) {
console.warn('\n⚠️ Some candidates were rejected:');
validation.rejected.forEach(block => {
const reasons = validation.reasons[block.section] || [];
console.warn(` - ${block.section}: ${reasons.join(', ')}`);
});
console.warn('');
}
// Check if we have any accepted blocks
if (validation.accepted.length === 0) {
throw new Error(
'No valid candidates found. All selected candidates failed validation. Please review and fix the checklist.'
);
}
// Use only accepted blocks
const finalConfirmed = {
version: confirmed.version,
confirmed: validation.accepted
};
// Save confirmed intent for traceability
const confirmedPath = path.join(cwd, '.ripp', 'intent.confirmed.yaml');
fs.writeFileSync(confirmedPath, yaml.dump(finalConfirmed, { indent: 2 }), 'utf8');
// Store validation results on options for reporting in handleBuildCommand
options._validationResults = validationResults;
return finalConfirmed;
}
/**
* Build RIPP packet from confirmed intent blocks

@@ -339,3 +444,4 @@ */

buildRippPacket,
generateHandoffMarkdown
generateHandoffMarkdown,
buildFromChecklist
};
+2
-3

@@ -61,5 +61,4 @@ const fs = require('fs');

// Validate against schema
// Resolve schema path from project root (3 levels up from lib/)
const projectRoot = path.join(__dirname, '../../..');
const schemaPath = path.join(projectRoot, 'schema/ripp-config.schema.json');
// Resolve schema path from bundled schema directory
const schemaPath = path.join(__dirname, '../schema/ripp-config.schema.json');

@@ -66,0 +65,0 @@ if (!fs.existsSync(schemaPath)) {

@@ -56,7 +56,30 @@ const fs = require('fs');

console.log(`\n--- Candidate ${i + 1}/${candidates.candidates.length} ---`);
console.log(`Section: ${candidate.section || 'unknown'}`);
const sectionName = candidate.purpose?.problem ? 'purpose' : 'full-packet';
console.log(`Section: ${sectionName}`);
console.log(`Confidence: ${(candidate.confidence * 100).toFixed(1)}%`);
console.log(`Evidence: ${candidate.evidence.length} reference(s)`);
console.log('\nContent:');
console.log(yaml.dump(candidate.content, { indent: 2 }));
// Build content object from candidate fields
const content = {};
const contentFields = [
'purpose',
'ux_flow',
'data_contracts',
'api_contracts',
'permissions',
'failure_modes',
'audit_events',
'nfrs',
'acceptance_tests',
'design_philosophy',
'design_decisions',
'constraints',
'success_criteria'
];
contentFields.forEach(field => {
if (candidate[field]) {
content[field] = candidate[field];
}
});
console.log(yaml.dump(content, { indent: 2 }));

@@ -66,4 +89,27 @@ const answer = await question(rl, '\nAccept this candidate? (y/n/e to edit/s to skip): ');

if (answer.toLowerCase() === 'y') {
// Build content object from candidate fields
const content = {};
const contentFields = [
'purpose',
'ux_flow',
'data_contracts',
'api_contracts',
'permissions',
'failure_modes',
'audit_events',
'nfrs',
'acceptance_tests',
'design_philosophy',
'design_decisions',
'constraints',
'success_criteria'
];
contentFields.forEach(field => {
if (candidate[field]) {
content[field] = candidate[field];
}
});
confirmed.push({
section: candidate.section,
section: candidate.purpose?.problem ? 'purpose' : 'full-packet',
source: 'confirmed',

@@ -74,3 +120,3 @@ confirmed_at: new Date().toISOString(),

evidence: candidate.evidence,
content: candidate.content
content: content
});

@@ -142,3 +188,6 @@ console.log('✓ Accepted');

candidates.candidates.forEach((candidate, index) => {
markdown += `## Candidate ${index + 1}: ${candidate.section || 'unknown'}\n\n`;
// Extract section name from purpose or use generic identifier
const sectionName = candidate.purpose?.problem ? 'purpose' : 'full-packet';
markdown += `## Candidate ${index + 1}: ${sectionName}\n\n`;
markdown += `- **Confidence**: ${(candidate.confidence * 100).toFixed(1)}%\n`;

@@ -152,3 +201,25 @@ markdown += `- **Evidence**: ${candidate.evidence.length} reference(s)\n\n`;

markdown += '```yaml\n';
markdown += yaml.dump(candidate.content, { indent: 2 });
// Build content object from candidate fields (purpose, ux_flow, data_contracts, etc.)
const content = {};
const contentFields = [
'purpose',
'ux_flow',
'data_contracts',
'api_contracts',
'permissions',
'failure_modes',
'audit_events',
'nfrs',
'acceptance_tests',
'design_philosophy',
'design_decisions',
'constraints',
'success_criteria'
];
contentFields.forEach(field => {
if (candidate[field]) {
content[field] = candidate[field];
}
});
markdown += yaml.dump(content, { indent: 2 });
markdown += '```\n\n';

@@ -155,0 +226,0 @@

@@ -153,4 +153,8 @@ /**

* Format packaged packet as Markdown
* @param {Object} packaged - Packaged RIPP packet
* @param {Object} options - Formatting options
* @param {boolean} options.single - Generate consolidated single-file format
*/
function formatAsMarkdown(packaged, options = {}) {
const isSingle = options.single || false;
let md = '';

@@ -160,19 +164,27 @@

md += `# ${packaged.title}\n\n`;
md += `**Packet ID**: \`${packaged.packet_id}\` \n`;
md += `**Level**: ${packaged.level} \n`;
md += `**Status**: ${packaged.status} \n`;
md += `**Created**: ${packaged.created} \n`;
md += `**Updated**: ${packaged.updated} \n`;
if (packaged.version) {
md += `**Version**: ${packaged.version} \n`;
}
if (isSingle) {
// Consolidated single-file format (more concise, optimized for AI consumption)
md += `> **RIPP Handoff Document**\n`;
md += `> Packet ID: \`${packaged.packet_id}\` | Level: ${packaged.level} | Status: ${packaged.status}\n\n`;
} else {
// Standard format with full metadata
md += `**Packet ID**: \`${packaged.packet_id}\` \n`;
md += `**Level**: ${packaged.level} \n`;
md += `**Status**: ${packaged.status} \n`;
md += `**Created**: ${packaged.created} \n`;
md += `**Updated**: ${packaged.updated} \n`;
md += '\n---\n\n';
if (packaged.version) {
md += `**Version**: ${packaged.version} \n`;
}
// Packaging metadata
md += '## Packaging Information\n\n';
md += `This document was packaged by \`${packaged._meta.packaged_by}\` on ${packaged._meta.packaged_at}.\n\n`;
md += '---\n\n';
md += '\n---\n\n';
// Packaging metadata (omit in single-file mode for brevity)
md += '## Packaging Information\n\n';
md += `This document was packaged by \`${packaged._meta.packaged_by}\` on ${packaged._meta.packaged_at}.\n\n`;
md += '---\n\n';
}
// Purpose

@@ -179,0 +191,0 @@ md += '## Purpose\n\n';

{
"name": "ripp-cli",
"version": "1.0.1",
"description": "Official CLI validator for Regenerative Intent Prompting Protocol (RIPP)",
"version": "1.2.1",
"description": "Official CLI validator and tooling for Regenerative Intent Prompting Protocol (RIPP)",
"main": "index.js",

@@ -9,4 +9,11 @@ "bin": {

},
"files": [
"index.js",
"lib/",
"schema/",
"README.md",
"CHANGELOG.md"
],
"scripts": {
"test": "echo \"Warning: No tests specified\" && exit 0"
"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"
},

@@ -13,0 +20,0 @@ "keywords": [