OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet / contradiction-detector.js

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

// Contradiction Detector
// Checks new corrections against existing principles
// When a contradiction is found, writes a question file for the user
// Integrated into the correction capture pipeline

const fs = require('fs')
const path = require('path')

const PRINCIPLES_FILE = path.join(__dirname, 'data', 'principles.json')
const QUESTIONS_DIR = path.join(__dirname, 'questions')
const OVERRIDES_FILE = path.join(__dirname, 'data', 'overrides.json')
const OVERRIDE_THRESHOLD = 3

// Load principles
function loadPrinciples () {
  if (!fs.existsSync(PRINCIPLES_FILE)) return []
  try {
    return JSON.parse(fs.readFileSync(PRINCIPLES_FILE, 'utf8'))
  } catch {
    return []
  }
}

// Load override counts
function loadOverrides () {
  if (!fs.existsSync(OVERRIDES_FILE)) return {}
  try {
    return JSON.parse(fs.readFileSync(OVERRIDES_FILE, 'utf8'))
  } catch {
    return {}
  }
}

function saveOverrides (overrides) {
  const dir = path.dirname(OVERRIDES_FILE)
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
  fs.writeFileSync(OVERRIDES_FILE, JSON.stringify(overrides, null, 2))
}

// Check if a correction contradicts any principle
function findContradictions (correctionText, principles) {
  const lower = correctionText.toLowerCase()
  const contradictions = []

  for (const principle of principles) {
    if (principle.status !== 'active') continue

    // Check if the correction's domain overlaps with the principle's domain
    const principleWords = principle.principle.toLowerCase().split(/\s+/)
    const factWords = principle.supportingFacts.join(' ').toLowerCase().split(/[\s_-]+/)
    const allPrincipleContext = [...principleWords, ...factWords]

    // Count overlapping context words (skip common words)
    const skipWords = new Set(['the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been',
      'do', 'does', 'did', 'have', 'has', 'had', 'will', 'would', 'could', 'should',
      'may', 'might', 'must', 'shall', 'can', 'to', 'of', 'in', 'for', 'on', 'with',
      'at', 'by', 'from', 'not', 'no', 'or', 'and', 'but', 'if', 'then', 'than',
      'that', 'this', 'it', 'its', 'my', 'your', 'we', 'they', 'any', 'all', 'every'])

    const strip = w => w.replace(/[^a-z0-9]/g, '')
    const correctionWords = lower.split(/\s+/).map(strip).filter(w => !skipWords.has(w) && w.length > 2)
    // Only match against principle text itself, not supporting facts (too broad)
    const contextWords = principleWords.map(strip).filter(w => !skipWords.has(w) && w.length > 2)

    const overlap = correctionWords.filter(w => contextWords.includes(w))

    if (overlap.length >= 2) {
      // Same domain — check for contradiction signals
      const contradictionSignals = [
        /\b(actually|but|however|instead|wrong about|not true|changed my mind)\b/i,
        /\b(use .+ now|prefer .+ instead|go ahead and|it's fine to|you can)\b/i,
        /\b(ignore that|forget .+ rule|that's outdated|doesn't apply)\b/i
      ]

      const hasContradictionSignal = contradictionSignals.some(p => p.test(correctionText))

      if (hasContradictionSignal) {
        contradictions.push({
          principle: principle.principle,
          overlap: overlap.join(', '),
          principleIndex: principles.indexOf(principle)
        })
      }
    }
  }

  return contradictions
}

// Record an override (user proceeded past a hook block)
function recordOverride (principleText) {
  const overrides = loadOverrides()
  if (!overrides[principleText]) {
    overrides[principleText] = { count: 0, timestamps: [] }
  }
  overrides[principleText].count++
  overrides[principleText].timestamps.push(new Date().toISOString())
  saveOverrides(overrides)

  // Check threshold
  if (overrides[principleText].count >= OVERRIDE_THRESHOLD) {
    writeQuestion(
      `override-threshold-${Date.now()}`,
      `You've overridden this principle ${overrides[principleText].count} times:\n\n` +
      `"${principleText}"\n\n` +
      `Overrides on: ${overrides[principleText].timestamps.join(', ')}\n\n` +
      `Should I:\n` +
      `1. RETIRE this principle (remove from active set)\n` +
      `2. REVISE it (tell me the updated version)\n` +
      `3. KEEP it (the overrides were exceptions, principle is still valid)\n`
    )
    return true
  }
  return false
}

// Write a question file for the user
function writeQuestion (id, questionText) {
  if (!fs.existsSync(QUESTIONS_DIR)) fs.mkdirSync(QUESTIONS_DIR, { recursive: true })

  const questionFile = path.join(QUESTIONS_DIR, `${id}.md`)
  const content = `---\nid: ${id}\ncreated: ${new Date().toISOString()}\nstatus: pending\n---\n\n${questionText}`

  fs.writeFileSync(questionFile, content)
  console.log(`Question written: ${questionFile}`)
}

// Check for contradictions in a new correction
function checkCorrection (correctionText) {
  const principles = loadPrinciples()
  if (principles.length === 0) return []

  const contradictions = findContradictions(correctionText, principles)

  for (const c of contradictions) {
    writeQuestion(
      `contradiction-${Date.now()}`,
      `A new correction may contradict an existing principle.\n\n` +
      `**Existing principle:**\n"${c.principle}"\n\n` +
      `**New correction:**\n"${correctionText.slice(0, 500)}"\n\n` +
      `**Overlapping domain:** ${c.overlap}\n\n` +
      `Has your thinking changed? Should I:\n` +
      `1. UPDATE the principle to reflect your new understanding\n` +
      `2. KEEP the principle (the new correction is a special case, not a contradiction)\n` +
      `3. REPLACE the principle with a new one (tell me what it should say)\n`
    )
  }

  return contradictions
}

// CLI interface
const args = process.argv.slice(2)
const command = args[0]

if (command === 'check') {
  // Check a correction text for contradictions
  const text = args.slice(1).join(' ')
  if (!text) {
    console.error('Usage: contradiction-detector.js check "correction text"')
    process.exit(1)
  }
  const contradictions = checkCorrection(text)
  if (contradictions.length === 0) {
    console.log('No contradictions found.')
  } else {
    console.log(`Found ${contradictions.length} potential contradiction(s). Questions written to ${QUESTIONS_DIR}/`)
  }
} else if (command === 'override') {
  // Record a principle override
  const principleText = args.slice(1).join(' ')
  if (!principleText) {
    console.error('Usage: contradiction-detector.js override "principle text"')
    process.exit(1)
  }
  const thresholdReached = recordOverride(principleText)
  if (thresholdReached) {
    console.log(`Override threshold reached! Question written to ${QUESTIONS_DIR}/`)
  }
} else if (command === 'pending') {
  // List pending questions
  if (!fs.existsSync(QUESTIONS_DIR)) {
    console.log('No pending questions.')
    process.exit(0)
  }
  const questions = fs.readdirSync(QUESTIONS_DIR).filter(f => f.endsWith('.md'))
  if (questions.length === 0) {
    console.log('No pending questions.')
  } else {
    console.log(`${questions.length} pending question(s):`)
    for (const q of questions) {
      const content = fs.readFileSync(path.join(QUESTIONS_DIR, q), 'utf8')
      const firstLine = content.split('\n').find(l => l.trim() && !l.startsWith('---') && !l.startsWith('id:') && !l.startsWith('created:') && !l.startsWith('status:'))
      console.log(`  ${q}: ${firstLine || '(no preview)'}`)
    }
  }
} else {
  console.log('Knowledge Ratchet — Contradiction Detector')
  console.log('')
  console.log('Commands:')
  console.log('  check "text"     Check correction text against active principles')
  console.log('  override "text"  Record an override of a principle')
  console.log('  pending          List pending questions for the user')
}