#!/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')
}