#!/usr/bin/env node
// Pattern & Principle Extractor
// Reads feedback_*.md files, groups by topic, outputs structured patterns and principles
// Used by the ratchet pipeline to generate candidate principles from raw corrections
const fs = require('fs')
const path = require('path')
const MEMORY_DIR = '/home/fleeky/.claude/projects/-storage-projects-code-hypercore-kb/memory'
const OUTPUT_DIR = path.join(__dirname, 'data')
const PATTERNS_FILE = path.join(OUTPUT_DIR, 'patterns.json')
const PRINCIPLES_FILE = path.join(OUTPUT_DIR, 'principles.json')
// Topic keywords for grouping
const TOPIC_KEYWORDS = {
'verification': ['verify', 'test', 'check', 'confirm', 'assume', 'investigate', 'repro', 'binary', 'background process'],
'deployment': ['deploy', 'push', 'pear', 'pgh', 'scp', 'ssh', 'rsync', 'stortron', 'ci', 'pipeline', 'sidecar', 'launcher', 'service script'],
'persistence': ['storage', 'delete', 'destroy', 'identity', 'data', 'rocksdb', 'hyperdrive', 'cleanup', 'gc', 'append-only'],
'discipline': ['fix', 'skip', 'kludge', 'bypass', 'workaround', 'shortcut', 'band-aid', 'batch', 'commit', 'incremental', 'step back', 'messy'],
'system-knowledge': ['understand', 'api', 'winding', 'version', 'bump', 'config', 'server', 'restart', 'nested', 'repo', 'git root'],
'communication': ['apolog', 'sorry']
}
function readFeedbackFiles () {
const files = fs.readdirSync(MEMORY_DIR)
.filter(f => f.startsWith('feedback_') && f.endsWith('.md'))
.sort() // alphabetical = stable order
.map(f => {
const content = fs.readFileSync(path.join(MEMORY_DIR, f), 'utf8')
const name = f.replace('feedback_', '').replace('.md', '')
return { file: f, name, content }
})
return files
}
function extractTopics (content) {
const lower = content.toLowerCase()
const topics = []
for (const [topic, keywords] of Object.entries(TOPIC_KEYWORDS)) {
if (keywords.some(kw => lower.includes(kw))) {
topics.push(topic)
}
}
return topics.length > 0 ? topics : ['uncategorized']
}
function extractCoreRule (content) {
// Pull the main rule from the feedback file
// Look for the first bold statement or the content after the frontmatter
const lines = content.split('\n')
let inFrontmatter = false
let rule = ''
for (const line of lines) {
if (line.trim() === '---') {
inFrontmatter = !inFrontmatter
continue
}
if (inFrontmatter) continue
const trimmed = line.trim()
if (!trimmed) continue
// First non-empty line after frontmatter, or first bold line
if (trimmed.startsWith('**') || trimmed.startsWith('#')) {
rule = trimmed.replace(/\*\*/g, '').replace(/^#+\s*/, '')
break
}
if (!rule) {
rule = trimmed
break
}
}
return rule
}
function extractWhy (content) {
const whyMatch = content.match(/\*\*Why:\*\*\s*(.+?)(?:\n\n|\*\*How)/s)
if (whyMatch) return whyMatch[1].trim().split('\n')[0]
const whySection = content.match(/Why:.*?\n(.+?)(?:\n\n|How to apply)/s)
if (whySection) return whySection[1].trim().split('\n')[0]
return ''
}
function groupByTopic (facts) {
const groups = {}
for (const fact of facts) {
for (const topic of fact.topics) {
if (!groups[topic]) groups[topic] = []
groups[topic].push(fact)
}
}
return groups
}
function extractPatterns (groups) {
const patterns = []
for (const [topic, facts] of Object.entries(groups)) {
if (facts.length < 2) continue // need at least 2 facts to form a pattern
// Find the common thread across facts in this group
const rules = facts.map(f => f.rule)
const whys = facts.map(f => f.why).filter(Boolean)
patterns.push({
topic,
factCount: facts.length,
facts: facts.map(f => f.name),
rules,
whys,
extractedAt: new Date().toISOString()
})
}
return patterns
}
function crystallizePrinciples (patterns) {
// Map well-populated patterns to principles
// These are the cross-cutting rules validated by the retrospective test
const principleMap = {
'discipline': {
principle: 'When something breaks, fix the system — never work around it. One change, one verify, one commit.',
coveredPatterns: ['discipline'],
strength: 'high'
},
'verification': {
principle: 'Verify before acting — understand the full system, confirm state, never assume.',
coveredPatterns: ['verification'],
strength: 'high'
},
'deployment': {
principle: 'Use established tools and paths. Never invent ad-hoc alternatives for deployment.',
coveredPatterns: ['deployment'],
strength: 'high'
},
'persistence': {
principle: 'Never destroy persistent state. Every storage decision must answer: what is the deletion story?',
coveredPatterns: ['persistence'],
strength: 'high'
},
'system-knowledge': {
principle: 'Understand the full system before modifying any part. Research APIs, check downstream impact.',
coveredPatterns: ['system-knowledge'],
strength: 'medium'
}
}
const principles = []
for (const pattern of patterns) {
const mapped = principleMap[pattern.topic]
if (mapped) {
principles.push({
...mapped,
supportingFacts: pattern.facts,
factCount: pattern.factCount,
validatedAt: new Date().toISOString(),
contradictions: [],
overrideCount: 0,
status: 'active'
})
}
}
return principles
}
function main () {
if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true })
// Step 1: Read all feedback files
const feedbackFiles = readFeedbackFiles()
console.log(`Read ${feedbackFiles.length} feedback files`)
// Step 2: Extract facts with topics
const facts = feedbackFiles.map(f => ({
name: f.name,
file: f.file,
rule: extractCoreRule(f.content),
why: extractWhy(f.content),
topics: extractTopics(f.content),
content: f.content
}))
// Step 3: Group by topic
const groups = groupByTopic(facts)
console.log(`\nTopic groups:`)
for (const [topic, members] of Object.entries(groups)) {
console.log(` ${topic}: ${members.length} facts`)
for (const m of members) {
console.log(` - ${m.name}`)
}
}
// Step 4: Extract patterns
const patterns = extractPatterns(groups)
fs.writeFileSync(PATTERNS_FILE, JSON.stringify(patterns, null, 2))
console.log(`\nExtracted ${patterns.length} patterns → ${PATTERNS_FILE}`)
// Step 5: Crystallize principles
const principles = crystallizePrinciples(patterns)
fs.writeFileSync(PRINCIPLES_FILE, JSON.stringify(principles, null, 2))
console.log(`Crystallized ${principles.length} principles → ${PRINCIPLES_FILE}`)
// Step 6: Print summary
console.log('\n═══ EXTRACTED PRINCIPLES ═══')
for (const p of principles) {
console.log(`\n[${p.strength}] ${p.principle}`)
console.log(` Supported by ${p.factCount} facts: ${p.supportingFacts.join(', ')}`)
}
return { facts, patterns, principles }
}
main()