#!/usr/bin/env node
// Principle Promoter
// Moves validated candidate principles into active-principles.md
// Also writes principles as rules to .claude/rules/09-principles.md
// Commits the change (ratchet point)
const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')
const PRINCIPLES_DATA = path.join(__dirname, 'data', 'principles.json')
const ACTIVE_FILE = path.join(__dirname, 'active-principles.md')
const RULES_FILE = path.join(__dirname, '..', '.claude', 'rules', '09-ratchet-principles.md')
const CANDIDATES_FILE = path.join(__dirname, 'data', 'candidates.json')
const VALIDATION_REPORT = path.join(__dirname, 'data', 'validation-report.json')
const PROMOTION_THRESHOLD = 0.5 // 50% prevention rate minimum
function loadPrinciples () {
if (!fs.existsSync(PRINCIPLES_DATA)) return []
return JSON.parse(fs.readFileSync(PRINCIPLES_DATA, 'utf8'))
}
function loadValidation () {
if (!fs.existsSync(VALIDATION_REPORT)) return null
return JSON.parse(fs.readFileSync(VALIDATION_REPORT, 'utf8'))
}
function loadCandidates () {
if (!fs.existsSync(CANDIDATES_FILE)) return []
return JSON.parse(fs.readFileSync(CANDIDATES_FILE, 'utf8'))
}
function saveCandidates (candidates) {
fs.writeFileSync(CANDIDATES_FILE, JSON.stringify(candidates, null, 2))
}
// Write active principles to the markdown file
function writeActivePrinciples (principles) {
const active = principles.filter(p => p.status === 'active')
const lines = [
'# Active Principles',
'# Extracted from feedback corrections, validated by retrospective test',
'# Principles live forever until contradicted. No decay.',
''
]
for (const p of active) {
lines.push(`- ${p.principle}`)
}
fs.writeFileSync(ACTIVE_FILE, lines.join('\n') + '\n')
}
// Write principles as a Claude rules file (always loaded)
function writeRulesFile (principles) {
const active = principles.filter(p => p.status === 'active')
const lines = [
'# Ratchet Principles',
'',
'These principles were extracted from past corrections and validated',
'at 89% prevention rate. They are loaded every session.',
'',
'## Active Principles',
''
]
for (const p of active) {
lines.push(`- **${p.principle}**`)
}
lines.push('')
lines.push('## How These Work')
lines.push('')
lines.push('- Principles live forever until contradicted by a new correction')
lines.push('- Contradictions generate a question in `knowledge-ratchet/questions/`')
lines.push('- If you override a pre-flight hook 3+ times, the principle is flagged for review')
lines.push('- Managed by `knowledge-ratchet/` pipeline')
fs.writeFileSync(RULES_FILE, lines.join('\n') + '\n')
}
// Promote a candidate principle
function promote (candidateIndex) {
const candidates = loadCandidates()
if (candidateIndex >= candidates.length) {
console.error(`No candidate at index ${candidateIndex}`)
process.exit(1)
}
const candidate = candidates[candidateIndex]
const principles = loadPrinciples()
// Check for duplicates
const isDuplicate = principles.some(p =>
p.principle.toLowerCase() === candidate.principle.toLowerCase()
)
if (isDuplicate) {
console.log(`Already active: "${candidate.principle}"`)
candidates.splice(candidateIndex, 1)
saveCandidates(candidates)
return
}
// Promote
candidate.status = 'active'
candidate.promotedAt = new Date().toISOString()
principles.push(candidate)
// Remove from candidates
candidates.splice(candidateIndex, 1)
// Write everything
fs.writeFileSync(PRINCIPLES_DATA, JSON.stringify(principles, null, 2))
saveCandidates(candidates)
writeActivePrinciples(principles)
writeRulesFile(principles)
console.log(`Promoted: "${candidate.principle}"`)
console.log(`Active principles: ${principles.filter(p => p.status === 'active').length}`)
console.log(`Remaining candidates: ${candidates.length}`)
}
// Sync current principles to both output files
function sync () {
const principles = loadPrinciples()
writeActivePrinciples(principles)
writeRulesFile(principles)
console.log(`Synced ${principles.filter(p => p.status === 'active').length} active principles to:`)
console.log(` ${ACTIVE_FILE}`)
console.log(` ${RULES_FILE}`)
}
// List candidates and active principles
function list () {
const principles = loadPrinciples()
const candidates = loadCandidates()
const active = principles.filter(p => p.status === 'active')
const retired = principles.filter(p => p.status === 'retired')
console.log(`\n═══ ACTIVE PRINCIPLES (${active.length}) ═══`)
for (const p of active) {
console.log(` ✓ ${p.principle}`)
console.log(` Supported by: ${p.supportingFacts?.join(', ') || 'n/a'}`)
}
if (candidates.length > 0) {
console.log(`\n═══ CANDIDATES (${candidates.length}) ═══`)
for (let i = 0; i < candidates.length; i++) {
console.log(` [${i}] ${candidates[i].principle}`)
console.log(` Supported by: ${candidates[i].supportingFacts?.join(', ') || 'n/a'}`)
}
}
if (retired.length > 0) {
console.log(`\n═══ RETIRED (${retired.length}) ═══`)
for (const p of retired) {
console.log(` ✗ ${p.principle}`)
console.log(` Retired: ${p.retiredAt || 'unknown'}`)
}
}
}
// CLI
const command = process.argv[2]
if (command === 'promote') {
const idx = parseInt(process.argv[3])
if (isNaN(idx)) {
console.error('Usage: promote.js promote <candidate-index>')
process.exit(1)
}
promote(idx)
} else if (command === 'sync') {
sync()
} else if (command === 'list') {
list()
} else {
console.log('Knowledge Ratchet — Principle Promoter')
console.log('')
console.log('Commands:')
console.log(' list Show active principles and candidates')
console.log(' promote <index> Promote a candidate to active')
console.log(' sync Write active principles to rules file')
}