| 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 | ||
| }; |
+370
| /** | ||
| * 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 | ||
| }; |
+410
| 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}`); |
+115
-9
| 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)) { |
+77
-6
@@ -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 @@ |
+25
-13
@@ -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'; |
+10
-3
| { | ||
| "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": [ |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 9 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 9 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
231495
44.78%23
53.33%6463
51.96%0
-100%35
6.06%6
50%