OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet / atomspace / success-promote.js

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

/**
 * success-promote.js — Promote repeated success patterns to preferred-approach rules
 *
 * When N successes share context (same files, same shape, same module), that's
 * a repeatable pattern worth preserving as guidance — not just a one-off success.
 *
 * Process:
 *   1. Group success records by (primary-file, shape) — shape=resolution/validation
 *   2. If ≥3 successes in a group, create a preferred-approach rule
 *   3. The rule surfaces in attention-router when editing the primary-file
 *
 * Output: new meta:preferred-approach: atoms linked to files.
 */

const fs = require('fs')
const path = require('path')
const { evalScheme, isCogServerUp } = require('./cogserver')
const { extractSuccesses, classifySuccess } = require('./success-memory')

const DATA_DIR = path.join(__dirname, '..', 'data')
const MIN_SUCCESSES = 3

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

function normalizePath (f) {
  if (!f) return ''
  return f
    .replace(/^\/storage\/projects\/code\/hypercore-kb\/hyper-fleeky\//, '')
    .replace(/^\/storage\/projects\/code\/hypercore-kb\//, '')
    .replace(/^\/storage\/projects\/code\//, '')
}

/**
 * Group successes into promotable patterns.
 * Group by (normalizedPath of primary file, shape).
 */
function groupSuccesses (successes) {
  const groups = new Map()
  for (const s of successes) {
    // Primary file = first file edited in window
    const primary = normalizePath(s.files[0])
    if (!primary) continue
    const shape = classifySuccess(s)
    const key = `${primary}::${shape}`
    if (!groups.has(key)) groups.set(key, { primary, shape, successes: [] })
    groups.get(key).successes.push(s)
  }
  return groups
}

/**
 * For each group with ≥N successes, create a preferred-approach atom.
 */
function buildPreferredApproaches (groups) {
  const approaches = []
  for (const [key, group] of groups) {
    if (group.successes.length < MIN_SUCCESSES) continue

    // Aggregate context: all secondary files touched across successes
    const secondaryFiles = {}
    for (const s of group.successes) {
      for (const f of s.files.slice(1, 5)) {
        const np = normalizePath(f)
        if (np) secondaryFiles[np] = (secondaryFiles[np] || 0) + 1
      }
    }
    // Top 3 secondary files
    const topSecondary = Object.entries(secondaryFiles)
      .sort((a, b) => b[1] - a[1])
      .slice(0, 3)
      .map(([f]) => f)

    // Avg metrics
    const avgScore = group.successes.reduce((s, x) => s + x.avgScore, 0) / group.successes.length
    const sessions = [...new Set(group.successes.map(s => s.sessionId))]

    approaches.push({
      id: `preferred:${group.primary}:${group.shape}`,
      primaryFile: group.primary,
      shape: group.shape,
      successCount: group.successes.length,
      sessionCount: sessions.length,
      avgScore: Math.round(avgScore * 1000) / 1000,
      secondaryFiles: topSecondary,
      // Sample approval text (first approval with text)
      sampleApproval: (group.successes.find(s => s.approvalText) || group.successes[0]).approvalText
    })
  }
  return approaches
}

function approachToScheme (a) {
  const exprs = []
  const node = `(ConceptNode "${esc(a.id)}")`

  exprs.push(node)
  exprs.push(`(cog-set-value! ${node} (Predicate "kind") (StringValue "preferred-approach"))`)
  exprs.push(`(cog-set-value! ${node} (Predicate "shape") (StringValue "${a.shape}"))`)
  exprs.push(`(cog-set-value! ${node} (Predicate "success-count") (FloatValue ${a.successCount}))`)
  exprs.push(`(cog-set-value! ${node} (Predicate "session-count") (FloatValue ${a.sessionCount}))`)
  exprs.push(`(cog-set-value! ${node} (Predicate "avg-score") (FloatValue ${a.avgScore}))`)
  exprs.push(`(cog-set-value! ${node} (Predicate "sample-approval") (StringValue "${esc(a.sampleApproval || '')}"))`)

  // Link to primary file (PREFERRED approach when editing this file)
  const primaryAtom = `(ConceptNode "file:${esc(a.primaryFile)}")`
  exprs.push(`(EdgeLink (BondNode "preferred-for-file") (ListLink ${node} ${primaryAtom}))`)

  // Link to secondary files (often edited together)
  for (const sf of a.secondaryFiles) {
    exprs.push(`(EdgeLink (BondNode "preferred-secondary") (ListLink ${node} (ConceptNode "file:${esc(sf)}")))`)
  }

  return exprs
}

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

  const detail = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'fused-turns-detail.json'), 'utf8'))
  const sessions = detail.sessions || detail

  // Extract all successes
  const allSuccesses = []
  for (const s of sessions) {
    allSuccesses.push(...extractSuccesses(s))
  }
  console.log(`Total successes: ${allSuccesses.length}`)

  // Group and filter
  const groups = groupSuccesses(allSuccesses)
  const approaches = buildPreferredApproaches(groups)

  console.log(`Groups with ≥${MIN_SUCCESSES} successes: ${approaches.length}`)

  console.log('\n── Top 15 preferred approaches (by success count) ──')
  const sorted = [...approaches].sort((a, b) => b.successCount - a.successCount)
  for (const a of sorted.slice(0, 15)) {
    console.log(`  ${String(a.successCount).padStart(3)} successes [${a.shape}]  ${a.primaryFile}  (${a.sessionCount} sessions, avg score ${a.avgScore})`)
    if (a.secondaryFiles.length > 0) {
      console.log(`       + often touches: ${a.secondaryFiles.slice(0, 2).join(', ')}`)
    }
  }

  if (dryRun) {
    console.log('\n--dry-run: not ingesting')
    return
  }

  if (!await isCogServerUp()) {
    console.log('\nCogServer not running, skipping ingest')
    return
  }

  const allExprs = approaches.flatMap(approachToScheme)
  console.log(`\nIngesting ${allExprs.length} Scheme expressions...`)
  const BATCH = 50
  let sent = 0
  for (let i = 0; i < allExprs.length; i += BATCH) {
    const batch = allExprs.slice(i, i + BATCH)
    await evalScheme('(begin ' + batch.join(' ') + ')')
    sent += batch.length
  }
  console.log(`Stored: ${sent} atoms/edges for ${approaches.length} preferred approaches`)
}

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

module.exports = { groupSuccesses, buildPreferredApproaches }