#!/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 }