OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet / atomspace / test / why-analysis.js

186 lines · javascript ref:
#!/usr/bin/env node

/**
 * why-analysis.js — Discover WHY corrections happen using the combined graph
 *
 * Cross-domain analysis that neither codetree nor ratchet can do alone:
 *   1. Do corrections cluster at module boundaries?
 *   2. What file roles get corrected most?
 *   3. What loop types dominate?
 *   4. Which modules have worst correction density?
 *   5. Are there structural patterns that predict corrections?
 *   6. Rules that apply to structurally connected files
 */

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

function parseList (str) {
  if (!str) return []
  return str.replace(/^"|"$/g, '').split('|').filter(Boolean)
}

async function main () {
  if (!await isCogServerUp()) {
    console.log('CogServer not running')
    process.exit(1)
  }

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

  console.log('=== WHY ANALYSIS — Correction Patterns from AtomSpace ===\n')

  // ── 1. Module boundary analysis ──
  console.log('── 1. Do corrections cluster at module boundaries? ──')

  const loopFileStr = await evalScheme(
    '(string-join (delete-duplicates (map (lambda (e) (cog-name (cog-outgoing-atom (cog-outgoing-atom e 1) 1))) (cog-incoming-by-type (BondNode "loop-in-file") (quote EdgeLink)))) "|")')
  const loopFiles = new Set(parseList(loopFileStr))

  const importFileStr = await evalScheme(
    '(string-join (delete-duplicates (map (lambda (e) (cog-name (cog-outgoing-atom (cog-outgoing-atom e 1) 0))) (cog-incoming-by-type (BondNode "imports") (quote EdgeLink)))) "|")')
  const importFiles = new Set(parseList(importFileStr))

  const loopsAtBoundary = [...loopFiles].filter(f => importFiles.has(f)).length
  const loopsInternal = loopFiles.size - loopsAtBoundary

  console.log(`  Files with loops: ${loopFiles.size}`)
  console.log(`  Files with loops AND cross-module imports: ${loopsAtBoundary}`)
  console.log(`  Files with loops but NO imports: ${loopsInternal}`)
  if (loopFiles.size > 0) {
    console.log(`  Boundary correction rate: ${(loopsAtBoundary / loopFiles.size * 100).toFixed(0)}%`)
  }

  // ── 2. File role analysis ──
  console.log('\n── 2. Which file roles get corrected most? ──')
  const roleCounts = {}
  for (const f of loopFiles) {
    const role = await evalScheme(`(let ((v (cog-value (ConceptNode "${f}") (Predicate "role")))) (if v (cog-value-ref v 0) "unknown"))`)
    const clean = (role || 'unknown').replace(/"/g, '')
    roleCounts[clean] = (roleCounts[clean] || 0) + 1
  }
  for (const [role, count] of Object.entries(roleCounts).sort((a, b) => b[1] - a[1])) {
    console.log(`  ${String(count).padStart(4)} loop-files  role=${role}`)
  }

  // ── 3. Loop type distribution ──
  console.log('\n── 3. What types of loops dominate? ──')
  const typeStr = await evalScheme(
    '(string-join (map (lambda (e) (let ((n (cog-outgoing-atom (cog-outgoing-atom e 1) 0))) (let ((v (cog-value n (Predicate "type")))) (if v (cog-value-ref v 0) "unknown")))) (cog-incoming-by-type (BondNode "loop-in-file") (quote EdgeLink))) "|")')
  const typeCounts = {}
  for (const t of parseList(typeStr)) {
    const clean = t.replace(/"/g, '')
    typeCounts[clean] = (typeCounts[clean] || 0) + 1
  }
  for (const [type, count] of Object.entries(typeCounts).sort((a, b) => b[1] - a[1])) {
    console.log(`  ${String(count).padStart(4)}  ${type}`)
  }

  // ── 4. Module correction density ──
  console.log('\n── 4. Which modules have worst correction density? ──')
  const containStr = await evalScheme(
    '(string-join (map (lambda (e) (string-append (cog-name (cog-outgoing-atom (cog-outgoing-atom e 1) 0)) "::" (cog-name (cog-outgoing-atom (cog-outgoing-atom e 1) 1)))) (cog-incoming-by-type (BondNode "contains") (quote EdgeLink))) "|")')
  const modFileCounts = {}
  for (const pair of parseList(containStr)) {
    const mod = pair.split('::')[0]
    modFileCounts[mod] = (modFileCounts[mod] || 0) + 1
  }

  const modLoopCounts = {}
  for (const f of loopFiles) {
    for (const mod of Object.keys(modFileCounts)) {
      const prefix = mod.replace('mod:', '')
      if (f.includes(prefix + '/')) {
        modLoopCounts[mod] = (modLoopCounts[mod] || 0) + 1
        break
      }
    }
  }

  const modDensity = Object.entries(modFileCounts)
    .map(([mod, files]) => ({
      mod: mod.replace('mod:', ''),
      files,
      loops: modLoopCounts[mod] || 0,
      density: (modLoopCounts[mod] || 0) / files
    }))
    .filter(m => m.files > 2)
    .sort((a, b) => b.density - a.density)

  for (const m of modDensity.slice(0, 12)) {
    const bar = '█'.repeat(Math.round(m.density * 20))
    console.log(`  ${m.mod.padEnd(22)} ${(m.density * 100).toFixed(0).padStart(3)}%  ${bar}  (${m.loops} loops / ${m.files} files)`)
  }

  // ── 5. Structural risk patterns ──
  console.log('\n── 5. Structural risk patterns ──')

  // Files with exports that also have rules
  const exportFileStr = await evalScheme(
    '(string-join (delete-duplicates (map (lambda (e) (cog-name (cog-outgoing-atom (cog-outgoing-atom e 1) 0))) (cog-incoming-by-type (BondNode "exports") (quote EdgeLink)))) "|")')
  const exportFiles = new Set(parseList(exportFileStr))

  const ruleFileStr = await evalScheme(
    '(string-join (delete-duplicates (map (lambda (e) (cog-name (cog-outgoing-atom (cog-outgoing-atom e 1) 1))) (cog-incoming-by-type (BondNode "rule-for-file") (quote EdgeLink)))) "|")')
  const ruleFiles = new Set(parseList(ruleFileStr))

  const exportWithRules = [...exportFiles].filter(f => ruleFiles.has(f))
  const exportWithLoops = [...exportFiles].filter(f => loopFiles.has(f))

  console.log(`  Files that export public API: ${exportFiles.size}`)
  console.log(`  Of those, files with learned rules: ${exportWithRules.length} (${(exportWithRules.length / exportFiles.size * 100).toFixed(0)}%)`)
  console.log(`  Of those, files with correction loops: ${exportWithLoops.length} (${(exportWithLoops.length / exportFiles.size * 100).toFixed(0)}%)`)

  // ── 6. Top risk files (combined structural + behavioral) ──
  console.log('\n── 6. Highest risk files (exports + loops + rules) ──')

  // Count rules per file
  const ruleCountStr = await evalScheme(
    '(string-join (map (lambda (e) (cog-name (cog-outgoing-atom (cog-outgoing-atom e 1) 1))) (cog-incoming-by-type (BondNode "rule-for-file") (quote EdgeLink))) "|")')
  const ruleCountMap = {}
  for (const f of parseList(ruleCountStr)) ruleCountMap[f] = (ruleCountMap[f] || 0) + 1

  // Count loops per file
  const loopCountStr = await evalScheme(
    '(string-join (map (lambda (e) (cog-name (cog-outgoing-atom (cog-outgoing-atom e 1) 1))) (cog-incoming-by-type (BondNode "loop-in-file") (quote EdgeLink))) "|")')
  const loopCountMap = {}
  for (const f of parseList(loopCountStr)) loopCountMap[f] = (loopCountMap[f] || 0) + 1

  // Combined risk score: loops * 3 + rules * 1 + exports * 2
  const allFiles = new Set([...loopFiles, ...ruleFiles, ...exportFiles])
  const riskScores = [...allFiles].map(f => ({
    file: f.replace('file:', ''),
    loops: loopCountMap[f] || 0,
    rules: ruleCountMap[f] || 0,
    exports: exportFiles.has(f) ? 1 : 0,
    risk: (loopCountMap[f] || 0) * 3 + (ruleCountMap[f] || 0) * 1 + (exportFiles.has(f) ? 2 : 0)
  }))
    .filter(f => f.risk > 3)
    .sort((a, b) => b.risk - a.risk)

  for (const f of riskScores.slice(0, 15)) {
    console.log(`  risk=${String(f.risk).padStart(3)}  ${f.file}  (${f.loops} loops, ${f.rules} rules${f.exports ? ', exports API' : ''})`)
  }

  // ── 7. Key insight ──
  console.log('\n── 7. Key Insights ──')
  const totalLoopEdges = parseInt(await evalScheme('(length (cog-incoming-by-type (BondNode "loop-in-file") (quote EdgeLink)))')) || 0
  const totalRuleEdges = parseInt(await evalScheme('(length (cog-incoming-by-type (BondNode "rule-for-file") (quote EdgeLink)))')) || 0
  const totalExportEdges = parseInt(await evalScheme('(length (cog-incoming-by-type (BondNode "exports") (quote EdgeLink)))')) || 0

  console.log(`  Graph: ${totalLoopEdges} loop edges, ${totalRuleEdges} rule edges, ${totalExportEdges} export edges`)
  console.log(`  Module boundary corrections: ${loopsAtBoundary}/${loopFiles.size} (${(loopsAtBoundary / loopFiles.size * 100).toFixed(0)}%)`)
  console.log(`  Public API files with problems: ${exportWithRules.length}/${exportFiles.size}`)

  const topType = Object.entries(typeCounts).sort((a, b) => b[1] - a[1])[0]
  if (topType) console.log(`  Most common loop type: ${topType[0]} (${topType[1]} occurrences)`)

  const worstMod = modDensity[0]
  if (worstMod) console.log(`  Worst module by density: ${worstMod.mod} (${(worstMod.density * 100).toFixed(0)}% of files have loops)`)
}

main().catch(err => {
  console.error('Analysis failed:', err.message)
  process.exit(1)
})