**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
507 lines
18 KiB
JavaScript
507 lines
18 KiB
JavaScript
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════
|
|
// 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, '&')
|
|
.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;
|
|
}
|