diff --git a/docs/symbolic-debugger.md b/docs/symbolic-debugger.md new file mode 100644 index 00000000..de0402b0 --- /dev/null +++ b/docs/symbolic-debugger.md @@ -0,0 +1,71 @@ +# GOFAI Symbolic Engine Debugger Overlay + +Refs: the-nexus #871 + +## Overview + +A specialized debug overlay that shows the internal state of the Symbolic Engine in real-time. Press **Ctrl+Shift+G** to toggle. + +## Features + +### 1. Active Symbols Panel +Displays all facts currently in the symbolic engine with their truth values: +- Green ● = true +- Red ○ = false + +### 2. FSM States Panel +Shows the current state of all registered finite state machines: +- Agent ID on the left +- Current state on the right (highlighted) + +### 3. Reasoning Paths Panel +Chronological log of all reasoning steps: +- Timestamp +- Rule that fired +- Outcome produced + +### 4. Knowledge Graph Panel +- Node count and edge count +- Mini visualization of graph topology (up to 15 nodes) +- Color-coded by type (Agent, Location, etc.) + +### 5. Performance Metrics +- Number of facts +- Number of rules +- JavaScript heap usage (if available) + +## Usage + +```javascript +import { SymbolicDebugger } from './nexus/components/symbolic-debugger.js'; +import { SymbolicEngine } from './nexus/symbolic-engine.js'; + +// Create engine +const engine = new SymbolicEngine(); + +// Initialize debugger +SymbolicDebugger.init({ + engine: engine, + fsmRegistry: new Map(), // optional + knowledgeGraph: null // optional +}); + +// Show the overlay +SymbolicDebugger.show(); + +// Or toggle with Ctrl+Shift+G +``` + +## Auto-refresh + +The debugger updates automatically every second when visible. Manual refresh via the ↻ button. + +## Dragging + +The overlay can be dragged by its header to reposition on screen. + +## Testing + +```bash +node tests/test_symbolic_debugger.js +``` diff --git a/nexus/components/symbolic-debugger.js b/nexus/components/symbolic-debugger.js new file mode 100644 index 00000000..9eb9747b --- /dev/null +++ b/nexus/components/symbolic-debugger.js @@ -0,0 +1,506 @@ +// ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// GOFAI Symbolic Engine Debugger Overlay (issue #871) +// ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// +// Specialized debug overlay showing internal state of the Symbolic Engine: +// — Active symbols and their truth values +// — Reasoning paths with timestamps +// — FSM state visualizations +// — Knowledge graph topology +// — Performance metrics +// +// Usage: +// import { SymbolicDebugger } from './symbolic-debugger.js'; +// SymbolicDebugger.init({ engine, blackboard, fsmRegistry }); +// SymbolicDebugger.show(); +// SymbolicDebugger.hide(); +// SymbolicDebugger.update(); // refresh from current state +// ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +const SymbolicDebugger = (() => { + let _overlay = null; + let _engine = null; + let _blackboard = null; + let _fsmRegistry = null; + let _kg = null; + let _visible = false; + let _refreshInterval = null; + + // ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + function init(opts = {}) { + _engine = opts.engine || null; + _blackboard = opts.blackboard || null; + _fsmRegistry = opts.fsmRegistry || null; + _kg = opts.knowledgeGraph || null; + + // Create overlay if not exists + if (!document.getElementById('symbolic-debugger-overlay')) { + _createOverlay(); + } + _overlay = document.getElementById('symbolic-debugger-overlay'); + + // Keyboard shortcut: Ctrl+Shift+G to toggle + document.addEventListener('keydown', (e) => { + if (e.ctrlKey && e.shiftKey && e.key === 'G') { + toggle(); + } + }); + + console.log('[SymbolicDebugger] Initialized. Press Ctrl+Shift+G to toggle.'); + } + + // ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + function _createOverlay() { + const div = document.createElement('div'); + div.id = 'symbolic-debugger-overlay'; + div.className = 'sym-debug-overlay'; + div.style.cssText = ` + position: fixed; + top: 60px; + right: 20px; + width: 480px; + max-height: calc(100vh - 80px); + background: rgba(10, 15, 30, 0.95); + border: 1px solid #4af0c0; + border-radius: 4px; + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + color: #e0f0ff; + overflow-y: auto; + z-index: 9999; + display: none; + box-shadow: 0 0 20px rgba(74, 240, 192, 0.3); + `; + + div.innerHTML = ` +
+ ◈ GOFAI SYMBOLIC DEBUGGER +
+ + +
+
+ +
+ +
+
Active Symbols
+
+
+ + +
+
FSM States
+
+
+ + +
+
Reasoning Paths
+
+
+ + +
+
Knowledge Graph
+
+
+
+ + +
+
Performance
+
+
+
+ `; + + document.body.appendChild(div); + + // Event listeners + document.getElementById('sym-debug-close').onclick = hide; + document.getElementById('sym-debug-refresh').onclick = update; + + // Make draggable + let isDragging = false; + let dragOffsetX = 0; + let dragOffsetY = 0; + + const header = div.querySelector('.sym-debug-header'); + header.addEventListener('mousedown', (e) => { + isDragging = true; + dragOffsetX = e.clientX - div.offsetLeft; + dragOffsetY = e.clientY - div.offsetTop; + }); + + document.addEventListener('mousemove', (e) => { + if (!isDragging) return; + div.style.left = (e.clientX - dragOffsetX) + 'px'; + div.style.top = (e.clientY - dragOffsetY) + 'px'; + div.style.right = 'auto'; + }); + + document.addEventListener('mouseup', () => { + isDragging = false; + }); + } + + // ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + function update() { + if (!_visible || !_overlay) return; + + // Update symbols + _updateSymbols(); + + // Update FSM states + _updateFSM(); + + // Update reasoning log + _updateReasoning(); + + // Update knowledge graph + _updateKnowledgeGraph(); + + // Update metrics + _updateMetrics(); + } + + function _updateSymbols() { + const container = document.getElementById('sym-debug-symbols'); + if (!container || !_engine) { + if (container) container.innerHTML = 'No engine connected'; + return; + } + + let html = ''; + if (_engine.facts && _engine.facts.size > 0) { + for (const [key, value] of _engine.facts) { + const truthColor = value ? '#4af0c0' : '#ff4466'; + const truthIcon = value ? '●' : '○'; + html += ` + ${_esc(key)} + ${truthIcon} ${_esc(String(value))} + `; + } + } else { + html = 'No active symbols'; + } + container.innerHTML = html; + } + + function _updateFSM() { + const container = document.getElementById('sym-debug-fsm'); + if (!container) return; + + let html = ''; + if (_fsmRegistry && _fsmRegistry.size > 0) { + for (const [agentId, fsm] of _fsmRegistry) { + const transitions = fsm.transitions ? Object.keys(fsm.transitions).length : 0; + html += ` +
+ ${_esc(agentId)} + ${_esc(fsm.state)} +
+ `; + } + } else if (_engine && _engine.fsm) { + // Single FSM mode + html += ` +
+ Primary FSM + ${_esc(_engine.fsm.state)} +
+ `; + } else { + html = 'No FSM registered'; + } + container.innerHTML = html; + } + + function _updateReasoning() { + const container = document.getElementById('sym-debug-reasoning'); + if (!container || !_engine) { + if (container) container.innerHTML = 'No reasoning log'; + return; + } + + let html = ''; + if (_engine.reasoningLog && _engine.reasoningLog.length > 0) { + for (const entry of _engine.reasoningLog) { + const time = entry.timestamp + ? new Date(entry.timestamp).toLocaleTimeString() + : '--:--:--'; + html += ` +
+
${time}
+
${_esc(entry.rule || 'Unknown rule')}
+
→ ${_esc(entry.outcome || 'No outcome')}
+
+ `; + } + } else { + html = 'No reasoning paths recorded'; + } + container.innerHTML = html; + } + + function _updateKnowledgeGraph() { + const statsContainer = document.getElementById('sym-debug-kg-stats'); + const vizContainer = document.getElementById('sym-debug-kg-viz'); + if (!statsContainer || !vizContainer) return; + + if (!_kg) { + statsContainer.innerHTML = 'No knowledge graph connected'; + vizContainer.innerHTML = ''; + return; + } + + const nodeCount = _kg.nodes ? _kg.nodes.size : 0; + const edgeCount = _kg.edges ? _kg.edges.length : 0; + + statsContainer.innerHTML = ` + ${nodeCount} nodes + | + ${edgeCount} edges + `; + + // Simple visualization + if (nodeCount > 0 && vizContainer) { + let vizHtml = ''; + let idx = 0; + for (const [nodeId, node] of _kg.nodes) { + const x = 20 + (idx % 5) * 80; + const y = 20 + Math.floor(idx / 5) * 30; + const color = node.type === 'Agent' ? '#4af0c0' : + node.type === 'Location' ? '#ffaa22' : '#4488ff'; + vizHtml += ` +
+ `; + idx++; + if (idx >= 15) break; // Limit visualization + } + vizContainer.innerHTML = vizHtml; + } else { + vizContainer.innerHTML = ''; + } + } + + function _updateMetrics() { + const container = document.getElementById('sym-debug-metrics'); + if (!container) return; + + let html = ''; + + // Engine metrics + if (_engine) { + const factCount = _engine.facts ? _engine.facts.size : 0; + const ruleCount = _engine.rules ? _engine.rules.length : 0; + html += ` + Facts: ${factCount} + Rules: ${ruleCount} + `; + } + + // Memory usage (rough estimate) + if (performance && performance.memory) { + const usedMB = Math.round(performance.memory.usedJSHeapSize / 1048576); + html += ` + Heap: ${usedMB} MB + `; + } + + container.innerHTML = html || 'No metrics available'; + } + + // ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + function show() { + if (!_overlay) return; + _overlay.style.display = 'block'; + _visible = true; + update(); + _startAutoRefresh(); + } + + function hide() { + if (!_overlay) return; + _overlay.style.display = 'none'; + _visible = false; + _stopAutoRefresh(); + } + + function toggle() { + if (_visible) hide(); + else show(); + } + + function isVisible() { + return _visible; + } + + function _startAutoRefresh() { + if (_refreshInterval) return; + _refreshInterval = setInterval(update, 1000); // Update every second + } + + function _stopAutoRefresh() { + if (_refreshInterval) { + clearInterval(_refreshInterval); + _refreshInterval = null; + } + } + + function _esc(str) { + if (typeof str !== 'string') return String(str); + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + // ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + return { + init, + show, + hide, + toggle, + isVisible, + update, + }; +})(); + +// Auto-export for module or global usage +if (typeof module !== 'undefined' && module.exports) { + module.exports = { SymbolicDebugger }; +} else if (typeof window !== 'undefined') { + window.SymbolicDebugger = SymbolicDebugger; +} diff --git a/tests/test_symbolic_debugger.js b/tests/test_symbolic_debugger.js new file mode 100644 index 00000000..fe44001e --- /dev/null +++ b/tests/test_symbolic_debugger.js @@ -0,0 +1,95 @@ +/** + * Tests for SymbolicDebugger component (issue #871) + */ + +import { SymbolicDebugger } from '../nexus/components/symbolic-debugger.js'; +import { SymbolicEngine, AgentFSM, KnowledgeGraph } from '../nexus/symbolic-engine.js'; + +// Mock DOM for Node.js testing +if (typeof document === 'undefined') { + const mockElements = new Map(); + + global.document = { + createElement: (tag) => { + const el = { + tagName: tag, + style: {}, + innerHTML: '', + children: [], + appendChild: function(child) { this.children.push(child); return child; }, + prepend: function(child) { this.children.unshift(child); return child; }, + querySelector: () => null, + querySelectorAll: () => [], + addEventListener: () => {}, + setAttribute: () => {}, + getAttribute: () => null, + }; + return el; + }, + body: { + appendChild: (el) => { + mockElements.set(el.id, el); + return el; + } + }, + addEventListener: () => {}, + getElementById: (id) => mockElements.get(id) || { + style: {}, + innerHTML: '', + onclick: null, + addEventListener: () => {}, + }, + }; + global.window = { SymbolicDebugger: null }; +} + +function assert(condition, message) { + if (!condition) { + console.error(`❌ FAILED: ${message}`); + process.exit(1); + } + console.log(`✔ PASSED: ${message}`); +} + +console.log('--- Running Symbolic Debugger Tests ---'); + +// Test 1: Module exports +assert(typeof SymbolicDebugger === 'object', 'SymbolicDebugger exports an object'); +assert(typeof SymbolicDebugger.init === 'function', 'SymbolicDebugger has init method'); +assert(typeof SymbolicDebugger.show === 'function', 'SymbolicDebugger has show method'); +assert(typeof SymbolicDebugger.hide === 'function', 'SymbolicDebugger has hide method'); +assert(typeof SymbolicDebugger.toggle === 'function', 'SymbolicDebugger has toggle method'); +assert(typeof SymbolicDebugger.update === 'function', 'SymbolicDebugger has update method'); + +// Test 2: Initial state +assert(SymbolicDebugger.isVisible() === false, 'Debugger starts hidden'); + +// Test 3: Engine integration (mock) +const mockEngine = { + facts: new Map([['energy', 75], ['stable', true]]), + rules: [{ condition: () => true, action: () => 'test' }], + reasoningLog: [ + { timestamp: Date.now(), rule: 'TestRule', outcome: 'TestOutcome' } + ] +}; + +SymbolicDebugger.init({ engine: mockEngine }); +assert(true, 'Debugger initializes with engine'); + +// Test 4: FSM integration +const mockFSM = { state: 'IDLE', transitions: { IDLE: [] } }; +SymbolicDebugger.init({ engine: mockEngine, fsmRegistry: new Map([['Agent1', mockFSM]]) }); +assert(true, 'Debugger initializes with FSM registry'); + +// Test 5: Knowledge Graph integration +const mockKG = { + nodes: new Map([ + ['A', { id: 'A', type: 'Agent' }], + ['B', { id: 'B', type: 'Location' }] + ]), + edges: [{ from: 'A', to: 'B', relation: 'AT' }] +}; +SymbolicDebugger.init({ engine: mockEngine, knowledgeGraph: mockKG }); +assert(true, 'Debugger initializes with Knowledge Graph'); + +console.log('--- All Symbolic Debugger Tests Passed ---');