#!/usr/bin/env node
/**
* wisdom-score.js — Compute and store wisdom scores for rules and meta atoms
*
* Three measurable dimensions:
* 1. referent_health — % of files referenced that still exist
* 2. breadth — how many distinct contexts the rule applies to
* 3. utility — prevented_fires / (prevented + contradicted)
*
* Composite: wisdom = utility × referent_health × log(1 + breadth)
*
* Storage (as FloatValues on each rule/meta atom):
* (Predicate "wisdom") composite score 0..1+
* (Predicate "referent-health") 0..1
* (Predicate "breadth") integer count
* (Predicate "utility") 0..1
* (Predicate "utility-fires") times this rule fired
* (Predicate "utility-contradictions") times a correction followed a firing
*
* NO ATOMS ARE DELETED. This only ranks.
*
* Usage:
* node atomspace/wisdom-score.js # compute + store
* node atomspace/wisdom-score.js --report # just print, don't store
*/
const fs = require('fs')
const path = require('path')
const { evalScheme, isCogServerUp } = require('./cogserver')
const HYPERCORE_KB = path.resolve(__dirname, '../..')
const HYPER_FLEEKY = path.join(HYPERCORE_KB, 'hyper-fleeky')
function esc (s) {
return String(s || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"')
}
/**
* Parse a scheme result listing atom names.
*/
function parseAtomNames (resultStr) {
if (!resultStr) return []
const names = []
const re = /(?:ConceptNode|Concept) "([^"]+)"/g
let m
while ((m = re.exec(resultStr)) !== null) names.push(m[1])
return names
}
/**
* Check if a file exists at its real filesystem location.
* Paths in AtomSpace are normalized — reconstruct the full path.
*/
function fileExists (normalPath) {
if (!normalPath) return false
// Skip non-file paths (memory notes, plans, external)
if (normalPath.startsWith('/home/') || normalPath.startsWith('/tmp/')) {
// Memory files — check existence at actual path
try { return fs.existsSync(normalPath) } catch { return false }
}
const candidates = [
normalPath, // already absolute
path.join(HYPER_FLEEKY, normalPath),
path.join(HYPERCORE_KB, normalPath),
path.join('/storage/projects/code', normalPath),
path.join('/storage/projects/code/godot', normalPath) // Godot projects live here
]
for (const p of candidates) {
try { if (fs.existsSync(p)) return true } catch {}
}
// For Godot paths with module/<file>.cpp pattern, try module/src/<file>.cpp
// (ratchet's project-mapper sometimes strips the src/ prefix)
const parts = normalPath.split('/')
if (parts.length === 2) {
const withSrc = path.join('/storage/projects/code/godot', parts[0], 'src', parts[1])
try { if (fs.existsSync(withSrc)) return true } catch {}
}
return false
}
/**
* For each rule/meta atom, get its referenced files.
* Rules link via 'rule-for-file'. Meta atoms link via 'approach-works',
* 'approach-fails', 'try-first', 'avoid-first'.
*/
async function getFilesForAtom (atomName, bondTypes) {
const files = new Set()
for (const bond of bondTypes) {
const result = await evalScheme(
`(cog-execute! (Meet (TypedVariable (Variable "$f") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "${bond}") (ListLink (ConceptNode "${esc(atomName)}") (Variable "$f"))))))`)
for (const name of parseAtomNames(result)) {
if (name.startsWith('file:')) files.add(name.slice(5))
}
}
return [...files]
}
/**
* Breadth: how many distinct contexts this rule applies to.
* For meta:loop: atoms — always 1 (single loop).
* For meta:pattern: atoms — read loop-count Value.
* For rule: atoms — count distinct files referenced (proxy for scope).
*/
async function computeBreadth (atomName) {
if (atomName.startsWith('meta:pattern:')) {
const lc = await evalScheme(
`(let ((v (cog-value (ConceptNode "${esc(atomName)}") (Predicate "loop-count")))) (if v (cog-value-ref v 0) 1))`)
return Math.max(1, parseFloat(lc) || 1)
}
return 1
}
/**
* Compute wisdom for all rule and meta atoms.
*/
async function computeAll (report = false) {
if (!await isCogServerUp()) {
console.log('CogServer not running')
process.exit(1)
}
// Collect all rule: and meta: atoms
const allAtomsStr = await evalScheme(
'(string-join (map cog-name (cog-get-atoms (quote ConceptNode))) "|")')
const allAtoms = (allAtomsStr || '').replace(/^"|"$/g, '').split('|')
const ruleAtoms = allAtoms.filter(n => n.startsWith('rule:'))
const metaAtoms = allAtoms.filter(n => n.startsWith('meta:'))
console.log(`Scoring ${ruleAtoms.length} rule atoms + ${metaAtoms.length} meta atoms`)
const results = []
const batch = []
// Rule atoms: use 'rule-for-file' bond
for (const atomName of ruleAtoms) {
const files = await getFilesForAtom(atomName, ['rule-for-file'])
const existingFiles = files.filter(fileExists)
const referent_health = files.length > 0 ? existingFiles.length / files.length : 1.0
// Utility: read existing values, default to 1.0
const firesStr = await evalScheme(
`(let ((v (cog-value (ConceptNode "${esc(atomName)}") (Predicate "utility-fires")))) (if v (cog-value-ref v 0) 0))`)
const contradStr = await evalScheme(
`(let ((v (cog-value (ConceptNode "${esc(atomName)}") (Predicate "utility-contradictions")))) (if v (cog-value-ref v 0) 0))`)
const fires = parseFloat(firesStr) || 0
const contradictions = parseFloat(contradStr) || 0
let utility = (fires + contradictions) === 0
? 1.0 // innocent until proven guilty
: Math.max(0.05, (fires - contradictions) / (fires + contradictions) * 0.5 + 0.5)
// Phase 4.2: Time decay — rules that haven't fired in 30+ days decay
// Does NOT delete — just reduces utility until it fires again
const lastFiredStr = await evalScheme(
`(let ((v (cog-value (ConceptNode "${esc(atomName)}") (Predicate "last-fired-at")))) (if v (cog-value-ref v 0) 0))`)
const lastFired = parseFloat(lastFiredStr) || 0
if (lastFired > 0) {
const daysSinceFire = (Date.now() / 1000 - lastFired) / 86400
if (daysSinceFire > 30) {
// Gentle decay: -10% utility per 30 days of dormancy, floor 0.3
const decayFactor = Math.max(0.3, 1 - ((daysSinceFire - 30) / 30) * 0.1)
utility = utility * decayFactor
}
}
// Breadth: # files referenced (proxy for scope)
const breadth = Math.max(1, files.length)
const wisdom = utility * referent_health * Math.log(1 + breadth)
results.push({
atom: atomName,
kind: 'rule',
files: files.length,
existing: existingFiles.length,
referent_health,
breadth,
utility,
fires,
contradictions,
lastFiredDaysAgo: lastFired > 0 ? Math.round((Date.now() / 1000 - lastFired) / 86400) : null,
wisdom
})
// Queue for batch ingest
const node = `(ConceptNode "${esc(atomName)}")`
batch.push(`(cog-set-value! ${node} (Predicate "referent-health") (FloatValue ${referent_health}))`)
batch.push(`(cog-set-value! ${node} (Predicate "breadth") (FloatValue ${breadth}))`)
batch.push(`(cog-set-value! ${node} (Predicate "utility") (FloatValue ${utility}))`)
batch.push(`(cog-set-value! ${node} (Predicate "wisdom") (FloatValue ${wisdom}))`)
}
// Meta atoms: use 'approach-works' + 'approach-fails' or 'try-first' + 'avoid-first'
for (const atomName of metaAtoms) {
const bondTypes = atomName.startsWith('meta:pattern:')
? ['try-first', 'avoid-first']
: ['approach-works', 'approach-fails']
const files = await getFilesForAtom(atomName, bondTypes)
const existingFiles = files.filter(fileExists)
const referent_health = files.length > 0 ? existingFiles.length / files.length : 1.0
const firesStr = await evalScheme(
`(let ((v (cog-value (ConceptNode "${esc(atomName)}") (Predicate "utility-fires")))) (if v (cog-value-ref v 0) 0))`)
const contradStr = await evalScheme(
`(let ((v (cog-value (ConceptNode "${esc(atomName)}") (Predicate "utility-contradictions")))) (if v (cog-value-ref v 0) 0))`)
const fires = parseFloat(firesStr) || 0
const contradictions = parseFloat(contradStr) || 0
const utility = (fires + contradictions) === 0
? 1.0
: Math.max(0.05, (fires - contradictions) / (fires + contradictions) * 0.5 + 0.5)
const breadth = await computeBreadth(atomName)
const wisdom = utility * referent_health * Math.log(1 + breadth)
results.push({
atom: atomName,
kind: atomName.startsWith('meta:pattern:') ? 'pattern' : 'per-loop',
files: files.length,
existing: existingFiles.length,
referent_health,
breadth,
utility,
fires,
contradictions,
wisdom
})
const node = `(ConceptNode "${esc(atomName)}")`
batch.push(`(cog-set-value! ${node} (Predicate "referent-health") (FloatValue ${referent_health}))`)
batch.push(`(cog-set-value! ${node} (Predicate "breadth") (FloatValue ${breadth}))`)
batch.push(`(cog-set-value! ${node} (Predicate "utility") (FloatValue ${utility}))`)
batch.push(`(cog-set-value! ${node} (Predicate "wisdom") (FloatValue ${wisdom}))`)
}
// Report
const byWisdom = [...results].sort((a, b) => b.wisdom - a.wisdom)
console.log('\n── Wisdom Distribution ──')
const bins = { high: 0, med: 0, low: 0, zero: 0 }
const highThreshold = 1.0 // high-wisdom rules
const medThreshold = 0.3
for (const r of results) {
if (r.wisdom >= highThreshold) bins.high++
else if (r.wisdom >= medThreshold) bins.med++
else if (r.wisdom > 0) bins.low++
else bins.zero++
}
console.log(` high (wisdom ≥ 1.0): ${bins.high}`)
console.log(` med (0.3 ≤ wisdom < 1.0): ${bins.med}`)
console.log(` low (0 < wisdom < 0.3): ${bins.low}`)
console.log(` zero (wisdom = 0): ${bins.zero}`)
console.log('\n── Top 10 wisest rules ──')
for (const r of byWisdom.slice(0, 10)) {
const label = r.atom.length > 60 ? r.atom.slice(0, 60) + '…' : r.atom
console.log(` wisdom=${r.wisdom.toFixed(2)} (u=${r.utility.toFixed(2)} rh=${r.referent_health.toFixed(2)} br=${r.breadth}) ${label}`)
}
console.log('\n── Referent health issues (health < 0.5) ──')
const broken = results.filter(r => r.referent_health < 0.5 && r.files > 0).slice(0, 10)
for (const r of broken) {
const label = r.atom.length > 60 ? r.atom.slice(0, 60) + '…' : r.atom
console.log(` health=${r.referent_health.toFixed(2)} (${r.existing}/${r.files} files exist) ${label}`)
}
if (report) {
console.log('\n(--report mode: not storing)')
return { total: results.length, bins }
}
// Store as FloatValues
console.log(`\nStoring ${batch.length} wisdom values...`)
const BATCH = 50
let sent = 0
for (let i = 0; i < batch.length; i += BATCH) {
const slice = batch.slice(i, i + BATCH)
await evalScheme('(begin ' + slice.join(' ') + ')')
sent += slice.length
}
console.log(`Stored: ${sent} values`)
return { total: results.length, bins, sent }
}
if (require.main === module) {
const report = process.argv.includes('--report')
computeAll(report).then(result => {
console.log('\n' + JSON.stringify(result, null, 2))
}).catch(err => { console.error(err.message); process.exit(1) })
}
module.exports = { computeAll }