OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet / atomspace / contrast-rules.js

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

/**
 * contrast-rules.js — Generate meta-cognition atoms from success/failure contrast
 *
 * For each resolved loop, contrast filesTried vs resolution.files to find
 * "approaches that didn't work" and "approach that did work."
 *
 * Also clusters loops by (project, language, loop-type) to find patterns that
 * hold across multiple loops — stronger evidence than single-loop contrasts.
 *
 * Output:
 *   meta: atoms with: context, positive-approach, negative-approaches, evidence-count
 *   contrast-edges: meta-atom → file edges (approach-works, approach-fails)
 *
 * Usage:
 *   node atomspace/contrast-rules.js               # generate and ingest
 *   node atomspace/contrast-rules.js --dry-run    # print, don't ingest
 */

const fs = require('fs')
const path = require('path')
const { evalScheme, isCogServerUp } = require('./cogserver')
const S = require('./schema')

const LOOPS_FILE = path.join(__dirname, '..', 'data', 'loops.json')

function esc (s) {
  return String(s || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"')
}

/**
 * Normalize file path to match codetree conventions.
 * Strips /storage/projects/code/hypercore-kb/hyper-fleeky/ etc.
 */
function normalizePath (p) {
  if (!p) return ''
  return p
    .replace(/^\/storage\/projects\/code\/hypercore-kb\/hyper-fleeky\//, '')
    .replace(/^\/storage\/projects\/code\/hypercore-kb\//, '')
    .replace(/^\/storage\/projects\/code\//, '')
}

/**
 * Infer language from file extension.
 */
function langOf (p) {
  if (/\.(cpp|cc|h|hpp)$/.test(p)) return 'cpp'
  if (/\.gd$/.test(p)) return 'gdscript'
  if (/\.(glsl|gdshader|shader)$/.test(p)) return 'glsl'
  if (/\.py$/.test(p)) return 'python'
  if (/\.js$/.test(p)) return 'javascript'
  if (/\.md$/.test(p)) return 'markdown'
  if (/\.scm$/.test(p)) return 'scheme'
  if (/\.json$/.test(p)) return 'json'
  return 'unknown'
}

/**
 * Per-loop contrast: files tried that didn't fix it vs files that did.
 */
function contrastPerLoop (loop) {
  if (!loop.resolved) return null
  if (!loop.filesTried || !loop.resolution || !loop.resolution.files) return null

  const tried = loop.filesTried.map(normalizePath).filter(Boolean)
  const worked = loop.resolution.files.map(normalizePath).filter(Boolean)
  const wasted = tried.filter(f => !worked.includes(f))

  if (worked.length === 0 || wasted.length === 0) return null

  // Primary language of the resolution (usually same-language files cluster together)
  const workedLangs = worked.map(langOf)
  const dominantLang = workedLangs[0] || 'unknown'

  return {
    loopId: loop.fingerprint || loop.id,
    project: loop.project,
    type: loop.type,
    language: dominantLang,
    worked,   // approach that did work
    wasted,   // approaches that didn't work
    correctionSnippet: (loop.corrections && loop.corrections[0] || '').slice(0, 200),
    duration: loop.duration,
    correctionCount: loop.correctionCount || 0
  }
}

/**
 * Cluster per-loop contrasts into cross-loop patterns.
 * Group by (project, language, loop-type) — loops in same context.
 *
 * For each group, compute per-file:
 *   resolveCount  — # of loops where this file was in resolution.files
 *   wastedCount   — # of loops where this file was in filesTried but NOT resolution
 *
 * Files with high resolveCount = "try this first"
 * Files with high wastedCount = "don't try this"
 */
function clusterContrasts (perLoopContrasts) {
  const groups = new Map()
  for (const c of perLoopContrasts) {
    const key = `${c.project}:${c.language}:${c.type}`
    if (!groups.has(key)) {
      groups.set(key, { project: c.project, language: c.language, type: c.type, loops: [] })
    }
    groups.get(key).loops.push(c)
  }

  // Build a broader grouping: also group by (project, language) alone — wider nets.
  const broadGroups = new Map()
  for (const c of perLoopContrasts) {
    const key = `${c.project}:${c.language}:*`
    if (!broadGroups.has(key)) {
      broadGroups.set(key, { project: c.project, language: c.language, type: '*', loops: [] })
    }
    broadGroups.get(key).loops.push(c)
  }
  for (const [key, grp] of broadGroups) {
    if (!groups.has(key)) groups.set(key, grp)
  }

  const patterns = []
  for (const [key, grp] of groups) {
    if (grp.loops.length < 2) continue  // need ≥2 loops for a pattern (research threshold)

    const resolveCount = new Map()
    const wastedCount = new Map()
    for (const c of grp.loops) {
      for (const f of c.worked) {
        resolveCount.set(f, (resolveCount.get(f) || 0) + 1)
      }
      for (const f of c.wasted) {
        wastedCount.set(f, (wastedCount.get(f) || 0) + 1)
      }
    }

    // Strong signals: file resolved ≥2 loops, or file was wasted ≥3 times
    const tryFirst = [...resolveCount.entries()]
      .filter(([f, n]) => n >= 2)
      .sort((a, b) => b[1] - a[1])
      .map(([f, n]) => ({ file: f, evidence: n }))

    const avoid = [...wastedCount.entries()]
      .filter(([f, n]) => n >= 2 && (resolveCount.get(f) || 0) === 0)
      .sort((a, b) => b[1] - a[1])
      .map(([f, n]) => ({ file: f, evidence: n }))

    if (tryFirst.length > 0 || avoid.length > 0) {
      patterns.push({
        key,
        context: { project: grp.project, language: grp.language, type: grp.type },
        loopCount: grp.loops.length,
        tryFirst,
        avoid
      })
    }
  }

  return patterns
}

/**
 * Generate Scheme expressions for meta-cognition atoms and ingest them.
 */
async function ingestContrasts (perLoopContrasts, patterns, dryRun = false) {
  const exprs = []

  // Per-loop contrast atoms (individual evidence)
  for (const c of perLoopContrasts) {
    const metaId = `meta:loop:${c.loopId}`
    const metaNode = `(ConceptNode "${esc(metaId)}")`

    exprs.push(metaNode)
    exprs.push(`(cog-set-value! ${metaNode} (Predicate "kind") (StringValue "per-loop-contrast"))`)
    exprs.push(`(cog-set-value! ${metaNode} (Predicate "project") (StringValue "${esc(c.project)}"))`)
    exprs.push(`(cog-set-value! ${metaNode} (Predicate "loop-type") (StringValue "${esc(c.type)}"))`)
    exprs.push(`(cog-set-value! ${metaNode} (Predicate "language") (StringValue "${esc(c.language)}"))`)
    exprs.push(`(cog-set-value! ${metaNode} (Predicate "correction-snippet") (StringValue "${esc(c.correctionSnippet)}"))`)
    exprs.push(`(cog-set-value! ${metaNode} (Predicate "duration") (FloatValue ${c.duration || 0}))`)

    // Link meta → worked files (approach-works)
    for (const f of c.worked) {
      exprs.push(`(EdgeLink (BondNode "approach-works") (ListLink ${metaNode} (ConceptNode "file:${esc(f)}")))`)
    }
    // Link meta → wasted files (approach-fails)
    for (const f of c.wasted) {
      exprs.push(`(EdgeLink (BondNode "approach-fails") (ListLink ${metaNode} (ConceptNode "file:${esc(f)}")))`)
    }
  }

  // Cross-loop pattern atoms (aggregate evidence)
  for (const p of patterns) {
    const metaId = `meta:pattern:${p.key}`
    const metaNode = `(ConceptNode "${esc(metaId)}")`

    exprs.push(metaNode)
    exprs.push(`(cog-set-value! ${metaNode} (Predicate "kind") (StringValue "cross-loop-pattern"))`)
    exprs.push(`(cog-set-value! ${metaNode} (Predicate "project") (StringValue "${esc(p.context.project)}"))`)
    exprs.push(`(cog-set-value! ${metaNode} (Predicate "language") (StringValue "${esc(p.context.language)}"))`)
    exprs.push(`(cog-set-value! ${metaNode} (Predicate "loop-type") (StringValue "${esc(p.context.type)}"))`)
    exprs.push(`(cog-set-value! ${metaNode} (Predicate "loop-count") (FloatValue ${p.loopCount}))`)

    for (const t of p.tryFirst) {
      exprs.push(`(EdgeLink (BondNode "try-first") (ListLink ${metaNode} (ConceptNode "file:${esc(t.file)}")))`)
      // Store evidence count as Value on the EdgeLink? Use a separate counter.
    }
    for (const a of p.avoid) {
      exprs.push(`(EdgeLink (BondNode "avoid-first") (ListLink ${metaNode} (ConceptNode "file:${esc(a.file)}")))`)
    }
  }

  console.log(`Generated ${exprs.length} Scheme expressions from:`)
  console.log(`  ${perLoopContrasts.length} per-loop contrasts`)
  console.log(`  ${patterns.length} cross-loop patterns`)

  if (dryRun) {
    console.log('\n--- SAMPLE PATTERNS (top 5 by loop count) ---')
    for (const p of patterns.sort((a, b) => b.loopCount - a.loopCount).slice(0, 5)) {
      console.log(`\n  ${p.key}  (${p.loopCount} loops)`)
      if (p.tryFirst.length) {
        console.log('    try first:')
        for (const t of p.tryFirst.slice(0, 3)) console.log(`      ${t.file}  (resolved ${t.evidence} loops)`)
      }
      if (p.avoid.length) {
        console.log('    avoid first:')
        for (const a of p.avoid.slice(0, 3)) console.log(`      ${a.file}  (wasted ${a.evidence} times)`)
      }
    }
    return { ok: true, dryRun: true, contrasts: perLoopContrasts.length, patterns: patterns.length }
  }

  if (!await isCogServerUp()) {
    console.log('CogServer not running, skipping ingest')
    return { ok: false, reason: 'cogserver-down' }
  }

  // Batch ingest
  const BATCH = 50
  let sent = 0
  for (let i = 0; i < exprs.length; i += BATCH) {
    const batch = exprs.slice(i, i + BATCH)
    const result = await evalScheme('(begin ' + batch.join(' ') + ')')
    if (result === null) {
      console.error(`Connection lost at batch ${i}`)
      return { ok: false, reason: 'connection-lost', sent }
    }
    sent += batch.length
  }

  console.log(`Ingested ${sent} atoms/edges`)
  return { ok: true, sent, contrasts: perLoopContrasts.length, patterns: patterns.length }
}

async function main () {
  const dryRun = process.argv.includes('--dry-run')

  const loopsData = JSON.parse(fs.readFileSync(LOOPS_FILE, 'utf8'))
  const loops = loopsData.loops || []

  console.log(`Analyzing ${loops.length} loops...`)

  // Per-loop contrasts
  const contrasts = loops.map(contrastPerLoop).filter(Boolean)
  console.log(`Per-loop contrasts: ${contrasts.length}`)

  // Cross-loop patterns
  const patterns = clusterContrasts(contrasts)
  console.log(`Cross-loop patterns: ${patterns.length}`)

  const result = await ingestContrasts(contrasts, patterns, dryRun)
  console.log('\n' + JSON.stringify(result, null, 2))
}

main().catch(err => { console.error(err.message); process.exit(1) })