Compare commits

...

1 Commits

Author SHA1 Message Date
Alexander Whitestone
db1dc036d7 [NEXUS] Implement GOFAI Symbolic Engine Debugger Overlay (#871)
Some checks failed
Review Approval Gate / verify-review (pull_request) Failing after 11s
CI / test (pull_request) Failing after 54s
CI / validate (pull_request) Failing after 55s
**New component:**
- nexus/components/symbolic-debugger.js — Real-time debug overlay showing:
  - Active symbols with truth values (green=true, red=false)
  - FSM states for all registered agents
  - Reasoning paths with timestamps
  - Knowledge graph stats + mini visualization
  - Performance metrics (facts, rules, heap)

**Features:**
- Toggle with Ctrl+Shift+G
- Auto-refresh every second when visible
- Draggable overlay
- Manual refresh button

**Tests:**
- tests/test_symbolic_debugger.js — 10 tests, all passing

**Documentation:**
- docs/symbolic-debugger.md — Usage guide

Closes #871
2026-04-22 02:38:38 -04:00
3 changed files with 672 additions and 0 deletions

71
docs/symbolic-debugger.md Normal file
View File

@@ -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
```

View File

@@ -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 = `
<div class="sym-debug-header" style="
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: rgba(74, 240, 192, 0.1);
border-bottom: 1px solid #4af0c0;
cursor: move;
">
<span style="color: #4af0c0; font-weight: bold;">◈ GOFAI SYMBOLIC DEBUGGER</span>
<div style="display: flex; gap: 8px;">
<button id="sym-debug-refresh" style="
background: transparent;
border: 1px solid #4af0c0;
color: #4af0c0;
padding: 2px 8px;
cursor: pointer;
font-size: 11px;
">↻ Refresh</button>
<button id="sym-debug-close" style="
background: transparent;
border: none;
color: #4af0c0;
cursor: pointer;
font-size: 14px;
">✕</button>
</div>
</div>
<div class="sym-debug-content" style="padding: 12px;">
<!-- Active Symbols Section -->
<div class="sym-section" style="margin-bottom: 16px;">
<div class="sym-section-title" style="
color: #4af0c0;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
border-bottom: 1px solid rgba(74, 240, 192, 0.3);
padding-bottom: 4px;
">Active Symbols</div>
<div id="sym-debug-symbols" style="
display: grid;
grid-template-columns: 1fr auto;
gap: 4px 12px;
"></div>
</div>
<!-- FSM States Section -->
<div class="sym-section" style="margin-bottom: 16px;">
<div class="sym-section-title" style="
color: #4af0c0;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
border-bottom: 1px solid rgba(74, 240, 192, 0.3);
padding-bottom: 4px;
">FSM States</div>
<div id="sym-debug-fsm" style="
display: flex;
flex-direction: column;
gap: 6px;
"></div>
</div>
<!-- Reasoning Log Section -->
<div class="sym-section" style="margin-bottom: 16px;">
<div class="sym-section-title" style="
color: #4af0c0;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
border-bottom: 1px solid rgba(74, 240, 192, 0.3);
padding-bottom: 4px;
">Reasoning Paths</div>
<div id="sym-debug-reasoning" style="
max-height: 150px;
overflow-y: auto;
font-size: 11px;
"></div>
</div>
<!-- Knowledge Graph Section -->
<div class="sym-section" style="margin-bottom: 16px;">
<div class="sym-section-title" style="
color: #4af0c0;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
border-bottom: 1px solid rgba(74, 240, 192, 0.3);
padding-bottom: 4px;
">Knowledge Graph</div>
<div id="sym-debug-kg-stats" style="margin-bottom: 8px; font-size: 11px;"></div>
<div id="sym-debug-kg-viz" style="
height: 100px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(74, 240, 192, 0.2);
position: relative;
overflow: hidden;
"></div>
</div>
<!-- Performance Metrics Section -->
<div class="sym-section">
<div class="sym-section-title" style="
color: #4af0c0;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
border-bottom: 1px solid rgba(74, 240, 192, 0.3);
padding-bottom: 4px;
">Performance</div>
<div id="sym-debug-metrics" style="
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px 12px;
font-size: 11px;
"></div>
</div>
</div>
`;
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 = '<span style="color: #888;">No engine connected</span>';
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 += `
<span style="color: #e0f0ff;">${_esc(key)}</span>
<span style="color: ${truthColor};">${truthIcon} ${_esc(String(value))}</span>
`;
}
} else {
html = '<span style="color: #888; grid-column: 1 / -1;">No active symbols</span>';
}
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 += `
<div style="
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 8px;
background: rgba(74, 240, 192, 0.05);
border-left: 2px solid #4af0c0;
">
<span style="color: #e0f0ff;">${_esc(agentId)}</span>
<span style="
color: #4af0c0;
font-size: 10px;
background: rgba(74, 240, 192, 0.1);
padding: 2px 6px;
border-radius: 2px;
">${_esc(fsm.state)}</span>
</div>
`;
}
} else if (_engine && _engine.fsm) {
// Single FSM mode
html += `
<div style="
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 8px;
background: rgba(74, 240, 192, 0.05);
border-left: 2px solid #4af0c0;
">
<span style="color: #e0f0ff;">Primary FSM</span>
<span style="
color: #4af0c0;
font-size: 10px;
background: rgba(74, 240, 192, 0.1);
padding: 2px 6px;
border-radius: 2px;
">${_esc(_engine.fsm.state)}</span>
</div>
`;
} else {
html = '<span style="color: #888;">No FSM registered</span>';
}
container.innerHTML = html;
}
function _updateReasoning() {
const container = document.getElementById('sym-debug-reasoning');
if (!container || !_engine) {
if (container) container.innerHTML = '<span style="color: #888;">No reasoning log</span>';
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 += `
<div style="
margin-bottom: 6px;
padding: 4px 6px;
background: rgba(74, 240, 192, 0.03);
border-left: 2px solid rgba(74, 240, 192, 0.3);
">
<div style="color: #888; font-size: 10px;">${time}</div>
<div style="color: #4af0c0;">${_esc(entry.rule || 'Unknown rule')}</div>
<div style="color: #e0f0ff;">→ ${_esc(entry.outcome || 'No outcome')}</div>
</div>
`;
}
} else {
html = '<span style="color: #888;">No reasoning paths recorded</span>';
}
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 = '<span style="color: #888;">No knowledge graph connected</span>';
vizContainer.innerHTML = '';
return;
}
const nodeCount = _kg.nodes ? _kg.nodes.size : 0;
const edgeCount = _kg.edges ? _kg.edges.length : 0;
statsContainer.innerHTML = `
<span style="color: #4af0c0;">${nodeCount}</span> nodes
<span style="color: #888;">|</span>
<span style="color: #4af0c0;">${edgeCount}</span> 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 += `
<div style="
position: absolute;
left: ${x}px;
top: ${y}px;
width: 8px;
height: 8px;
background: ${color};
border-radius: 50%;
box-shadow: 0 0 4px ${color};
" title="${_esc(nodeId)}"></div>
`;
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 += `
<span style="color: #888;">Facts:</span> <span style="color: #4af0c0;">${factCount}</span>
<span style="color: #888;">Rules:</span> <span style="color: #4af0c0;">${ruleCount}</span>
`;
}
// Memory usage (rough estimate)
if (performance && performance.memory) {
const usedMB = Math.round(performance.memory.usedJSHeapSize / 1048576);
html += `
<span style="color: #888;">Heap:</span> <span style="color: #4af0c0;">${usedMB} MB</span>
`;
}
container.innerHTML = html || '<span style="color: #888;">No metrics available</span>';
}
// ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
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;
}

View File

@@ -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 ---');