OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet / atomspace / ingest-ratchet.js

244 lines · javascript ref:
/**
 * ingest-ratchet.js — Load ratchet correction data into AtomSpace
 *
 * Reads loops.json, learned-rules.json, project-maps.json and creates:
 *   - Loop nodes (loop:FINGERPRINT) with type, project, duration values
 *   - Rule nodes (rule:ID) with symptom, fix values
 *   - EdgeLinks: loop-in-file, resolved-by, rule-for-file
 *   - Danger level values on file nodes
 *
 * Usage:
 *   node ingest-ratchet.js
 */

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

const DATA_DIR = path.resolve(__dirname, '../data')

function readJSON (filename) {
  const filepath = path.join(DATA_DIR, filename)
  if (!fs.existsSync(filepath)) return null
  return JSON.parse(fs.readFileSync(filepath, 'utf8'))
}

/**
 * Convert loops.json to Scheme expressions.
 */
function loopsToScheme (loopsData) {
  const exprs = []
  if (!loopsData || !loopsData.loops) return exprs

  for (const loop of loopsData.loops) {
    const fp = loop.fingerprint || loop.id || ''
    if (!fp) continue

    const node = S.loopNode(fp)
    exprs.push(node)

    if (loop.type) exprs.push(S.setStringValue(node, 'type', loop.type))
    if (loop.project) exprs.push(S.setStringValue(node, 'project', loop.project))
    if (loop.duration) exprs.push(S.setFloatValue(node, 'duration', loop.duration))
    if (loop.date) exprs.push(S.setStringValue(node, 'date', loop.date))
    exprs.push(S.setFloatValue(node, 'resolved', loop.resolved ? 1 : 0))

    // Correction count
    if (loop.corrections) {
      exprs.push(S.setFloatValue(node, 'correction-count', loop.corrections.length))
    }

    // Files tried — create loop-in-file edges
    if (loop.filesTried) {
      for (const file of loop.filesTried) {
        const fNode = S.fileNode(normalizeRatchetPath(file, loop.project))
        exprs.push(fNode)
        exprs.push(S.edge('loop-in-file', node, fNode))
      }
    }

    // Resolution files — create resolved-by edges
    if (loop.resolutionFiles) {
      for (const file of loop.resolutionFiles) {
        const fNode = S.fileNode(normalizeRatchetPath(file, loop.project))
        exprs.push(fNode)
        exprs.push(S.edge('resolved-by', node, fNode))
      }
    }
  }

  return exprs
}

/**
 * Convert learned-rules.json to Scheme expressions.
 */
function rulesToScheme (rulesData) {
  const exprs = []
  if (!rulesData) return exprs

  const rules = Array.isArray(rulesData) ? rulesData : (rulesData.rules || [])

  for (const rule of rules) {
    const id = rule.id || ''
    if (!id) continue

    const node = S.ruleNode(id)
    exprs.push(node)

    if (rule.quality) exprs.push(S.setStringValue(node, 'quality', rule.quality))

    // Symptom text
    if (rule.trigger && rule.trigger.symptom) {
      exprs.push(S.setStringValue(node, 'symptom', rule.trigger.symptom.slice(0, 200)))
    }

    // Fix description
    if (rule.action && rule.action.description) {
      exprs.push(S.setStringValue(node, 'fix', rule.action.description.slice(0, 200)))
    }

    // Antipattern
    if (rule.antipattern && rule.antipattern.description) {
      exprs.push(S.setStringValue(node, 'antipattern', rule.antipattern.description.slice(0, 200)))
    }

    // Files the rule applies to
    const ruleFiles = []
    if (rule.trigger && rule.trigger.files) ruleFiles.push(...rule.trigger.files)
    if (rule.action && rule.action.fixFiles) ruleFiles.push(...rule.action.fixFiles)

    for (const file of [...new Set(ruleFiles)]) {
      const project = rule.trigger ? rule.trigger.project : ''
      const fNode = S.fileNode(normalizeRatchetPath(file, project))
      exprs.push(fNode)
      exprs.push(S.edge('rule-for-file', node, fNode))
    }
  }

  return exprs
}

/**
 * Convert project-maps.json to danger-level values on file nodes.
 */
