OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet / atomspace / classify-files.js

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

/**
 * classify-files.js — Add language, domain, and role metadata to all loop-files
 *
 * Disambiguates the 467 "unknown" role files by examining extensions and paths.
 * Stores classification as Values on file atoms in AtomSpace.
 */

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

function classifyFile (f) {
  // Language from extension
  let lang = 'unknown'
  if (/\.(cpp|cc|h|hpp)$/.test(f)) lang = 'cpp'
  else if (/\.gd$/.test(f)) lang = 'gdscript'
  else if (/\.(glsl|gdshader|shader)$/.test(f)) lang = 'glsl'
  else if (/\.py$/.test(f)) lang = 'python'
  else if (/\.js$/.test(f)) lang = 'javascript'
  else if (/\.json$/.test(f)) lang = 'json'
  else if (/\.md$/.test(f)) lang = 'markdown'
  else if (/\.scm$/.test(f)) lang = 'scheme'
  else if (/\.(toml|yaml|yml)$/.test(f)) lang = 'config'
  else if (/\.(tscn|tres)$/.test(f)) lang = 'godot-scene'

  // Domain from path
  let domain = 'unknown'
  if (f.includes('/code/godot/')) domain = 'godot'
  else if (f.includes('/memory/') || f.includes('MEMORY.md')) domain = 'claude-memory'
  else if (f.includes('.claude/')) domain = 'claude-config'
  else if (f.includes('knowledge-ratchet')) domain = 'ratchet'
  else if (f.includes('hyper-fleeky/') || /^(pear-git|hypernote|hyper-|wirecore|codetree|trust-map)/.test(f)) domain = 'p2p-stack'

  // Role from path patterns
  let role = 'unknown'
  if (/\/test\/|\.test\./.test(f)) role = 'test'
  else if (f.includes('/src/')) role = 'src'
  else if (f.includes('/lib/')) role = 'lib'
  else if (f.includes('/bin/')) role = 'bin'
  else if (/\/examples?\//.test(f)) role = 'example'
  else if (f.includes('/docs/') || f.endsWith('.md')) role = 'docs'
  else if (f.includes('/scenes/')) role = 'scene'
  else if (f.includes('/shaders/') || f.includes('/addons/')) role = 'addon'
  else if (/\.(json|toml|yaml|yml|cfg)$/.test(f)) role = 'config'
  else if (/(index|main)\.(js|gd|cpp)$/.test(f)) role = 'entry'

  return { lang, domain, role }
}

function esc (s) {
  return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
}

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

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

  // Get all loop-files
  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 = loopFileStr.replace(/^"|"$/g, '').split('|').filter(Boolean)

  console.log('Classifying ' + loopFiles.length + ' files...')

  const batch = []
  const stats = { lang: {}, domain: {}, role: {} }

  for (const f of loopFiles) {
    const { lang, domain, role } = classifyFile(f)

    stats.lang[lang] = (stats.lang[lang] || 0) + 1
    stats.domain[domain] = (stats.domain[domain] || 0) + 1
    stats.role[role] = (stats.role[role] || 0) + 1

    const node = '(ConceptNode "' + esc(f) + '")'
    batch.push('(cog-set-value! ' + node + ' (Predicate "language") (StringValue "' + lang + '"))')
    batch.push('(cog-set-value! ' + node + ' (Predicate "domain") (StringValue "' + domain + '"))')
    batch.push('(cog-set-value! ' + node + ' (Predicate "role") (StringValue "' + role + '"))')

    if (batch.length >= 90) {
      await evalScheme('(begin ' + batch.join(' ') + ')')
      batch.length = 0
    }
  }

  if (batch.length > 0) {
    await evalScheme('(begin ' + batch.join(' ') + ')')
  }

  console.log('\nClassified ' + loopFiles.length + ' files\n')

  console.log('By language:')
  for (const [k, v] of Object.entries(stats.lang).sort((a, b) => b[1] - a[1])) {
    console.log('  ' + String(v).padStart(4) + '  ' + k)
  }

  console.log('\nBy domain:')
  for (const [k, v] of Object.entries(stats.domain).sort((a, b) => b[1] - a[1])) {
    console.log('  ' + String(v).padStart(4) + '  ' + k)
  }

  console.log('\nBy role:')
  for (const [k, v] of Object.entries(stats.role).sort((a, b) => b[1] - a[1])) {
    console.log('  ' + String(v).padStart(4) + '  ' + k)
  }

  // Now answer the deeper questions
  console.log('\n=== CROSS-DOMAIN: Correction rate by language ===')
  // Which languages have the most loops per file?
  const langLoops = {}
  const langFiles = {}
  for (const f of loopFiles) {
    const { lang } = classifyFile(f)
    langFiles[lang] = (langFiles[lang] || new Set()).add ? langFiles[lang] || new Set() : new Set()
    if (!langFiles[lang].add) langFiles[lang] = new Set()
    langFiles[lang].add(f)
  }

  // Count actual loops (not unique files) per language
  const allLoopEdges = 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 loopEdgeFiles = allLoopEdges.replace(/^"|"$/g, '').split('|').filter(Boolean)

  for (const f of loopEdgeFiles) {
    const { lang } = classifyFile(f)
    langLoops[lang] = (langLoops[lang] || 0) + 1
  }

  for (const [lang, loops] of Object.entries(langLoops).sort((a, b) => b[1] - a[1])) {
    const files = stats.lang[lang] || 1
    console.log('  ' + lang.padEnd(15) + String(loops).padStart(4) + ' loop-edges across ' + String(files).padStart(3) + ' files  (' + (loops / files).toFixed(1) + ' loops/file)')
  }

  console.log('\n=== CROSS-DOMAIN: Correction rate by domain ===')
  const domainLoops = {}
  for (const f of loopEdgeFiles) {
    const { domain } = classifyFile(f)
    domainLoops[domain] = (domainLoops[domain] || 0) + 1
  }
  for (const [domain, loops] of Object.entries(domainLoops).sort((a, b) => b[1] - a[1])) {
    const files = stats.domain[domain] || 1
    console.log('  ' + domain.padEnd(15) + String(loops).padStart(4) + ' loop-edges across ' + String(files).padStart(3) + ' files  (' + (loops / files).toFixed(1) + ' loops/file)')
  }
}

main().catch(err => { console.error(err.message); process.exit(1) })