OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet / atomspace / query.js

758 lines · javascript ref:
/**
 * query.js — AtomSpace queries for code intelligence
 *
 * Three query functions that improve Claude's responses:
 *   blastRadius(filePath) — what's at risk if I edit this file?
 *   similarLoops(editedFiles) — what past loops match this situation?
 *   riskAssessment(editList) — aggregate risk across all edited files
 *
 * All functions return null if CogServer is not running (graceful fallback).
 */

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

/**
 * Parse a MeetLink result string into an array of atom names.
 * MeetLink returns: (SetLink (ConceptNode "x") (ConceptNode "y") ...)
 * or just: (ConceptNode "x") for single results
 */
function parseAtomNames (resultStr) {
  if (!resultStr) return []
  const names = []
  // CogServer prints either "ConceptNode" or abbreviated "Concept"
  const re = /(?:ConceptNode|Concept) "([^"]+)"/g
  let match
  while ((match = re.exec(resultStr)) !== null) {
    names.push(match[1])
  }
  return names
}

/**
 * Parse a float value result.
 */
function parseFloat_ (resultStr) {
  if (!resultStr) return 0
  const match = resultStr.match(/[\d.]+/)
  return match ? parseFloat(match[0]) : 0
}

/**
 * blastRadius(filePath) — What's at risk if I edit this file?
 *
 * Returns: {
 *   consumers: string[],      // files that import from this file
 *   dependencies: string[],   // files this file imports from
 *   loops: string[],          // loop fingerprints involving this file
 *   rules: string[],          // rule IDs applying to this file
 *   dangerLevel: string,      // "low"|"medium"|"high"
 *   module: string,           // which module this file belongs to
 * }
 */