function projectMapsToScheme (mapsData) {
  const exprs = []
  if (!mapsData || !mapsData.projects) return exprs

  for (const [projectName, project] of Object.entries(mapsData.projects)) {
    if (!project.subsystems) continue

    for (const [subsysName, subsys] of Object.entries(project.subsystems)) {
      if (!subsys.dangerLevel) continue

      // Create a node for the subsystem
      const subsysPath = projectName + '/' + subsysName
      const fNode = S.fileNode(subsysPath)
      exprs.push(fNode)
      exprs.push(S.setStringValue(fNode, 'danger-level', subsys.dangerLevel))
      exprs.push(S.setFloatValue(fNode, 'loop-count', subsys.loopCount || 0))
      exprs.push(S.setFloatValue(fNode, 'correction-count', subsys.correctionCount || 0))

      // Individual files in the subsystem
      if (subsys.files) {
        for (const file of subsys.files) {
          const fileFNode = S.fileNode(normalizeRatchetPath(file, projectName))
          exprs.push(fileFNode)
          exprs.push(S.setStringValue(fileFNode, 'danger-level', subsys.dangerLevel))
          // Subsystem contains file
          exprs.push(S.edge('contains', fNode, fileFNode))
        }
      }
    }
  }

  return exprs
}

/**
 * Normalize a ratchet file path to match codetree conventions.
 * Ratchet paths may be absolute or relative to project root.
 */
function normalizeRatchetPath (filePath, project) {
  if (!filePath) return ''
  // Strip absolute prefix if present
  let normalized = filePath
    .replace(/^\/storage\/projects\/code\/hypercore-kb\/hyper-fleeky\//, '')
    .replace(/^\/storage\/projects\/code\/hypercore-kb\//, '')
  // If project is known and path doesn't start with it, prepend
  if (project && !normalized.startsWith(project + '/')) {
    // Check if it's a bare filename
    if (!normalized.includes('/')) {
      normalized = project + '/' + normalized
    }
  }
  return normalized
}

/**
 * Ingest all ratchet data into AtomSpace.
 */
async function ingestRatchet () {
  const up = await isCogServerUp()
  if (!up) {
    console.log('[atomspace-ingest] CogServer not running, skipping ratchet ingest')
    return { ok: false, reason: 'cogserver-down' }
  }

  const allExprs = []

  // Loops
  const loopsData = readJSON('loops.json')
  if (loopsData) {
    const loopExprs = loopsToScheme(loopsData)
    allExprs.push(...loopExprs)
    console.log(`[atomspace-ingest] ${loopExprs.length} expressions from ${(loopsData.loops || []).length} loops`)
  }

  // Rules
  const rulesData = readJSON('learned-rules.json')
  if (rulesData) {
    const ruleExprs = rulesToScheme(rulesData)
    allExprs.push(...ruleExprs)
    const ruleCount = Array.isArray(rulesData) ? rulesData.length : (rulesData.rules || []).length
    console.log(`[atomspace-ingest] ${ruleExprs.length} expressions from ${ruleCount} rules`)
  }

  // Project maps
  const mapsData = readJSON('project-maps.json')
  if (mapsData) {
    const mapExprs = projectMapsToScheme(mapsData)
    allExprs.push(...mapExprs)
    console.log(`[atomspace-ingest] ${mapExprs.length} expressions from project maps`)
  }

  // Send in batches
  const BATCH_SIZE = 50
  let sent = 0
  for (let i = 0; i < allExprs.length; i += BATCH_SIZE) {
    const batch = allExprs.slice(i, i + BATCH_SIZE)
    const combined = '(begin ' + batch.join(' ') + ')'
    const result = await evalScheme(combined)
    if (result === null) {
      console.error(`[atomspace-ingest] Connection lost at batch ${i}`)
      return { ok: false, reason: 'connection-lost', sent }
    }
    sent += batch.length
  }

  console.log(`[atomspace-ingest] Ratchet data ingested: ${sent} atoms/edges`)
  return { ok: true, sent }
}

// CLI entry point
if (require.main === module) {
  ingestRatchet().then(result => {
    console.log(JSON.stringify(result, null, 2))
    process.exit(result.ok ? 0 : 1)
  })
}

module.exports = { ingestRatchet, loopsToScheme, rulesToScheme, projectMapsToScheme }