OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb / opencog-kb / basal-atomspace / index.js

271 lines · javascript ref:
/**
 * Basal AtomSpace - Minimal Implementation
 *
 * Full AtomSpace: ~65,000 lines C++
 * This version: ~300 lines JavaScript
 *
 * What we keep:
 * - Node (named, typed, unique)
 * - Link (typed, ordered outgoing set, unique)
 * - Values (mutable data on atoms)
 * - AtomSpace (uniqueness index + incoming sets)
 * - Basic queries
 *
 * What we drop:
 * - 100+ specialized atom types (we use plain type strings)
 * - Complex pattern matching (we use simple filters)
 * - Scheme/Python bindings (JS only)
 * - Thread safety machinery (single-threaded or use workers)
 * - C++ smart pointer complexity (JS has GC)
 */

import { createHash } from 'crypto'

// ============================================================================
// ATOMS (~50 lines vs 3,543 in C++)
// ============================================================================

class Atom {
  constructor(type) {
    this.type = type
    this.values = new Map()    // key -> any value
    this.incoming = new Set()  // Links containing this atom
    this._hash = null
  }

  // Attach a value to this atom
  setValue(key, value) {
    this.values.set(key, value)
  }

  getValue(key) {
    return this.values.get(key)
  }

  get hash() {
    if (!this._hash) this._hash = this._computeHash()
    return this._hash
  }

  _computeHash() {
    throw new Error('Subclass must implement')
  }
}

class Node extends Atom {
  constructor(type, name) {
    super(type)
    this.name = name
  }

  _computeHash() {
    return createHash('sha256')
      .update(`N:${this.type}:${this.name}`)
      .digest('hex')
      .slice(0, 16)
  }

  toString() {
    return `(${this.type} "${this.name}")`
  }
}

class Link extends Atom {
  constructor(type, outgoing) {
    super(type)
    this.outgoing = outgoing  // Array of Atoms
  }

  _computeHash() {
    const outHashes = this.outgoing.map(a => a.hash).join(',')
    return createHash('sha256')
      .update(`L:${this.type}:[${outHashes}]`)
      .digest('hex')
      .slice(0, 16)
  }

  toString() {
    const out = this.outgoing.map(a => a.toString()).join(' ')
    return `(${this.type} ${out})`
  }
}

// ============================================================================
// ATOMSPACE (~100 lines vs 2,923 in C++)
// ============================================================================

class AtomSpace {
  constructor() {
    this.atoms = new Map()      // hash -> Atom (uniqueness)
    this.byType = new Map()     // type -> Set<Atom>
    this.byName = new Map()     // name -> Atom (for nodes)
  }

  /**
   * Add an atom, returning existing if duplicate
   * This is the key operation - ensures global uniqueness
   */
  add(atom) {
    const hash = atom.hash

    // Already exists? Return existing
    if (this.atoms.has(hash)) {
      return this.atoms.get(hash)
    }

    // Store it
    this.atoms.set(hash, atom)

    // Index by type
    if (!this.byType.has(atom.type)) {
      this.byType.set(atom.type, new Set())
    }
    this.byType.get(atom.type).add(atom)

    // Index nodes by name
    if (atom instanceof Node) {
      this.byName.set(`${atom.type}:${atom.name}`, atom)
    }

    // Update incoming sets for links
    if (atom instanceof Link) {
      for (const child of atom.outgoing) {
        child.incoming.add(atom)
      }
    }

    return atom
  }

  // Convenience: create and add a node
  addNode(type, name) {
    return this.add(new Node(type, name))
  }

  // Convenience: create and add a link
  addLink(type, outgoing) {
    return this.add(new Link(type, outgoing))
  }

  // Find a node by type and name
  getNode(type, name) {
    return this.byName.get(`${type}:${name}`)
  }

  // Get all atoms of a type
  getByType(type) {
    return this.byType.get(type) || new Set()
  }

  // Get incoming links of specific type
  getIncoming(atom, linkType = null) {
    if (!linkType) return [...atom.incoming]
    return [...atom.incoming].filter(l => l.type === linkType)
  }

  // Simple query: find links matching a pattern
  query(pattern) {
    const { type, outgoing } = pattern
    const candidates = this.getByType(type)

    return [...candidates].filter(link => {
      if (!(link instanceof Link)) return false
      if (outgoing && outgoing.length !== link.outgoing.length) return false

      for (let i = 0; i < (outgoing?.length || 0); i++) {
        const p = outgoing[i]
        const a = link.outgoing[i]

        // null = wildcard
        if (p === null) continue

        // Atom reference = exact match
        if (p instanceof Atom && p !== a) return false

        // Object = match properties
        if (typeof p === 'object') {
          if (p.type && p.type !== a.type) return false
          if (p.name && a instanceof Node && p.name !== a.name) return false
        }
      }
      return true
    })
  }

  // Stats
  get size() {
    return this.atoms.size
  }
}

// ============================================================================
// VALUES (~20 lines vs 5,082 in C++)
// ============================================================================

// In C++, Values are complex classes with type hierarchies
// In JS, we just use plain objects/arrays and attach directly

// Common value keys (by convention)
const TRUTH_VALUE = Symbol('tv')      // [strength, confidence]
const ATTENTION_VALUE = Symbol('av')  // [sti, lti, vlti]
const COUNT_VALUE = Symbol('count')   // observation count

// Helper to set truth value
function setTruthValue(atom, strength, confidence = 1.0) {
  atom.setValue(TRUTH_VALUE, [strength, confidence])
}

function getTruthValue(atom) {
  return atom.getValue(TRUTH_VALUE) || [0, 0]
}

// ============================================================================
// EXAMPLE USAGE
// ============================================================================

function demo() {
  const as = new AtomSpace()

  // Create some concepts
  const dog = as.addNode('ConceptNode', 'dog')
  const animal = as.addNode('ConceptNode', 'animal')
  const mammal = as.addNode('ConceptNode', 'mammal')
  const cat = as.addNode('ConceptNode', 'cat')

  // Create relationships
  const dogIsAnimal = as.addLink('InheritanceLink', [dog, animal])
  const catIsAnimal = as.addLink('InheritanceLink', [cat, animal])
  const dogIsMammal = as.addLink('InheritanceLink', [dog, mammal])
  const mammalIsAnimal = as.addLink('InheritanceLink', [mammal, animal])

  // Attach truth values
  setTruthValue(dogIsAnimal, 1.0, 0.99)
  setTruthValue(catIsAnimal, 1.0, 0.99)

  // Query: what inherits from animal?
  const results = as.query({
    type: 'InheritanceLink',
    outgoing: [null, animal]  // [?, animal]
  })

  console.log('Things that are animals:')
  for (const link of results) {
    console.log('  ', link.outgoing[0].name)
  }

  // Navigate incoming set: what is dog?
  console.log('\nWhat is dog?')
  for (const link of as.getIncoming(dog, 'InheritanceLink')) {
    console.log('  dog is-a', link.outgoing[1].name)
  }

  console.log('\nAtomSpace size:', as.size, 'atoms')
}

export { Atom, Node, Link, AtomSpace, TRUTH_VALUE, setTruthValue, getTruthValue }

// Run demo if executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
  demo()
}