#!/usr/bin/env node
/**
* graph-export.js — Export a slice of AtomSpace as D3-compatible JSON
*
* Queries AtomSpace for a filtered subgraph and returns {nodes, edges}
* in the format expected by D3 force-directed layout.
*
* Filters (via query params or options):
* - module: only include atoms related to this module
* - minWisdom: filter atoms below this wisdom score
* - nodeTypes: which prefixes to include (file, mod, rule, loop, meta, pkg)
* - limit: max nodes to return (default 100)
*
* Usage:
* const { exportGraph } = require('./graph-export')
* const graph = await exportGraph({ minWisdom: 0.5, limit: 150 })
* // graph = { nodes: [...], edges: [...] }
*
* CLI:
* node graph-export.js # default slice
* node graph-export.js --module=pear-git # focused
* node graph-export.js --min-wisdom=1.0 # only high-wisdom
*/
const { evalScheme, isCogServerUp } = require('./cogserver')
// Node type classification by prefix
const NODE_TYPES = {
'file:': 'file',
'mod:': 'module',
'pkg:': 'package',
'fn:': 'function',
'rule:': 'rule',
'loop:': 'loop',
'meta:pattern:': 'pattern',
'meta:loop:': 'meta-loop',
'session:': 'session',
'live:correction:': 'correction',
'live:approval:': 'approval',
'live:edit:': 'edit'
}
function classifyAtom (name) {
for (const [prefix, type] of Object.entries(NODE_TYPES)) {
if (name.startsWith(prefix)) return type
}
return 'other'
}
function parseAtomNames (resultStr) {
if (!resultStr) return []
const names = []
const re = /(?:ConceptNode|Concept) "([^"]+)"/g
let m
while ((m = re.exec(resultStr)) !== null) names.push(m[1])
return names
}
/**
* Get wisdom score for an atom (0 if not scored).
*/
async function getWisdom (name) {
const result = await evalScheme(
`(let ((v (cog-value (ConceptNode "${name.replace(/"/g, '\\"')}") (Predicate "wisdom")))) (if v (cog-value-ref v 0) 0))`)
return parseFloat(result) || 0
}
/**
* Collect candidate atoms based on filter criteria.
*/
async function collectCandidates (opts) {
await evalScheme('(use-modules (srfi srfi-1))')
// If a specific module requested, start from there and expand
if (opts.module) {
return await collectModuleSlice(opts.module, opts.limit || 100)
}
// Otherwise: top wisdom atoms + their connections
return await collectTopWisdomSlice(opts.minWisdom || 0.5, opts.limit || 100)
}
/**
* Collect a slice centered on one module.
*/
async function collectModuleSlice (moduleName, limit) {
const modNode = `(ConceptNode "mod:${moduleName.replace(/"/g, '\\"')}")`
const atoms = new Set([`mod:${moduleName}`])
// Files contained in this module
const filesStr = await evalScheme(
`(cog-execute! (Meet (TypedVariable (Variable "$f") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "contains") (ListLink ${modNode} (Variable "$f"))))))`)
for (const n of parseAtomNames(filesStr)) atoms.add(n)
// Files this module depends on / imports
const depsStr = await evalScheme(
`(cog-execute! (Meet (TypedVariable (Variable "$d") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "depends-on") (ListLink ${modNode} (Variable "$d"))))))`)
for (const n of parseAtomNames(depsStr)) atoms.add(n)
// Rules and meta atoms for the files in this module
const files = [...atoms].filter(n => n.startsWith('file:'))
for (const file of files.slice(0, 30)) {
const fileAtom = `(ConceptNode "${file.replace(/"/g, '\\"')}")`
const rulesStr = await evalScheme(
`(cog-execute! (Meet (TypedVariable (Variable "$r") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "rule-for-file") (ListLink (Variable "$r") ${fileAtom})))))`)
for (const n of parseAtomNames(rulesStr)) atoms.add(n)
const loopsStr = await evalScheme(
`(cog-execute! (Meet (TypedVariable (Variable "$l") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "loop-in-file") (ListLink (Variable "$l") ${fileAtom})))))`)
for (const n of parseAtomNames(loopsStr)) atoms.add(n)
if (atoms.size >= limit) break
}
return [...atoms].slice(0, limit)
}
/**
* Collect top-wisdom atoms plus their immediate neighbors.
*/
async function collectTopWisdomSlice (minWisdom, limit) {
// Get all meta atoms and rules with wisdom values
const allStr = await evalScheme(
'(string-join (map cog-name (cog-get-atoms (quote ConceptNode))) "|")')
const allAtoms = (allStr || '').replace(/^"|"$/g, '').split('|').filter(Boolean)
const metaAndRules = allAtoms.filter(n =>
n.startsWith('rule:') || n.startsWith('meta:pattern:') || n.startsWith('meta:loop:'))
const scored = []
for (const name of metaAndRules) {
const wisdom = await getWisdom(name)
if (wisdom >= minWisdom) scored.push({ name, wisdom })
}
scored.sort((a, b) => b.wisdom - a.wisdom)
const atoms = new Set(scored.slice(0, Math.min(50, Math.floor(limit / 2))).map(s => s.name))
// Expand to include connected files and modules
for (const atomName of [...atoms]) {
if (atoms.size >= limit) break
const atomLiteral = `(ConceptNode "${atomName.replace(/"/g, '\\"')}")`
const bonds = atomName.startsWith('rule:')
? ['rule-for-file']
: atomName.startsWith('meta:pattern:')
? ['try-first', 'avoid-first']
: ['approach-works', 'approach-fails']
for (const bond of bonds) {
if (atoms.size >= limit) break
const result = await evalScheme(
`(cog-execute! (Meet (TypedVariable (Variable "$f") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "${bond}") (ListLink ${atomLiteral} (Variable "$f"))))))`)
for (const n of parseAtomNames(result)) {
if (atoms.size >= limit) break
atoms.add(n)
}
}
}
// Also add the modules that contain the files
const files = [...atoms].filter(n => n.startsWith('file:'))
for (const file of files.slice(0, 30)) {
if (atoms.size >= limit) break
const fileAtom = `(ConceptNode "${file.replace(/"/g, '\\"')}")`
const modStr = await evalScheme(
`(cog-execute! (Meet (TypedVariable (Variable "$m") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "contains") (ListLink (Variable "$m") ${fileAtom})))))`)
for (const n of parseAtomNames(modStr)) {
if (atoms.size >= limit) break
atoms.add(n)
}
}
return [...atoms]
}
/**
* Collect all edges between the given set of atoms.
*/
async function collectEdges (atomSet) {
const atomNames = new Set(atomSet)
const edges = []
const bondTypes = [
'contains', 'imports', 'depends-on', 'exports',
'loop-in-file', 'rule-for-file', 'resolved-by',
'approach-works', 'approach-fails', 'try-first', 'avoid-first'
]
for (const bond of bondTypes) {
const edgeStr = await evalScheme(
`(cog-incoming-by-type (BondNode "${bond}") (quote EdgeLink))`)
if (!edgeStr) continue
// Parse: each Edge has (Bond X) (List A B)
const listPattern = /\(List\s+\(Concept "([^"]+)"\)\s+\(Concept "([^"]+)"\)\)/g
let m
while ((m = listPattern.exec(edgeStr)) !== null) {
const [, src, dst] = m
if (atomNames.has(src) && atomNames.has(dst)) {
edges.push({ source: src, target: dst, type: bond })
}
}
}
return edges
}
/**
* Build a D3-ready graph.
*/
async function exportGraph (opts = {}) {
if (!await isCogServerUp()) {
return { nodes: [], edges: [], error: 'CogServer not running' }
}
const candidates = await collectCandidates(opts)
if (candidates.length === 0) {
return { nodes: [], edges: [], error: 'no atoms matched filters' }
}
// Fetch wisdom for all candidates (for sizing)
const nodes = []
for (const name of candidates) {
const type = classifyAtom(name)
let wisdom = 0
if (name.startsWith('rule:') || name.startsWith('meta:')) {
wisdom = await getWisdom(name)
}
// Short display label
let label = name
if (name.startsWith('file:')) label = name.slice(5).split('/').pop()
else if (name.startsWith('mod:')) label = name.slice(4)
else if (name.startsWith('pkg:')) label = name.slice(4)
else if (name.startsWith('rule:')) label = name.slice(5, 25) + '…'
else if (name.startsWith('meta:pattern:')) label = name.slice(13)
else if (name.startsWith('meta:loop:')) label = 'loop:' + name.slice(10, 18)
else if (name.startsWith('loop:')) label = name.slice(5, 25) + '…'
nodes.push({
id: name,
type,
label,
wisdom,
// Fully-qualified for tooltip
fullName: name
})
}
const edges = await collectEdges(candidates)
return {
nodes,
edges,
stats: {
nodeCount: nodes.length,
edgeCount: edges.length,
byType: nodes.reduce((acc, n) => { acc[n.type] = (acc[n.type] || 0) + 1; return acc }, {})
}
}
}
// CLI
if (require.main === module) {
const args = process.argv.slice(2)
const opts = {}
for (const a of args) {
if (a.startsWith('--module=')) opts.module = a.split('=')[1]
else if (a.startsWith('--min-wisdom=')) opts.minWisdom = parseFloat(a.split('=')[1])
else if (a.startsWith('--limit=')) opts.limit = parseInt(a.split('=')[1])
}
exportGraph(opts).then(result => {
console.log(JSON.stringify(result, null, 2))
}).catch(err => { console.error(err.message); process.exit(1) })
}
module.exports = { exportGraph }