async function blastRadius (filePath) {
  if (!await isCogServerUp()) return null

  // Normalize path — strip leading slashes, common prefixes
  const normalPath = filePath
    .replace(/^\/storage\/projects\/code\/hypercore-kb\/hyper-fleeky\//, '')
    .replace(/^\/storage\/projects\/code\/hypercore-kb\//, '')

  const [consumers, dependencies, loops, rules, danger] = await Promise.all([
    evalScheme(S.queryImporters(normalPath)),
    evalScheme(S.queryDependencies(normalPath)),
    evalScheme(S.queryLoopsForFile(normalPath)),
    evalScheme(S.queryRulesForFile(normalPath)),
    evalScheme(S.getStringValue(S.fileNode(normalPath), 'danger-level'))
  ])

  // Sort rules by wisdom score (high-wisdom first), filter out zero-wisdom
  const ruleNames = parseAtomNames(rules)
  const rulesWithWisdom = await Promise.all(ruleNames.map(async r => ({
    name: r,
    wisdom: await getWisdom(r)
  })))
  // Keep rules even if wisdom is 0 — still visible, just ranked lower
  const rankedRules = rulesWithWisdom
    .sort((a, b) => b.wisdom - a.wisdom)
    .map(r => r.name)

  return {
    file: normalPath,
    consumers: parseAtomNames(consumers),
    dependencies: parseAtomNames(dependencies),
    loops: parseAtomNames(loops),
    rules: rankedRules,  // sorted by wisdom
    rulesHighWisdom: rulesWithWisdom.filter(r => r.wisdom >= 1.0).length,
    rulesLowWisdom: rulesWithWisdom.filter(r => r.wisdom < 0.3).length,
    dangerLevel: (danger && danger !== '""' && danger !== '') ? danger.replace(/"/g, '') : 'unknown'
  }
}

/**
 * Get the wisdom score for a rule or meta atom.
 * Returns 0.5 if no wisdom value stored (neutral default).
 */
async function getWisdom (atomName) {
  const result = await evalScheme(
    `(let ((v (cog-value (ConceptNode "${atomName.replace(/"/g, '\\"')}") (Predicate "wisdom")))) (if v (cog-value-ref v 0) 0.5))`)
  return parseFloat(result) || 0.5
}

/**
 * similarLoops(editedFiles) — What past loops match this situation?
 *
 * Finds loops that share file overlap with the currently edited files.
 * Scored by overlap count.
 *
 * Returns: {
 *   matches: [{ loopId, sharedFiles, score, type, project, resolved }]
 * }
 */
async function similarLoops (editedFiles) {
  if (!await isCogServerUp()) return null
  if (!editedFiles || !editedFiles.length) return { matches: [] }

  // For each edited file, find loops
  const allLoops = new Map() // loopId → { sharedFiles, type, project, resolved }

  for (const file of editedFiles) {
    const normalPath = file
      .replace(/^\/storage\/projects\/code\/hypercore-kb\/hyper-fleeky\//, '')
      .replace(/^\/storage\/projects\/code\/hypercore-kb\//, '')

    const result = await evalScheme(S.queryLoopsForFile(normalPath))
    const loopNames = parseAtomNames(result)

    for (const loopName of loopNames) {
      const id = loopName.replace('loop:', '')
      if (!allLoops.has(id)) {
        // Get loop metadata
        const node = S.loopNode(id)
        const [type, project, resolved] = await Promise.all([
          evalScheme(S.getStringValue(node, 'type')),
          evalScheme(S.getStringValue(node, 'project')),
          evalScheme(S.getFloatValue(node, 'resolved'))
        ])
        allLoops.set(id, {
          loopId: id,
          sharedFiles: [normalPath],
          type: (type || '').replace(/"/g, ''),
          project: (project || '').replace(/"/g, ''),
          resolved: parseFloat_(resolved) > 0.5
        })
      } else {
        allLoops.get(id).sharedFiles.push(normalPath)
      }
    }
  }

  // Score by shared file count and sort
  const matches = [...allLoops.values()]
    .map(m => ({ ...m, score: m.sharedFiles.length }))
    .sort((a, b) => b.score - a.score)
    .slice(0, 10)

  return { matches }
}

/**
 * riskAssessment(editList) — Aggregate risk across all edited files
 *
 * Returns: {
 *   risk: "low"|"medium"|"high",
 *   totalDownstream: number,
 *   totalLoops: number,
 *   totalRules: number,
 *   crossModuleEdits: string[],
 *   hotspots: [{ file, consumers, loops, danger }]
 * }
 */
async function riskAssessment (editList) {
  if (!await isCogServerUp()) return null
  if (!editList || !editList.length) return { risk: 'low', totalDownstream: 0, totalLoops: 0, totalRules: 0, crossModuleEdits: [], hotspots: [] }

  const hotspots = []
  let totalDownstream = 0
  let totalLoops = 0
  let totalRules = 0
  const modules = new Set()
  const crossModuleEdits = []

  for (const file of editList) {
    const br = await blastRadius(file)
    if (!br) continue

    // Track module boundaries
    const parts = br.file.split('/')
    if (parts.length > 0) {
      const mod = parts[0]
      if (modules.size > 0 && !modules.has(mod)) {
        crossModuleEdits.push(br.file)
      }
      modules.add(mod)
    }

    totalDownstream += br.consumers.length
    totalLoops += br.loops.length
    totalRules += br.rules.length

    if (br.consumers.length > 0 || br.loops.length > 0 || br.dangerLevel !== 'unknown') {
      hotspots.push({
        file: br.file,
        consumers: br.consumers.length,
        loops: br.loops.length,
        danger: br.dangerLevel
      })
    }
  }

  // Determine overall risk
  let risk = 'low'
  if (totalLoops > 3 || crossModuleEdits.length > 0 || hotspots.some(h => h.danger === 'high')) {
    risk = 'high'
  } else if (totalLoops > 0 || totalDownstream > 5 || hotspots.some(h => h.danger === 'medium')) {
    risk = 'medium'
  }

  return {
    risk,
    totalDownstream,
    totalLoops,
    totalRules,
    crossModuleEdits,
    hotspots: hotspots.sort((a, b) => (b.loops + b.consumers) - (a.loops + a.consumers)).slice(0, 5)
  }
}

/**
 * Format a blast radius result as a human-readable briefing string.
 */
function formatBlastRadius (br) {
  if (!br) return ''
  const lines = []
  if (br.consumers.length) lines.push(`Downstream consumers (${br.consumers.length}): ${br.consumers.slice(0, 5).map(c => c.replace('file:', '')).join(', ')}`)
  if (br.loops.length) lines.push(`Past correction loops (${br.loops.length}): ${br.loops.slice(0, 3).join(', ')}`)
  if (br.rules.length) lines.push(`Applicable rules: ${br.rules.length}`)
  if (br.dangerLevel !== 'unknown') lines.push(`Danger level: ${br.dangerLevel}`)
  return lines.join('\n')
}

/**
 * fileContext(filePath) — Language/domain/role-aware context for a file
 *
 * Returns: {
 *   file, language, domain, role,
 *   exports: number,     // count of things this file exports
 *   isExportedAPI: bool, // true if file has exports
 *   avgLoopsForLanguage: number,  // correction density for this language
 *   dominantLoopType: string,     // most common failure mode
 * }
 */
async function fileContext (filePath) {
  if (!await isCogServerUp()) return null

  const normalPath = filePath
    .replace(/^\/storage\/projects\/code\/hypercore-kb\/hyper-fleeky\//, '')
    .replace(/^\/storage\/projects\/code\/hypercore-kb\//, '')

  const [language, domain, role, exportStr] = await Promise.all([
    evalScheme(S.getStringValue(S.fileNode(normalPath), 'language')),
    evalScheme(S.getStringValue(S.fileNode(normalPath), 'domain')),
    evalScheme(S.getStringValue(S.fileNode(normalPath), 'role')),
    evalScheme(`(length (cog-execute! (Meet (TypedVariable (Variable "$e") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "exports") (ListLink (ConceptNode "file:${normalPath}") (Variable "$e")))))))`)
  ])

  const cleanStr = (s) => (s || '').replace(/"/g, '').trim()
  const lang = cleanStr(language) || 'unknown'
  const dom = cleanStr(domain) || 'unknown'
  const rl = cleanStr(role) || 'unknown'
  const exports = parseInt(exportStr) || 0

  return {
    file: normalPath,
    language: lang,
    domain: dom,
    role: rl,
    exports,
    isExportedAPI: exports > 0
  }
}

/**
 * findResolution(filePath, loopType) — What fixed similar past loops?
 *
 * Queries loops matching this file's language+domain+type. Returns
 * the resolution files from those loops — "last time, the fix was in X."
 *
 * Returns: {
 *   matches: [{ loopId, project, resolutionFiles, corrections }]
 * }
 */
async function findResolution (filePath, loopType) {
  if (!await isCogServerUp()) return null

  const ctx = await fileContext(filePath)
  if (!ctx) return null

  // Find loops in the same domain with similar type
  // (We can't query by language/domain on loops directly — they're on files)
  // So: find loops in files that share language+domain with this one

  await evalScheme('(use-modules (srfi srfi-1))')

  // Get all loops that touch files in the same domain
  const allLoopsStr = await evalScheme(
    '(string-join (map cog-name (cog-get-atoms (quote ConceptNode))) "|")')
  const allAtoms = (allLoopsStr || '').replace(/^"|"$/g, '').split('|').filter(s => s.startsWith('loop:'))

  const matches = []
  for (const loopName of allAtoms.slice(0, 100)) {  // limit for performance
    // Get loop's type and project
    const [type, project, resolved] = await Promise.all([
      evalScheme(`(let ((v (cog-value (ConceptNode "${loopName}") (Predicate "type")))) (if v (cog-value-ref v 0) ""))`),
      evalScheme(`(let ((v (cog-value (ConceptNode "${loopName}") (Predicate "project")))) (if v (cog-value-ref v 0) ""))`),
      evalScheme(`(let ((v (cog-value (ConceptNode "${loopName}") (Predicate "resolved")))) (if v (cog-value-ref v 0) 0))`)
    ])

    const cleanType = (type || '').replace(/"/g, '')
    const cleanProject = (project || '').replace(/"/g, '')
    const isResolved = parseFloat(resolved) > 0.5

    // Match if type matches or project matches
    if (!isResolved) continue
    if (loopType && cleanType !== loopType) continue

    // Get resolution files
    const resolutionStr = await evalScheme(
      `(cog-execute! (Meet (TypedVariable (Variable "$f") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "resolved-by") (ListLink (ConceptNode "${loopName}") (Variable "$f"))))))`)
    const resolutionFiles = parseAtomNames(resolutionStr)

    if (resolutionFiles.length > 0) {
      matches.push({
        loopId: loopName.replace('loop:', ''),
        type: cleanType,
        project: cleanProject,
        resolutionFiles: resolutionFiles.map(f => f.replace('file:', ''))
      })
    }
  }

  return {
    context: ctx,
    matches: matches.slice(0, 5)
  }
}

/**
 * unfamiliarityScore(filePath) — How much experience do I have with this module?
 *
 * Queries session-count on the parent module. Low session count + high
 * correction density = unfamiliarity risk.
 */
async function unfamiliarityScore (filePath) {
  if (!await isCogServerUp()) return null

  const ctx = await fileContext(filePath)
  if (!ctx) return null

  // Extract module from path (first segment)
  const modName = ctx.file.split('/')[0]
  if (!modName) return null

  const sessionStr = await evalScheme(
    `(let ((v (cog-value (ConceptNode "mod:${modName}") (Predicate "session-count")))) (if v (cog-value-ref v 0) 0))`)
  const sessions = parseFloat(sessionStr) || 0

  let level = 'unknown'
  if (sessions === 0) level = 'never-visited'
  else if (sessions < 3) level = 'unfamiliar'
  else if (sessions < 10) level = 'familiar'
  else level = 'well-known'

  return { module: modName, sessions, familiarity: level }
}

/**
 * Enhanced blastRadius — adds export weighting
 */
async function blastRadiusEnhanced (filePath) {
  const br = await blastRadius(filePath)
  if (!br) return null

  const ctx = await fileContext(filePath)
  const unfam = await unfamiliarityScore(filePath)

  // Compute weighted risk score
  let riskScore = br.loops.length * 3 + br.rules.length + br.consumers.length * 2
  if (ctx && ctx.isExportedAPI) riskScore += 5  // exports amplify blast radius
  if (unfam && (unfam.familiarity === 'unfamiliar' || unfam.familiarity === 'never-visited')) riskScore += 3

  return {
    ...br,
    ...ctx,
    ...unfam,
    riskScore
  }
}

/**
 * contrastGuidance(filePath) — Contrast-derived meta-cognition for a file
 *
 * Finds meta-cognition atoms where this file is either in the approach-works
 * or approach-fails position. Also finds cross-loop patterns for the file's
 * project+language context.
 *
 * Returns: {
 *   worksFor: [{ loopId, context, correction }],   // past cases where this file fixed things
 *   failedFor: [{ loopId, context, correction }],  // past cases where editing this file was wasted
 *   patterns: [{ context, tryFirst: [files], avoidFirst: [files], loopCount }]
 * }
 */
async function contrastGuidance (filePath) {
  if (!await isCogServerUp()) return null

  const normalPath = filePath
    .replace(/^\/storage\/projects\/code\/hypercore-kb\/hyper-fleeky\//, '')
    .replace(/^\/storage\/projects\/code\/hypercore-kb\//, '')
    .replace(/^\/storage\/projects\/code\//, '')

  const fileAtom = `(ConceptNode "file:${normalPath.replace(/"/g, '\\"')}")`

  // Find meta-atoms where this file is a successful approach
  const worksForStr = await evalScheme(
    `(cog-execute! (Meet (TypedVariable (Variable "$m") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "approach-works") (ListLink (Variable "$m") ${fileAtom})))))`)

  // Find meta-atoms where this file was wasted effort
  const failedForStr = await evalScheme(
    `(cog-execute! (Meet (TypedVariable (Variable "$m") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "approach-fails") (ListLink (Variable "$m") ${fileAtom})))))`)

  const worksMetaIds = parseAtomNames(worksForStr).filter(n => n.startsWith('meta:loop:'))
  const failedMetaIds = parseAtomNames(failedForStr).filter(n => n.startsWith('meta:loop:'))

  // Get context info for each meta-atom
  async function getMetaContext (metaId) {
    const [project, loopType, correction] = await Promise.all([
      evalScheme(`(let ((v (cog-value (ConceptNode "${metaId}") (Predicate "project")))) (if v (cog-value-ref v 0) ""))`),
      evalScheme(`(let ((v (cog-value (ConceptNode "${metaId}") (Predicate "loop-type")))) (if v (cog-value-ref v 0) ""))`),
      evalScheme(`(let ((v (cog-value (ConceptNode "${metaId}") (Predicate "correction-snippet")))) (if v (cog-value-ref v 0) ""))`)
    ])
    return {
      loopId: metaId.replace('meta:loop:', ''),
      project: (project || '').replace(/"/g, ''),
      loopType: (loopType || '').replace(/"/g, ''),
      correction: (correction || '').replace(/"/g, '')
    }
  }

  const worksFor = await Promise.all(worksMetaIds.slice(0, 5).map(getMetaContext))
  const failedFor = await Promise.all(failedMetaIds.slice(0, 5).map(getMetaContext))

  // Find cross-loop patterns whose context applies to this file
  // Need the file's language + the project it's in
  const fileCtx = await fileContext(filePath)

  const patterns = []
  if (fileCtx && fileCtx.language && fileCtx.language !== 'unknown') {
    // Extract project from path
    const parts = normalPath.split('/')
    const project = parts[0]

    // Query patterns matching this context
    const patternKeys = [
      `meta:pattern:${project}:${fileCtx.language}:*`,  // broad
    ]

    for (const key of patternKeys) {
      const patternNode = `(ConceptNode "${key}")`
      const existsStr = await evalScheme(`(cog-node (quote ConceptNode) "${key}")`)
      if (!existsStr || existsStr === 'false' || existsStr === '#f') continue

      const tryFirstStr = await evalScheme(
        `(cog-execute! (Meet (TypedVariable (Variable "$f") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "try-first") (ListLink ${patternNode} (Variable "$f"))))))`)
      const avoidStr = await evalScheme(
        `(cog-execute! (Meet (TypedVariable (Variable "$f") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "avoid-first") (ListLink ${patternNode} (Variable "$f"))))))`)
      const loopCountStr = await evalScheme(
        `(let ((v (cog-value ${patternNode} (Predicate "loop-count")))) (if v (cog-value-ref v 0) 0))`)

      const tryFirst = parseAtomNames(tryFirstStr).map(n => n.replace('file:', ''))
      const avoidFirst = parseAtomNames(avoidStr).map(n => n.replace('file:', ''))

      if (tryFirst.length > 0 || avoidFirst.length > 0) {
        patterns.push({
          context: key.replace('meta:pattern:', ''),
          tryFirst,
          avoidFirst,
          loopCount: parseFloat(loopCountStr) || 0
        })
      }
    }
  }

  return {
    file: normalPath,
    worksFor: worksFor.filter(Boolean),
    failedFor: failedFor.filter(Boolean),
    patterns
  }
}

/**
 * liveSessionContext(filePath) — Live events in the current session for a file
 *
 * Returns: {
 *   sessionId,
 *   sessionEditedFiles: number,      // how many files edited this session
 *   editsForThisFile: number,        // times this file edited this session
 *   correctionsNearThisFile: number, // corrections that happened near edits to this file
 *   recentCorrections: [{ text, turn }] // sample corrections
 * }
 *
 * If no active session, returns null.
 */
async function liveSessionContext (filePath) {
  if (!await isCogServerUp()) return null

  const fs = require('fs')
  let sessionId = null
  try { sessionId = fs.readFileSync('/tmp/claude-atomspace-session', 'utf8').trim() } catch {}
  if (!sessionId) return null

  const normalPath = filePath
    .replace(/^\/storage\/projects\/code\/hypercore-kb\/hyper-fleeky\//, '')
    .replace(/^\/storage\/projects\/code\/hypercore-kb\//, '')
    .replace(/^\/storage\/projects\/code\//, '')

  const sessionNode = `(ConceptNode "session:${sessionId}")`
  const fileAtom = `(ConceptNode "file:${normalPath.replace(/"/g, '\\"')}")`

  // Files edited this session
  const editedStr = await evalScheme(
    `(cog-execute! (Meet (TypedVariable (Variable "$f") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "session-edited") (ListLink ${sessionNode} (Variable "$f"))))))`)
  const editedFiles = parseAtomNames(editedStr)

  // Is THIS file in the session-edited set?
  const isThisEdited = editedFiles.includes(`file:${normalPath}`)
  const fileEdits = isThisEdited ? [fileAtom] : []

  // All corrections in the session
  const allCorrectionsStr = await evalScheme(
    `(cog-execute! (Meet (TypedVariable (Variable "$c") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "correction-in-session") (ListLink (Variable "$c") ${sessionNode})))))`)
  const allCorrectionIds = parseAtomNames(allCorrectionsStr)

  // Filter to corrections near edit of this specific file
  const correctionIds = []
  for (const cid of allCorrectionIds) {
    const nearEdit = await evalScheme(
      `(cog-link (quote EdgeLink) (BondNode "correction-near-edit") (ListLink (ConceptNode "${cid}") ${fileAtom}))`)
    if (nearEdit && nearEdit !== 'false' && !nearEdit.includes('#f')) {
      correctionIds.push(cid)
    }
  }

  // Get text for each correction
  const recentCorrections = []
  for (const cid of correctionIds.slice(0, 3)) {
    const [text, turn] = await Promise.all([
      evalScheme(`(let ((v (cog-value (ConceptNode "${cid}") (Predicate "text")))) (if v (cog-value-ref v 0) ""))`),
      evalScheme(`(let ((v (cog-value (ConceptNode "${cid}") (Predicate "turn")))) (if v (cog-value-ref v 0) 0))`)
    ])
    recentCorrections.push({
      text: (text || '').replace(/"/g, '').slice(0, 150),
      turn: parseFloat(turn) || 0
    })
  }

  return {
    sessionId,
    sessionEditedFiles: editedFiles.length,
    editsForThisFile: fileEdits.length,
    correctionsNearThisFile: correctionIds.length,
    totalSessionCorrections: allCorrectionIds.length,
    recentCorrections
  }
}

/**
 * relevantAtoms(context) — Top-k wisdom-ranked meta-cognition for current context
 *
 * context: {
 *   cwd?: string,           // current working directory
 *   files?: string[],       // files mentioned in prompt or recently edited
 *   module?: string,        // module name from cwd
 *   language?: string,      // inferred language of work area
 * }
 *
 * Returns: {
 *   tier1: [{ atom, wisdom, summary }],  // top 3, shown prominently
 *   tier2: [{ atom, wisdom, summary }],  // next 2, abbreviated
 *   totalMatched: number,                 // how many matched total
 *   totalTokens: number                   // estimated tokens in all briefing
 * }
 */
async function relevantAtoms (context) {
  if (!await isCogServerUp()) return null

  const files = context.files || []
  const module = context.module
  const language = context.language

  const candidates = new Map()  // atomName → { atom, wisdom, matchReason }

  // 1. Rules and meta atoms directly for the listed files
  for (const file of files) {
    const normalPath = file
      .replace(/^\/storage\/projects\/code\/hypercore-kb\/hyper-fleeky\//, '')
      .replace(/^\/storage\/projects\/code\/hypercore-kb\//, '')
      .replace(/^\/storage\/projects\/code\//, '')
    if (!normalPath) continue
    const fileAtom = `(ConceptNode "file:${normalPath.replace(/"/g, '\\"')}")`

    // Rules for this file
    const ruleResult = await evalScheme(
      `(cog-execute! (Meet (TypedVariable (Variable "$r") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "rule-for-file") (ListLink (Variable "$r") ${fileAtom})))))`)
    for (const name of parseAtomNames(ruleResult)) {
      if (!name.startsWith('rule:')) continue
      if (!candidates.has(name)) candidates.set(name, { atom: name, matchReason: `file: ${normalPath}` })
    }

    // Per-loop meta atoms linked via approach-works or approach-fails
    const metaResult = await evalScheme(
      `(cog-execute! (Meet (TypedVariable (Variable "$m") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "approach-works") (ListLink (Variable "$m") ${fileAtom})))))`)
    for (const name of parseAtomNames(metaResult)) {
      if (!name.startsWith('meta:')) continue
      if (!candidates.has(name)) candidates.set(name, { atom: name, matchReason: `approach-works for ${normalPath}` })
    }
  }

  // 2. Cross-loop patterns for the module + language
  if (module && language && language !== 'unknown') {
    const patternKeys = [
      `meta:pattern:${module}:${language}:*`,  // broad
    ]
    for (const key of patternKeys) {
      const existsStr = await evalScheme(`(cog-node (quote ConceptNode) "${key}")`)
      if (!existsStr || existsStr === 'false' || existsStr === '#f' || !existsStr.includes('Concept')) continue
      if (!candidates.has(key)) candidates.set(key, { atom: key, matchReason: `pattern for ${module}:${language}` })
    }
  }

  // 3. Fetch wisdom for all candidates, sort
  const scored = []
  for (const [name, info] of candidates) {
    const wisdom = await getWisdom(name)
    scored.push({ ...info, wisdom })
  }
  scored.sort((a, b) => b.wisdom - a.wisdom)

  // 4. Fetch human-readable summaries for top candidates
  async function getSummary (atomName) {
    const node = `(ConceptNode "${atomName.replace(/"/g, '\\"')}")`
    if (atomName.startsWith('rule:')) {
      const [symptom, fix] = await Promise.all([
        evalScheme(`(let ((v (cog-value ${node} (Predicate "symptom")))) (if v (cog-value-ref v 0) ""))`),
        evalScheme(`(let ((v (cog-value ${node} (Predicate "fix")))) (if v (cog-value-ref v 0) ""))`)
      ])
      const s = (symptom || '').replace(/"/g, '').trim()
      const f = (fix || '').replace(/"/g, '').trim()
      if (f && s) return `When: ${s.slice(0, 100)} → fix: ${f.slice(0, 100)}`
      if (f) return `Fix pattern: ${f.slice(0, 150)}`
      return 'Rule (no description)'
    }
    if (atomName.startsWith('meta:loop:')) {
      const correction = await evalScheme(`(let ((v (cog-value ${node} (Predicate "correction-snippet")))) (if v (cog-value-ref v 0) ""))`)
      return `Past correction: "${(correction || '').replace(/"/g, '').slice(0, 100)}..."`
    }
    if (atomName.startsWith('meta:pattern:')) {
      // Get try-first and avoid-first files
      const tryStr = await evalScheme(
        `(cog-execute! (Meet (TypedVariable (Variable "$f") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "try-first") (ListLink ${node} (Variable "$f"))))))`)
      const tryFiles = parseAtomNames(tryStr).map(n => n.replace('file:', '').split('/').pop()).slice(0, 2)
      const avoidStr = await evalScheme(
        `(cog-execute! (Meet (TypedVariable (Variable "$f") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "avoid-first") (ListLink ${node} (Variable "$f"))))))`)
      const avoidFiles = parseAtomNames(avoidStr).map(n => n.replace('file:', '').split('/').pop()).slice(0, 2)
      const ctx = atomName.replace('meta:pattern:', '')
      let parts = []
      if (tryFiles.length) parts.push(`try ${tryFiles.join(', ')}`)
      if (avoidFiles.length) parts.push(`avoid ${avoidFiles.join(', ')}`)
      return `[${ctx}] ${parts.join('; ')}`
    }
    return atomName
  }

  const totalMatched = scored.length
  // Tier 1: top 3 by wisdom
  const tier1 = []
  for (const c of scored.slice(0, 3)) {
    tier1.push({ ...c, summary: await getSummary(c.atom) })
  }
  // Tier 2: next 2
  const tier2 = []
  for (const c of scored.slice(3, 5)) {
    tier2.push({ ...c, summary: await getSummary(c.atom) })
  }

  // Estimate tokens (rough: 1 token ≈ 4 chars)
  const totalChars = [...tier1, ...tier2].reduce((acc, c) => acc + (c.summary || '').length, 0)
  const totalTokens = Math.ceil(totalChars / 4)

  return { tier1, tier2, totalMatched, totalTokens }
}

/**
 * preferredApproachFor(filePath) — Find preferred approach rules for editing this file
 *
 * Returns: [{ id, shape, successCount, sessionCount, avgScore, secondaryFiles, sampleApproval }]
 * Sorted by successCount desc.
 */
async function preferredApproachFor (filePath) {
  if (!await isCogServerUp()) return null

  const normalPath = filePath
    .replace(/^\/storage\/projects\/code\/hypercore-kb\/hyper-fleeky\//, '')
    .replace(/^\/storage\/projects\/code\/hypercore-kb\//, '')
    .replace(/^\/storage\/projects\/code\//, '')

  const fileAtom = `(ConceptNode "file:${normalPath.replace(/"/g, '\\"')}")`

  const result = await evalScheme(
    `(cog-execute! (Meet (TypedVariable (Variable "$p") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "preferred-for-file") (ListLink (Variable "$p") ${fileAtom})))))`)

  const approachIds = parseAtomNames(result).filter(n => n.startsWith('preferred:'))
  const approaches = []
  for (const pid of approachIds.slice(0, 5)) {
    const node = `(ConceptNode "${pid.replace(/"/g, '\\"')}")`
    const [shape, count, avgScore, sample] = await Promise.all([
      evalScheme(`(let ((v (cog-value ${node} (Predicate "shape")))) (if v (cog-value-ref v 0) ""))`),
      evalScheme(`(let ((v (cog-value ${node} (Predicate "success-count")))) (if v (cog-value-ref v 0) 0))`),
      evalScheme(`(let ((v (cog-value ${node} (Predicate "avg-score")))) (if v (cog-value-ref v 0) 0))`),
      evalScheme(`(let ((v (cog-value ${node} (Predicate "sample-approval")))) (if v (cog-value-ref v 0) ""))`)
    ])
    approaches.push({
      id: pid,
      shape: (shape || '').replace(/"/g, ''),
      successCount: parseFloat(count) || 0,
      avgScore: parseFloat(avgScore) || 0,
      sampleApproval: (sample || '').replace(/"/g, '').slice(0, 100)
    })
  }
  approaches.sort((a, b) => b.successCount - a.successCount)
  return approaches
}

module.exports = {
  blastRadius,
  blastRadiusEnhanced,
  similarLoops,
  riskAssessment,
  formatBlastRadius,
  fileContext,
  findResolution,
  unfamiliarityScore,
  contrastGuidance,
  liveSessionContext,
  relevantAtoms,
  getWisdom,
  preferredApproachFor
}