#!/usr/bin/env node
/**
* meta-patterns.js — Learn which KINDS of rules have highest utility
*
* For each rule/meta atom, compute:
* - Type (file-level rule, cross-loop pattern, preferred approach, strategic)
* - Average fire→no-correction rate (observed utility)
* - Average wisdom score
* - Cohort size
*
* Output: identifies which rule TYPES are most effective.
* Feeds back into rule generation — prefer proven-effective patterns.
*/
const fs = require('fs')
const path = require('path')
const { evalScheme, isCogServerUp } = require('./cogserver')
const DATA_DIR = path.join(__dirname, '..', 'data')
const FIRES_FILE = path.join(DATA_DIR, 'rule-fires.jsonl')
const CORRECTIONS_FILE = path.join(DATA_DIR, '..', 'corrections.log')
const UTILITY_WINDOW_SEC = 600 // 10 min after fire to detect "followed by correction"
function classifyAtomType (name) {
if (!name) return 'unknown'
if (name.startsWith('rule:rule-')) return 'ratchet-auto'
if (name.startsWith('rule:')) return 'rule'
if (name.startsWith('meta:pattern:')) return 'cross-loop-pattern'
if (name.startsWith('meta:loop:')) return 'per-loop-contrast'
if (name.startsWith('preferred:')) return 'preferred-approach'
if (name.startsWith('success:')) return 'success-record'
return 'other'
}
function readFires () {
if (!fs.existsSync(FIRES_FILE)) return []
const content = fs.readFileSync(FIRES_FILE, 'utf8')
return content.trim().split('\n').filter(Boolean).map(l => {
try { return JSON.parse(l) } catch { return null }
}).filter(Boolean)
}
function readCorrections () {
if (!fs.existsSync(CORRECTIONS_FILE)) return []
const content = fs.readFileSync(CORRECTIONS_FILE, 'utf8')
const blocks = content.split('\n---\n').filter(Boolean)
const entries = []
for (const block of blocks) {
const lines = block.trim().split('\n')
const m = (lines[0] || '').match(/^(\d{4}-\d{2}-\d{2}T[\d:.]+Z)\s*\[correction\]/)
if (!m) continue
entries.push({ ts: new Date(m[1]).getTime() })
}
return entries
}
async function main () {
const fires = readFires()
const corrections = readCorrections()
if (fires.length === 0) {
console.log('No fire data yet — run hooks for a while first.')
process.exit(0)
}
const cutoffMs = UTILITY_WINDOW_SEC * 1000
const correctionTimes = corrections.map(c => c.ts).sort((a, b) => a - b)
// For each fire, determine if correction followed within window
const byType = {}
for (const fire of fires) {
const t = new Date(fire.timestamp).getTime()
const kind = classifyAtomType(fire.atom)
if (!byType[kind]) byType[kind] = { fires: 0, followedByCorr: 0, atoms: new Set(), wisdomSum: 0, wisdomCount: 0 }
byType[kind].fires++
if (fire.wisdom != null) { byType[kind].wisdomSum += fire.wisdom; byType[kind].wisdomCount++ }
byType[kind].atoms.add(fire.atom)
// Did a correction happen within window?
const hasFollowup = correctionTimes.some(ct => ct > t && ct <= t + cutoffMs)
if (hasFollowup) byType[kind].followedByCorr++
}
// Compute utility rate per type
const meta = []
for (const [kind, stats] of Object.entries(byType)) {
const cleanRate = stats.fires > 0 ? (stats.fires - stats.followedByCorr) / stats.fires : 0
const avgWisdom = stats.wisdomCount > 0 ? stats.wisdomSum / stats.wisdomCount : 0
meta.push({
type: kind,
fires: stats.fires,
uniqueAtoms: stats.atoms.size,
followedByCorrection: stats.followedByCorr,
cleanRate: Math.round(cleanRate * 1000) / 1000,
avgWisdom: Math.round(avgWisdom * 100) / 100
})
}
meta.sort((a, b) => b.cleanRate - a.cleanRate)
console.log('=== Rule Type Meta-Analysis ===')
console.log(`Total fires: ${fires.length}`)
console.log(`Total corrections: ${corrections.length}`)
console.log(`Utility window: ${UTILITY_WINDOW_SEC}s\n`)
console.log('type fires unique clean-rate avg-wisdom')
console.log('─'.repeat(70))
for (const m of meta) {
console.log(
`${m.type.padEnd(26)} ${String(m.fires).padStart(5)} ` +
`${String(m.uniqueAtoms).padStart(6)} ${(m.cleanRate * 100).toFixed(1).padStart(8)}% ` +
`${m.avgWisdom.toFixed(2).padStart(9)}`
)
}
// Export meta-patterns for feedback loop
const output = {
generated: new Date().toISOString(),
utilityWindowSec: UTILITY_WINDOW_SEC,
totalFires: fires.length,
totalCorrections: corrections.length,
byType: meta
}
fs.writeFileSync(path.join(DATA_DIR, 'meta-patterns.json'), JSON.stringify(output, null, 2))
// Ingest as atoms
if (await isCogServerUp()) {
const exprs = []
for (const m of meta) {
const node = `(ConceptNode "meta-type:${m.type}")`
exprs.push(node)
exprs.push(`(cog-set-value! ${node} (Predicate "clean-rate") (FloatValue ${m.cleanRate}))`)
exprs.push(`(cog-set-value! ${node} (Predicate "fires") (FloatValue ${m.fires}))`)
exprs.push(`(cog-set-value! ${node} (Predicate "avg-wisdom") (FloatValue ${m.avgWisdom}))`)
}
if (exprs.length > 0) {
await evalScheme('(begin ' + exprs.join(' ') + ')')
console.log(`\nStored ${exprs.length} meta-pattern atoms.`)
}
}
}
if (require.main === module) {
main().catch(err => { console.error(err.message); process.exit(1) })
}
module.exports = { classifyAtomType }