OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet / atomspace / meta-patterns.js

148 lines · javascript ref:
#!/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 }