OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet / ratchet.js

100 lines · javascript ref:
#!/usr/bin/env node

// Forward-Only Ratchet
// Wraps principle changes in git commits
// If validation score drops after a change, reverts the commit
// Principles can only improve, never degrade

const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')

const ROOT = path.join(__dirname, '..')
const VALIDATION_SCRIPT = path.join(__dirname, 'validate-principles.js')
const PRINCIPLES_DATA = path.join(__dirname, 'data', 'principles.json')

function git (cmd) {
  return execSync(`git ${cmd}`, { cwd: ROOT, encoding: 'utf8', stdio: 'pipe' }).trim()
}

function getValidationScore () {
  try {
    execSync(`node ${VALIDATION_SCRIPT}`, { cwd: ROOT, stdio: 'pipe' })
    const report = JSON.parse(fs.readFileSync(
      path.join(__dirname, 'data', 'validation-report.json'), 'utf8'
    ))
    return report.scores.lenientRate
  } catch {
    return null
  }
}

function ratchetCommit (message) {
  // Step 1: get baseline score
  const baseline = getValidationScore()
  console.log(`Baseline validation score: ${baseline !== null ? (baseline * 100).toFixed(0) + '%' : 'n/a'}`)

  // Step 2: stage knowledge-ratchet changes
  try {
    git('add knowledge-ratchet/active-principles.md')
    git('add knowledge-ratchet/data/')
    git('add .claude/rules/09-ratchet-principles.md')
  } catch {}

  // Step 3: check if there's anything to commit
  const status = git('status --porcelain knowledge-ratchet/ .claude/rules/09-ratchet-principles.md')
  if (!status) {
    console.log('Nothing to commit.')
    return
  }

  // Step 4: commit
  const fullMessage = `${message}\n\nKnowledge ratchet: principle update\n\nCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>`
  try {
    git(`commit -m "${fullMessage.replace(/"/g, '\\"')}"`)
  } catch (e) {
    console.error('Commit failed:', e.message)
    return
  }
  console.log('Committed principle change.')

  // Step 5: validate
  const newScore = getValidationScore()
  console.log(`New validation score: ${newScore !== null ? (newScore * 100).toFixed(0) + '%' : 'n/a'}`)

  // Step 6: ratchet check
  if (baseline !== null && newScore !== null && newScore < baseline) {
    console.log(`\n⚠ REGRESSION: score dropped from ${(baseline * 100).toFixed(0)}% to ${(newScore * 100).toFixed(0)}%`)
    console.log('Reverting commit...')
    git('reset HEAD~1')
    git('checkout -- knowledge-ratchet/ .claude/rules/09-ratchet-principles.md')
    console.log('Reverted. Principles unchanged.')
  } else {
    console.log('✓ Ratchet holds. Principles improved or stable.')
  }
}

// CLI
const command = process.argv[2]

if (command === 'commit') {
  const message = process.argv.slice(3).join(' ') || 'Update knowledge ratchet principles'
  ratchetCommit(message)
} else if (command === 'score') {
  const score = getValidationScore()
  if (score !== null) {
    console.log(`Current validation score: ${(score * 100).toFixed(0)}%`)
  } else {
    console.log('Could not compute validation score.')
  }
} else {
  console.log('Knowledge Ratchet — Forward-Only Git Ratchet')
  console.log('')
  console.log('Commands:')
  console.log('  commit [message]  Commit principle changes with validation guard')
  console.log('  score             Show current validation score')
  console.log('')
  console.log('If validation score drops after commit, the change is reverted.')
  console.log('Principles can only improve, never degrade.')
}