OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet / promote.js

188 lines · javascript ref:
#!/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')
}