/**
* fire-log.js — Append-only log of rule firings
*
* Every time a hook injects a rule/meta-atom into context, record it here.
* Enables downstream utility tracking and live dashboard display.
*
* Format: JSONL, one fire per line
* { timestamp, session, hook, atom, atomType, wisdom, context, trigger }
*/
const fs = require('fs')
const path = require('path')
const LOG_FILE = path.join(__dirname, '..', 'data', 'rule-fires.jsonl')
const MAX_LINES = 10000 // Rotate when exceeded
function logFire (entry) {
try {
const record = {
timestamp: new Date().toISOString(),
session: entry.session || null,
hook: entry.hook || 'unknown',
atom: entry.atom,
atomType: entry.atomType || inferType(entry.atom),
wisdom: entry.wisdom != null ? entry.wisdom : null,
context: entry.context || null, // file path or prompt excerpt
trigger: entry.trigger || null // what caused this fire (file, prompt, etc.)
}
fs.appendFileSync(LOG_FILE, JSON.stringify(record) + '\n')
// Phase 4.2: Update last-fired-at and utility-fires counter on atom
// Non-blocking, best-effort. Feeds time-decay calculation in wisdom-score.
if (entry.atom && entry.session) {
setImmediate(() => {
try {
const { evalScheme, isCogServerUp } = require('./cogserver')
isCogServerUp().then(up => {
if (!up) return
const ts = Math.floor(Date.now() / 1000)
const atomName = entry.atom.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
evalScheme(
`(cog-set-value! (ConceptNode "${atomName}") (Predicate "last-fired-at") (FloatValue ${ts}))`
).catch(() => {})
evalScheme(
`(cog-set-value! (ConceptNode "${atomName}") (Predicate "utility-fires") ` +
`(FloatValue (+ 1 (let ((v (cog-value (ConceptNode "${atomName}") (Predicate "utility-fires")))) ` +
`(if v (cog-value-ref v 0) 0)))))`
).catch(() => {})
})
} catch {}
})
}
// Rotate if too large
try {
const stats = fs.statSync(LOG_FILE)
if (stats.size > 5 * 1024 * 1024) { // >5MB
const content = fs.readFileSync(LOG_FILE, 'utf8')
const lines = content.trim().split('\n')
const keep = lines.slice(-MAX_LINES)
fs.writeFileSync(LOG_FILE, keep.join('\n') + '\n')
}
} catch {}
} catch {
// Silent — never block a hook
}
}
function inferType (atom) {
if (!atom) return 'unknown'
if (atom.startsWith('rule:')) return 'rule'
if (atom.startsWith('meta:pattern:')) return 'pattern'
if (atom.startsWith('meta:loop:')) return 'meta-loop'
return 'other'
}
/**
* Read recent fires (for dashboard/querying).
*/
function readRecent (n = 50) {
try {
if (!fs.existsSync(LOG_FILE)) return []
const content = fs.readFileSync(LOG_FILE, 'utf8')
const lines = content.trim().split('\n').filter(Boolean)
return lines.slice(-n).map(l => { try { return JSON.parse(l) } catch { return null } }).filter(Boolean)
} catch { return [] }
}
/**
* Read fires within a time window (ms).
*/
function readSince (sinceMs) {
try {
if (!fs.existsSync(LOG_FILE)) return []
const cutoff = Date.now() - sinceMs
const content = fs.readFileSync(LOG_FILE, 'utf8')
const lines = content.trim().split('\n').filter(Boolean)
const parsed = lines.map(l => { try { return JSON.parse(l) } catch { return null } }).filter(Boolean)
return parsed.filter(p => {
const t = new Date(p.timestamp).getTime()
return t >= cutoff
})
} catch { return [] }
}
module.exports = { logFire, readRecent, readSince, LOG_FILE }