lore-memory 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/lore.js CHANGED
@@ -10,6 +10,9 @@ program
10
10
  .description('Persistent project memory for developers')
11
11
  .version('0.1.0')
12
12
  .action(async () => {
13
+ // Only launch the interactive menu if strictly NO arguments were provided
14
+ if (process.argv.length !== 2) return;
15
+
13
16
  const inquirer = require('inquirer');
14
17
  const chalk = require('chalk');
15
18
  const { execSync } = require('child_process');
@@ -36,6 +39,7 @@ program
36
39
  { name: '👀 Review pending drafts (lore drafts)', value: 'drafts' },
37
40
  { name: '📊 View project health (lore score)', value: 'score' },
38
41
  { name: '🔍 Search knowledge base (lore search)', value: 'search' },
42
+ { name: '🌐 Open Local Web Dashboard (lore ui)', value: 'ui' },
39
43
  { name: '⚙️ Start background watcher (lore watch --daemon)', value: 'watch --daemon' },
40
44
  new inquirer.Separator(),
41
45
  { name: '❓ Show Help', value: 'help' },
@@ -192,4 +196,10 @@ program
192
196
  .option('--build', 'Rebuild the full graph from source')
193
197
  .action(require('../src/commands/graph'));
194
198
 
199
+ program
200
+ .command('ui')
201
+ .description('Start the local Lore web dashboard')
202
+ .option('-p, --port <port>', 'Port to run the UI server on', '3333')
203
+ .action(require('../src/commands/ui'));
204
+
195
205
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lore-memory",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Persistent project memory for developers. Captures decisions, invariants, gotchas, and graveyard entries — automatically and manually — and injects them into AI coding sessions.",
5
5
  "main": "bin/lore.js",
6
6
  "bin": {
@@ -43,11 +43,13 @@
43
43
  "chalk": "^4",
44
44
  "chokidar": "^3.6.0",
45
45
  "commander": "^11",
46
+ "express": "^5.2.1",
46
47
  "fs-extra": "^11",
47
48
  "glob": "^10.5.0",
48
49
  "inquirer": "^8",
49
50
  "js-yaml": "^4",
50
51
  "natural": "^6.12.0",
51
- "ollama": "^0.6.3"
52
+ "ollama": "^0.6.3",
53
+ "open": "^11.0.0"
52
54
  }
53
55
  }
@@ -6,7 +6,7 @@ const fs = require('fs-extra');
6
6
  const { mineFile, mineDirectory } = require('../watcher/comments');
7
7
  const { requireInit } = require('../lib/guard');
8
8
 
9
- function mine(targetPath) {
9
+ async function mine(targetPath) {
10
10
  requireInit();
11
11
  const projectRoot = process.cwd();
12
12
  const target = targetPath || '.';
@@ -18,10 +18,11 @@ function mine(targetPath) {
18
18
 
19
19
  if (stat.isDirectory()) {
20
20
  console.log(chalk.cyan(`📖 Mining comments in ${target} ...`));
21
- count = mineDirectory(abs, projectRoot);
21
+ count = await mineDirectory(abs, projectRoot);
22
22
  } else {
23
23
  console.log(chalk.cyan(`📖 Mining comments in ${target} ...`));
24
- count = mineFile(abs, projectRoot).length;
24
+ const drafts = await mineFile(abs, projectRoot);
25
+ count = drafts.length;
25
26
  }
26
27
 
27
28
  if (count === 0) {
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+ const { requireInit } = require('../lib/guard');
7
+ const { readIndex, LORE_DIR } = require('../lib/index');
8
+ const { readEntry } = require('../lib/entries');
9
+ const { computeScore } = require('../lib/scorer');
10
+ const { listDrafts, acceptDraft, deleteDraft } = require('../lib/drafts');
11
+
12
+ // Only load 'open' dynamically to avoid overhead on other CLI commands if not needed
13
+ async function openBrowser(url) {
14
+ const open = (await import('open')).default;
15
+ await open(url);
16
+ }
17
+
18
+ function ui(options) {
19
+ requireInit();
20
+
21
+ const app = express();
22
+ const PORT = options.port || 3333;
23
+
24
+ app.use(express.json());
25
+
26
+ // CORS for local dev
27
+ app.use((req, res, next) => {
28
+ res.header('Access-Control-Allow-Origin', '*');
29
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
30
+ res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE');
31
+ next();
32
+ });
33
+
34
+ // API Endpoints
35
+
36
+ app.get('/api/stats', (req, res) => {
37
+ try {
38
+ const scoreData = computeScore();
39
+ const drafts = listDrafts();
40
+
41
+ const index = readIndex();
42
+ const counts = { decision: 0, invariant: 0, graveyard: 0, gotcha: 0 };
43
+ for (const entryPath of Object.values(index.entries)) {
44
+ const entry = readEntry(entryPath);
45
+ if (entry && counts[entry.type] !== undefined) counts[entry.type]++;
46
+ }
47
+
48
+ res.json({
49
+ score: scoreData,
50
+ counts,
51
+ draftCount: drafts.length,
52
+ totalEntries: Object.keys(index.entries).length
53
+ });
54
+ } catch (e) {
55
+ res.status(500).json({ error: e.message });
56
+ }
57
+ });
58
+
59
+ app.get('/api/entries', (req, res) => {
60
+ try {
61
+ const index = readIndex();
62
+ const entries = [];
63
+ for (const entryPath of Object.values(index.entries)) {
64
+ const entry = readEntry(entryPath);
65
+ if (entry) entries.push(entry);
66
+ }
67
+ // Sort newest first
68
+ entries.sort((a, b) => new Date(b.date) - new Date(a.date));
69
+ res.json(entries);
70
+ } catch (e) {
71
+ res.status(500).json({ error: e.message });
72
+ }
73
+ });
74
+
75
+ app.get('/api/drafts', (req, res) => {
76
+ try {
77
+ const drafts = listDrafts();
78
+ res.json(drafts);
79
+ } catch (e) {
80
+ res.status(500).json({ error: e.message });
81
+ }
82
+ });
83
+
84
+ app.post('/api/drafts/:id/accept', (req, res) => {
85
+ try {
86
+ const entry = acceptDraft(req.params.id);
87
+ res.json({ success: true, entry });
88
+ } catch (e) {
89
+ res.status(500).json({ error: e.message });
90
+ }
91
+ });
92
+
93
+ app.delete('/api/drafts/:id', (req, res) => {
94
+ try {
95
+ deleteDraft(req.params.id);
96
+ res.json({ success: true });
97
+ } catch (e) {
98
+ res.status(500).json({ error: e.message });
99
+ }
100
+ });
101
+
102
+ app.get('/api/graph', (req, res) => {
103
+ try {
104
+ const { loadGraph, saveGraph } = require('../lib/graph');
105
+ let g = loadGraph();
106
+
107
+ if (Object.keys(g.imports).length === 0) {
108
+ const { buildFullGraph } = require('../watcher/graph');
109
+ g = buildFullGraph(process.cwd());
110
+ saveGraph(g);
111
+ }
112
+
113
+ const nodesSet = new Set();
114
+ const edges = [];
115
+ for (const [file, deps] of Object.entries(g.imports)) {
116
+ nodesSet.add(file);
117
+ for (const dep of deps) {
118
+ nodesSet.add(dep);
119
+ edges.push({ from: file, to: dep });
120
+ }
121
+ }
122
+
123
+ const nodes = Array.from(nodesSet).map(id => ({ id, label: id }));
124
+ res.json({ nodes, edges });
125
+ } catch (e) {
126
+ res.status(500).json({ error: e.message });
127
+ }
128
+ });
129
+
130
+ // Handle unmapped API routes with 404 JSON (instead of serving index.html)
131
+ app.use('/api', (req, res) => {
132
+ res.status(404).json({ error: 'API route not found' });
133
+ });
134
+
135
+ // Serve the frontend application
136
+ const uiPath = path.join(__dirname, '..', 'ui', 'public');
137
+ app.use(express.static(uiPath));
138
+
139
+ // Catch-all to serve index.html for SPA routing
140
+ app.use((req, res) => {
141
+ res.sendFile(path.join(uiPath, 'index.html'));
142
+ });
143
+
144
+ const server = app.listen(PORT, () => {
145
+ const url = `http://localhost:${PORT}`;
146
+ console.log(chalk.green(`\n🚀 Lore UI Dashboard running at ${chalk.bold(url)}\n`));
147
+ console.log(chalk.cyan(` Press Ctrl+C to stop the server.`));
148
+
149
+ // Use native exec to open browser to avoid ESM import issues with 'open'
150
+ const { exec } = require('child_process');
151
+ const startPath = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
152
+ exec(`${startPath} ${url}`, (err) => {
153
+ if (err) {
154
+ console.log(chalk.dim(` (Could not open browser automatically. Please visit ${url} manually)`));
155
+ }
156
+ });
157
+ });
158
+
159
+ server.on('error', (e) => {
160
+ if (e.code === 'EADDRINUSE') {
161
+ console.error(chalk.red(`\nPort ${PORT} is already in use by another process.`));
162
+ console.error(chalk.yellow(`Use 'lore ui --port <number>' to specify a different port.\n`));
163
+ process.exit(1);
164
+ } else {
165
+ console.error(chalk.red(`\nFailed to start server: ${e.message}\n`));
166
+ process.exit(1);
167
+ }
168
+ });
169
+ }
170
+
171
+ module.exports = ui;
@@ -0,0 +1,286 @@
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ // --- Security ---
3
+ function escapeHtml(unsafe) {
4
+ if (!unsafe) return '';
5
+ return unsafe.toString()
6
+ .replace(/&/g, "&amp;")
7
+ .replace(/</g, "&lt;")
8
+ .replace(/>/g, "&gt;")
9
+ .replace(/"/g, "&quot;")
10
+ .replace(/'/g, "&#039;");
11
+ }
12
+
13
+ // --- Navigation ---
14
+ const navLinks = document.querySelectorAll('.nav-links a');
15
+ const views = document.querySelectorAll('.view');
16
+
17
+ function showView(targetId) {
18
+ navLinks.forEach(l => l.classList.remove('active'));
19
+ document.querySelector(`[data-target="${targetId}"]`).classList.add('active');
20
+
21
+ views.forEach(v => v.classList.remove('active'));
22
+ document.getElementById(`view-${targetId}`).classList.add('active');
23
+
24
+ if (targetId === 'graph') {
25
+ loadGraph();
26
+ }
27
+ }
28
+
29
+ navLinks.forEach(link => {
30
+ link.addEventListener('click', (e) => {
31
+ e.preventDefault();
32
+ showView(e.target.dataset.target);
33
+ });
34
+ });
35
+
36
+ // --- Toast Notifications ---
37
+ function showToast(msg) {
38
+ const toast = document.getElementById('toast');
39
+ toast.textContent = msg;
40
+ toast.classList.remove('hidden');
41
+ setTimeout(() => toast.classList.add('hidden'), 3000);
42
+ }
43
+
44
+ // --- Data Loading ---
45
+ async function loadStats() {
46
+ try {
47
+ const res = await fetch('/api/stats');
48
+ const data = await res.json();
49
+
50
+ // Dashboard
51
+ document.getElementById('score-value').textContent = data.score.score;
52
+ document.getElementById('stat-coverage').textContent = `${data.score.coverage}%`;
53
+ document.getElementById('fill-coverage').style.width = `${data.score.coverage}%`;
54
+ document.getElementById('stat-freshness').textContent = `${data.score.freshness}%`;
55
+ document.getElementById('fill-freshness').style.width = `${data.score.freshness}%`;
56
+ document.getElementById('stat-depth').textContent = `${data.score.depth}%`;
57
+ document.getElementById('fill-depth').style.width = `${data.score.depth}%`;
58
+
59
+ // Tips
60
+ const tipsUl = document.getElementById('score-tips');
61
+ tipsUl.innerHTML = '';
62
+ if (data.score.topUnlogged && data.score.topUnlogged.length > 0) {
63
+ const top = data.score.topUnlogged[0];
64
+ const li = document.createElement('li');
65
+ li.textContent = `CRITICAL: Unlogged high-activity module [${top.module}] (${top.commits} commits)`;
66
+ tipsUl.appendChild(li);
67
+ } else {
68
+ tipsUl.innerHTML = '<li>System nominal. All core modules documented.</li>';
69
+ }
70
+
71
+ // Nav Badge
72
+ document.getElementById('nav-draft-count').textContent = data.draftCount;
73
+
74
+ } catch (e) {
75
+ console.error('Failed to load stats', e);
76
+ }
77
+ }
78
+
79
+ let allEntries = [];
80
+
81
+ async function loadEntries() {
82
+ try {
83
+ const res = await fetch('/api/entries');
84
+ allEntries = await res.json();
85
+ renderEntries(allEntries);
86
+ } catch (e) {
87
+ console.error('Failed to load entries', e);
88
+ }
89
+ }
90
+
91
+ function renderEntries(entries) {
92
+ const container = document.getElementById('kb-list');
93
+ container.innerHTML = '';
94
+
95
+ if (entries.length === 0) {
96
+ container.innerHTML = '<p class="muted">No knowledge entries found in memory banks.</p>';
97
+ return;
98
+ }
99
+
100
+ entries.forEach(entry => {
101
+ const card = document.createElement('div');
102
+ card.className = 'entry-card';
103
+
104
+ const badgeClass = `type-${entry.type.toLowerCase()}`;
105
+
106
+ let filesHtml = '';
107
+ if (entry.files && entry.files.length > 0) {
108
+ filesHtml = `<div class="entry-meta">Files: ${escapeHtml(entry.files.join(', '))}</div>`;
109
+ }
110
+
111
+ let tagsHtml = '';
112
+ if (entry.tags && entry.tags.length > 0) {
113
+ tagsHtml = `<div class="entry-meta">Tags: ${escapeHtml(entry.tags.join(', '))}</div>`;
114
+ }
115
+
116
+ card.innerHTML = `
117
+ <div class="entry-header">
118
+ <span class="type-badge ${escapeHtml(badgeClass)}">[${escapeHtml(entry.type).toUpperCase()}]</span>
119
+ <span class="entry-date muted">${escapeHtml(entry.date.split('T')[0])}</span>
120
+ </div>
121
+ <div class="entry-title">${escapeHtml(entry.title)}</div>
122
+ <div class="entry-context">${escapeHtml(entry.context)}</div>
123
+ ${filesHtml}
124
+ ${tagsHtml}
125
+ `;
126
+ container.appendChild(card);
127
+ });
128
+ }
129
+
130
+ // Knowledge Base Filter & Search
131
+ document.getElementById('kb-search').addEventListener('input', (e) => {
132
+ const query = e.target.value.toLowerCase();
133
+ const typeMode = document.getElementById('kb-filter').value;
134
+ const filtered = allEntries.filter(e => {
135
+ const matchQuery = e.title.toLowerCase().includes(query) || e.context.toLowerCase().includes(query);
136
+ const matchType = typeMode === 'all' || e.type === typeMode;
137
+ return matchQuery && matchType;
138
+ });
139
+ renderEntries(filtered);
140
+ });
141
+
142
+ document.getElementById('kb-filter').addEventListener('change', (e) => {
143
+ const typeMode = e.target.value;
144
+ const query = document.getElementById('kb-search').value.toLowerCase();
145
+ const filtered = allEntries.filter(e => {
146
+ const matchQuery = e.title.toLowerCase().includes(query) || e.context.toLowerCase().includes(query);
147
+ const matchType = typeMode === 'all' || e.type === typeMode;
148
+ return matchQuery && matchType;
149
+ });
150
+ renderEntries(filtered);
151
+ });
152
+
153
+ // Drafts
154
+ async function loadDrafts() {
155
+ try {
156
+ const res = await fetch('/api/drafts');
157
+ const drafts = await res.json();
158
+
159
+ const container = document.getElementById('drafts-list');
160
+ container.innerHTML = '';
161
+
162
+ if (drafts.length === 0) {
163
+ container.innerHTML = '<p class="muted">All drafts have been processed.</p>';
164
+ return;
165
+ }
166
+
167
+ drafts.forEach(draft => {
168
+ const confPercent = Math.round((draft.confidence || 0) * 100);
169
+ const card = document.createElement('div');
170
+ card.className = 'entry-card';
171
+ card.id = `draft-${draft.draftId}`;
172
+
173
+ const badgeClass = `type-${draft.suggestedType.toLowerCase()}`;
174
+
175
+ let filesHtml = '';
176
+ if (draft.files && draft.files.length > 0) {
177
+ filesHtml = `<div class="entry-meta">Linked File: ${escapeHtml(draft.files[0])}</div>`;
178
+ }
179
+
180
+ card.innerHTML = `
181
+ <div class="entry-header">
182
+ <span class="type-badge ${escapeHtml(badgeClass)}">SUGGESTED: [${escapeHtml(draft.suggestedType).toUpperCase()}]</span>
183
+ <span class="entry-date muted">Confidence: ${confPercent}%</span>
184
+ </div>
185
+ <div class="entry-title">${escapeHtml(draft.suggestedTitle)}</div>
186
+ <div class="entry-context">Evidence: ${escapeHtml(draft.evidence)}</div>
187
+ ${filesHtml}
188
+ <div class="draft-actions">
189
+ <button class="btn btn-accept" onclick="acceptDraft('${escapeHtml(draft.draftId)}')">Accept</button>
190
+ <button class="btn btn-delete" onclick="deleteDraft('${escapeHtml(draft.draftId)}')">Delete</button>
191
+ </div>
192
+ `;
193
+ container.appendChild(card);
194
+ });
195
+
196
+ } catch (e) {
197
+ console.error('Failed to load drafts', e);
198
+ }
199
+ }
200
+
201
+ // Expose actions to global scope for inline handlers
202
+ window.acceptDraft = async (id) => {
203
+ try {
204
+ const res = await fetch(`/api/drafts/${id}/accept`, { method: 'POST' });
205
+ if (res.ok) {
206
+ document.getElementById(`draft-${id}`).remove();
207
+ showToast('Draft Accepted into Memory');
208
+ loadStats(); // update count
209
+ loadEntries(); // refresh KB
210
+ }
211
+ } catch (e) { }
212
+ };
213
+
214
+ window.deleteDraft = async (id) => {
215
+ try {
216
+ const res = await fetch(`/api/drafts/${id}`, { method: 'DELETE' });
217
+ if (res.ok) {
218
+ document.getElementById(`draft-${id}`).remove();
219
+ showToast('Draft Deleted');
220
+ loadStats();
221
+ }
222
+ } catch (e) { }
223
+ };
224
+
225
+ // Graph
226
+ let network = null;
227
+ async function loadGraph() {
228
+ if (network) return; // already loaded
229
+
230
+ try {
231
+ const res = await fetch('/api/graph');
232
+ const graphData = await res.json();
233
+
234
+ const container = document.getElementById('network-container');
235
+
236
+ const options = {
237
+ layout: {
238
+ hierarchical: {
239
+ enabled: true,
240
+ direction: 'UD', // Up-Down
241
+ sortMethod: 'directed', // Follows dependency arrows
242
+ levelSeparation: 150, // Space between tiers
243
+ nodeSpacing: 250,
244
+ treeSpacing: 400,
245
+ }
246
+ },
247
+ interaction: {
248
+ hover: true,
249
+ tooltipDelay: 200,
250
+ hideEdgesOnDrag: true
251
+ },
252
+ nodes: {
253
+ shape: 'dot',
254
+ size: 16,
255
+ font: { color: '#00FF41', face: 'monospace', size: 12 },
256
+ color: {
257
+ background: '#050505',
258
+ border: '#008F11',
259
+ highlight: { background: '#00FF41', border: '#FFFFFF' },
260
+ hover: { background: '#008F11', border: '#00FF41' }
261
+ },
262
+ shadow: { enabled: true, color: 'rgba(0, 255, 65, 0.4)', size: 10, x: 0, y: 0 }
263
+ },
264
+ edges: {
265
+ color: { color: '#004F09', highlight: '#00FF41', hover: '#008F11' },
266
+ arrows: { to: { enabled: true, scaleFactor: 0.5 } },
267
+ smooth: { type: 'cubicBezier', forceDirection: 'vertical', roundness: 0.4 }
268
+ },
269
+ physics: {
270
+ enabled: false // Physics usually conflicts with strict hierarchical layouts
271
+ }
272
+ };
273
+
274
+ network = new vis.Network(container, graphData, options);
275
+
276
+ } catch (e) {
277
+ console.error('Failed to load graph', e);
278
+ document.getElementById('network-container').innerHTML = '<p class="muted" style="padding: 20px;">Could not render graph data.</p>';
279
+ }
280
+ }
281
+
282
+ // Init
283
+ loadStats();
284
+ loadEntries();
285
+ loadDrafts();
286
+ });
@@ -0,0 +1,118 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Lore Dashboard</title>
7
+ <!-- Retro Fonts -->
8
+ <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Fira+Code:wght@400;600&display=swap" rel="stylesheet">
9
+ <!-- Vis Network for Graphs -->
10
+ <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
11
+ <link rel="stylesheet" href="style.css">
12
+ </head>
13
+ <body>
14
+ <div class="crt-overlay"></div>
15
+
16
+ <div class="layout">
17
+ <!-- Sidebar Navigation -->
18
+ <nav class="sidebar">
19
+ <div class="logo">
20
+ <pre>
21
+ ██╗ ██████╗ ██████╗ ███████╗
22
+ ██║ ██╔═══██╗██╔══██╗██╔════╝
23
+ ██║ ██║ ██║██████╔╝█████╗
24
+ ██║ ██║ ██║██╔══██╗██╔══╝
25
+ ███████╗╚██████╔╝██║ ██║███████╗
26
+ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
27
+ </pre>
28
+ <div class="subtitle">PROJECT MEMORY</div>
29
+ </div>
30
+
31
+ <ul class="nav-links">
32
+ <li><a href="#" class="active" data-target="dashboard">Dashboard</a></li>
33
+ <li><a href="#" data-target="knowledge">Knowledge Base</a></li>
34
+ <li><a href="#" data-target="drafts">Pending Drafts (<span id="nav-draft-count">0</span>)</a></li>
35
+ <li><a href="#" data-target="graph">Dependency Graph</a></li>
36
+ </ul>
37
+ </nav>
38
+
39
+ <!-- Main Content Area -->
40
+ <main class="content">
41
+
42
+ <!-- Dashboard View -->
43
+ <section id="view-dashboard" class="view active">
44
+ <h1 class="glitch" data-text="LORE SCORE">LORE SCORE</h1>
45
+ <div class="score-card">
46
+ <div class="main-score">
47
+ <span id="score-value">--</span><span class="muted">/100</span>
48
+ </div>
49
+ </div>
50
+
51
+ <div class="metrics-grid">
52
+ <div class="metric">
53
+ <h3>COVERAGE</h3>
54
+ <div class="progress-bar"><div class="fill" id="fill-coverage"></div></div>
55
+ <p class="stats" id="stat-coverage">--%</p>
56
+ </div>
57
+ <div class="metric">
58
+ <h3>FRESHNESS</h3>
59
+ <div class="progress-bar"><div class="fill" id="fill-freshness"></div></div>
60
+ <p class="stats" id="stat-freshness">--%</p>
61
+ </div>
62
+ <div class="metric">
63
+ <h3>DEPTH</h3>
64
+ <div class="progress-bar"><div class="fill" id="fill-depth"></div></div>
65
+ <p class="stats" id="stat-depth">--%</p>
66
+ </div>
67
+ </div>
68
+
69
+ <div class="tips-container">
70
+ <h3>SYSTEM ALERTS</h3>
71
+ <ul id="score-tips" class="terminal-list">
72
+ <li>Loading heuristics...</li>
73
+ </ul>
74
+ </div>
75
+ </section>
76
+
77
+ <!-- Knowledge Base View -->
78
+ <section id="view-knowledge" class="view">
79
+ <h1>KNOWLEDGE BASE</h1>
80
+ <div class="controls">
81
+ <input type="text" id="kb-search" placeholder="Search memory..._">
82
+ <select id="kb-filter">
83
+ <option value="all">ALL TYPES</option>
84
+ <option value="decision">DECISIONS</option>
85
+ <option value="invariant">INVARIANTS</option>
86
+ <option value="gotcha">GOTCHAS</option>
87
+ <option value="graveyard">GRAVEYARD</option>
88
+ </select>
89
+ </div>
90
+ <div id="kb-list" class="entries-grid">
91
+ <!-- Entries injected here by JS -->
92
+ </div>
93
+ </section>
94
+
95
+ <!-- Drafts View -->
96
+ <section id="view-drafts" class="view">
97
+ <h1>PENDING DRAFTS</h1>
98
+ <p class="subtitle">Awaiting human approval.</p>
99
+ <div id="drafts-list" class="drafts-container">
100
+ <!-- Drafts injected here by JS -->
101
+ </div>
102
+ </section>
103
+
104
+ <!-- Graph View -->
105
+ <section id="view-graph" class="view">
106
+ <h1>DEPENDENCY GRAPH</h1>
107
+ <div id="network-container"></div>
108
+ </section>
109
+
110
+ </main>
111
+ </div>
112
+
113
+ <!-- Modals / Tooltips -->
114
+ <div id="toast" class="toast hidden">Action successful</div>
115
+
116
+ <script src="app.js"></script>
117
+ </body>
118
+ </html>
@@ -0,0 +1,321 @@
1
+ :root {
2
+ --bg-color: #050505;
3
+ --panel-bg: #111111;
4
+ --text-main: #00FF41; /* CRT Green */
5
+ --text-muted: #008F11;
6
+ --accent-decision: #00FFFF; /* Cyan */
7
+ --accent-invariant: #FF003C; /* Red */
8
+ --accent-gotcha: #FFB000; /* Amber */
9
+ --accent-graveyard: #555555;
10
+ --border: 1px solid #008F11;
11
+ --font-pixel: 'Press Start 2P', cursive;
12
+ --font-code: 'Fira Code', 'Courier New', monospace;
13
+ --glitch-offset: 2px;
14
+ }
15
+
16
+ * {
17
+ box-sizing: border-box;
18
+ margin: 0;
19
+ padding: 0;
20
+ }
21
+
22
+ body {
23
+ background-color: var(--bg-color);
24
+ color: var(--text-main);
25
+ font-family: var(--font-code);
26
+ font-size: 14px;
27
+ line-height: 1.6;
28
+ overflow: hidden; /* App feels like a terminal */
29
+ }
30
+
31
+ /* CRT Scanline Overlay Effect */
32
+ .crt-overlay {
33
+ position: fixed;
34
+ top: 0;
35
+ left: 0;
36
+ width: 100vw;
37
+ height: 100vh;
38
+ background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
39
+ background-size: 100% 4px, 3px 100%;
40
+ pointer-events: none;
41
+ z-index: 999;
42
+ }
43
+
44
+ /* Layout */
45
+ .layout {
46
+ display: flex;
47
+ height: 100vh;
48
+ }
49
+
50
+ /* Sidebar */
51
+ .sidebar {
52
+ width: 350px;
53
+ background-color: var(--panel-bg);
54
+ border-right: var(--border);
55
+ padding: 30px;
56
+ display: flex;
57
+ flex-direction: column;
58
+ }
59
+
60
+ .logo pre {
61
+ font-family: monospace;
62
+ color: var(--text-main);
63
+ font-size: 8px; /* Fit the ASCII art */
64
+ white-space: pre;
65
+ margin-bottom: 5px;
66
+ line-height: 1.2;
67
+ }
68
+
69
+ .subtitle {
70
+ font-family: var(--font-code);
71
+ color: var(--text-muted);
72
+ font-size: 12px;
73
+ letter-spacing: 2px;
74
+ margin-bottom: 40px;
75
+ text-align: center;
76
+ }
77
+
78
+ .nav-links {
79
+ list-style: none;
80
+ }
81
+
82
+ .nav-links li {
83
+ margin-bottom: 15px;
84
+ }
85
+
86
+ .nav-links a {
87
+ color: var(--text-muted);
88
+ text-decoration: none;
89
+ font-family: var(--font-pixel);
90
+ font-size: 10px;
91
+ display: block;
92
+ padding: 10px;
93
+ border: 1px solid transparent;
94
+ transition: all 0.2s ease;
95
+ }
96
+
97
+ .nav-links a:hover, .nav-links a.active {
98
+ color: var(--bg-color);
99
+ background-color: var(--text-main);
100
+ border: 1px solid var(--text-main);
101
+ box-shadow: 0 0 10px var(--text-main);
102
+ }
103
+
104
+ /* Main Content Area */
105
+ .content {
106
+ flex: 1;
107
+ padding: 40px;
108
+ overflow-y: auto;
109
+ position: relative;
110
+ }
111
+
112
+ .view {
113
+ display: none;
114
+ animation: flicker 0.15s ease-in 1;
115
+ }
116
+
117
+ .view.active {
118
+ display: block;
119
+ }
120
+
121
+ h1 {
122
+ font-family: var(--font-pixel);
123
+ font-size: 24px;
124
+ margin-bottom: 30px;
125
+ color: var(--text-main);
126
+ text-shadow: 0 0 5px var(--text-main);
127
+ }
128
+
129
+ /* Dashboard Score Components */
130
+ .score-card {
131
+ border: var(--border);
132
+ padding: 40px;
133
+ text-align: center;
134
+ margin-bottom: 30px;
135
+ background: repeating-linear-gradient(
136
+ 45deg,
137
+ transparent,
138
+ transparent 10px,
139
+ rgba(0, 143, 17, 0.05) 10px,
140
+ rgba(0, 143, 17, 0.05) 20px
141
+ );
142
+ }
143
+
144
+ .main-score {
145
+ font-family: var(--font-pixel);
146
+ font-size: 64px;
147
+ text-shadow: 0 0 15px var(--text-main);
148
+ }
149
+
150
+ .muted {
151
+ font-size: 24px;
152
+ color: var(--text-muted);
153
+ }
154
+
155
+ .metrics-grid {
156
+ display: grid;
157
+ grid-template-columns: 1fr 1fr 1fr;
158
+ gap: 20px;
159
+ margin-bottom: 40px;
160
+ }
161
+
162
+ .metric h3 {
163
+ font-family: var(--font-pixel);
164
+ font-size: 10px;
165
+ margin-bottom: 15px;
166
+ color: var(--text-muted);
167
+ }
168
+
169
+ .progress-bar {
170
+ height: 20px;
171
+ border: var(--border);
172
+ padding: 2px;
173
+ margin-bottom: 10px;
174
+ }
175
+
176
+ .progress-bar .fill {
177
+ height: 100%;
178
+ background-color: var(--text-main);
179
+ width: 0%;
180
+ transition: width 1s ease-out;
181
+ }
182
+
183
+ .tips-container {
184
+ border-top: var(--border);
185
+ padding-top: 20px;
186
+ }
187
+
188
+ .terminal-list {
189
+ list-style: none;
190
+ }
191
+ .terminal-list li::before {
192
+ content: "> ";
193
+ color: var(--accent-gotcha);
194
+ }
195
+
196
+ /* Knowledge Base Entries */
197
+ .controls {
198
+ display: flex;
199
+ gap: 15px;
200
+ margin-bottom: 20px;
201
+ }
202
+
203
+ input, select, button {
204
+ background: var(--bg-color);
205
+ color: var(--text-main);
206
+ border: var(--border);
207
+ padding: 10px;
208
+ font-family: var(--font-code);
209
+ font-size: 14px;
210
+ }
211
+
212
+ input:focus, select:focus {
213
+ outline: none;
214
+ box-shadow: 0 0 8px var(--text-muted);
215
+ }
216
+
217
+ .entries-grid, .drafts-container {
218
+ display: flex;
219
+ flex-direction: column;
220
+ gap: 20px;
221
+ }
222
+
223
+ .entry-card {
224
+ border: var(--border);
225
+ padding: 20px;
226
+ background-color: rgba(17, 17, 17, 0.8);
227
+ }
228
+
229
+ .entry-header {
230
+ display: flex;
231
+ justify-content: space-between;
232
+ border-bottom: 1px dashed var(--text-muted);
233
+ padding-bottom: 10px;
234
+ margin-bottom: 15px;
235
+ }
236
+
237
+ .type-badge {
238
+ font-family: var(--font-pixel);
239
+ font-size: 10px;
240
+ padding: 4px 8px;
241
+ }
242
+
243
+ .type-decision { color: var(--bg-color); background-color: var(--accent-decision); }
244
+ .type-invariant { color: var(--bg-color); background-color: var(--accent-invariant); }
245
+ .type-gotcha { color: var(--bg-color); background-color: var(--accent-gotcha); }
246
+ .type-graveyard { color: var(--text-main); background-color: var(--accent-graveyard); }
247
+
248
+ .entry-title {
249
+ font-weight: bold;
250
+ font-size: 16px;
251
+ margin-bottom: 10px;
252
+ }
253
+
254
+ .entry-meta {
255
+ color: var(--text-muted);
256
+ font-size: 12px;
257
+ margin-top: 15px;
258
+ }
259
+
260
+ /* Draft Specifics */
261
+ .draft-actions {
262
+ display: flex;
263
+ gap: 10px;
264
+ margin-top: 15px;
265
+ }
266
+
267
+ .btn {
268
+ cursor: pointer;
269
+ font-family: var(--font-pixel);
270
+ font-size: 8px;
271
+ text-transform: uppercase;
272
+ transition: all 0.2s;
273
+ }
274
+
275
+ .btn-accept {
276
+ border-color: var(--text-main);
277
+ color: var(--text-main);
278
+ }
279
+ .btn-accept:hover { background: var(--text-main); color: var(--bg-color); }
280
+
281
+ .btn-delete {
282
+ border-color: var(--accent-invariant);
283
+ color: var(--accent-invariant);
284
+ }
285
+ .btn-delete:hover { background: var(--accent-invariant); color: var(--bg-color); }
286
+
287
+ /* Graph Container */
288
+ #network-container {
289
+ width: 100%;
290
+ height: 600px;
291
+ border: var(--border);
292
+ background-color: var(--panel-bg);
293
+ }
294
+
295
+ /* Animations */
296
+ @keyframes flicker {
297
+ 0% { opacity: 0; }
298
+ 10% { opacity: 0.5; }
299
+ 20% { opacity: 0; }
300
+ 50% { opacity: 1; }
301
+ 60% { opacity: 0.8; }
302
+ 100% { opacity: 1; }
303
+ }
304
+
305
+ .toast {
306
+ position: fixed;
307
+ bottom: 20px;
308
+ right: 20px;
309
+ background-color: var(--text-main);
310
+ color: var(--bg-color);
311
+ padding: 15px 30px;
312
+ font-family: var(--font-code);
313
+ font-weight: bold;
314
+ border: 1px solid white;
315
+ z-index: 1000;
316
+ transition: opacity 0.3s;
317
+ }
318
+ .toast.hidden {
319
+ opacity: 0;
320
+ pointer-events: none;
321
+ }
@@ -92,7 +92,7 @@ async function mineFile(absFilePath, projectRoot) {
92
92
  * @param {string[]} ignore
93
93
  * @returns {number} total drafts created
94
94
  */
95
- function mineDirectory(absDirPath, projectRoot, ignore) {
95
+ async function mineDirectory(absDirPath, projectRoot, ignore) {
96
96
  const { globSync } = require('glob');
97
97
  const ignoreList = ignore || ['node_modules', 'dist', '.git', '.lore', 'coverage'];
98
98
  const ignorePats = ignoreList.map(i => `${i}/**`);
@@ -105,7 +105,8 @@ function mineDirectory(absDirPath, projectRoot, ignore) {
105
105
 
106
106
  let total = 0;
107
107
  for (const file of files) {
108
- total += mineFile(file, projectRoot).length;
108
+ const drafts = await mineFile(file, projectRoot);
109
+ total += drafts.length;
109
110
  }
110
111
  return total;
111
112
  }