@hongmaple0820/scale-engine 0.43.0 → 0.45.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.
Files changed (55) hide show
  1. package/README.en.md +2 -2
  2. package/README.md +3 -3
  3. package/dist/api/cli.js +24 -2
  4. package/dist/api/cli.js.map +1 -1
  5. package/dist/api/mcp.js +86 -0
  6. package/dist/api/mcp.js.map +1 -1
  7. package/dist/codegraph/CodeIntelligence.d.ts +67 -0
  8. package/dist/codegraph/CodeIntelligence.js +457 -5
  9. package/dist/codegraph/CodeIntelligence.js.map +1 -1
  10. package/dist/cortex/SessionInjector.d.ts +1 -0
  11. package/dist/cortex/SessionInjector.js +33 -0
  12. package/dist/cortex/SessionInjector.js.map +1 -1
  13. package/dist/dashboard/DashboardServer.d.ts +33 -13
  14. package/dist/dashboard/DashboardServer.js +314 -182
  15. package/dist/dashboard/DashboardServer.js.map +1 -1
  16. package/dist/dashboard/index.d.ts +2 -2
  17. package/dist/dashboard/index.js +1 -1
  18. package/dist/dashboard/index.js.map +1 -1
  19. package/dist/dashboard/server.d.ts +8 -22
  20. package/dist/dashboard/server.js +2 -83
  21. package/dist/dashboard/server.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/memory/MemoryBrain.d.ts +22 -0
  26. package/dist/memory/MemoryBrain.js +183 -4
  27. package/dist/memory/MemoryBrain.js.map +1 -1
  28. package/dist/memory/MemoryProviders.d.ts +6 -1
  29. package/dist/memory/MemoryProviders.js +190 -6
  30. package/dist/memory/MemoryProviders.js.map +1 -1
  31. package/dist/setup/SetupWizard.js +21 -7
  32. package/dist/setup/SetupWizard.js.map +1 -1
  33. package/dist/skills/SkillRepository.js +64 -1
  34. package/dist/skills/SkillRepository.js.map +1 -1
  35. package/dist/topology/DomainMapper.d.ts +23 -0
  36. package/dist/topology/DomainMapper.js +179 -0
  37. package/dist/topology/DomainMapper.js.map +1 -0
  38. package/dist/topology/LayerClassifier.d.ts +8 -0
  39. package/dist/topology/LayerClassifier.js +109 -0
  40. package/dist/topology/LayerClassifier.js.map +1 -0
  41. package/dist/topology/TourGenerator.d.ts +18 -0
  42. package/dist/topology/TourGenerator.js +120 -0
  43. package/dist/topology/TourGenerator.js.map +1 -0
  44. package/dist/topology/index.d.ts +3 -0
  45. package/dist/topology/index.js +4 -0
  46. package/dist/topology/index.js.map +1 -0
  47. package/docs/README.md +3 -0
  48. package/docs/architecture/README.md +248 -0
  49. package/docs/migration/v0.38-to-v0.44.md +232 -0
  50. package/docs/reference/cli.md +234 -0
  51. package/package.json +6 -5
  52. package/docs/EXTERNAL_REFERENCES.md +0 -66
  53. package/docs/SKILL-REPOSITORY.md +0 -57
  54. package/docs/SKILL_RADAR.md +0 -135
  55. package/docs/THIRD_PARTY_SKILLS.md +0 -114
@@ -1,86 +1,232 @@
1
1
  /**
2
- * Dashboard Server — Web-based visualization for SCALE Engine state
3
- * Part of P2-2: Web Dashboard for real-time monitoring
2
+ * Dashboard Server 2.0 Unified Hono server with Node.js adapter
3
+ * SPA architecture, SSE real-time, ECharts visualization
4
4
  */
5
5
  import { Hono } from 'hono';
6
6
  import { cors } from 'hono/cors';
7
- import { serveStatic } from 'hono/bun';
8
- import { readFileSync } from 'node:fs';
9
- import { join, dirname } from 'node:path';
7
+ import { streamSSE } from 'hono/streaming';
8
+ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
9
+ import { join, dirname, extname } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
+ import { dumpCodeGraphData } from '../codegraph/CodeIntelligence.js';
12
+ import { classifyLayers } from '../topology/LayerClassifier.js';
13
+ import { mapDomains } from '../topology/DomainMapper.js';
14
+ import { generateTour } from '../topology/TourGenerator.js';
15
+ import { aggregateGovernanceMetrics } from './MetricsAggregator.js';
16
+ import { logger } from '../core/logger.js';
11
17
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
- /**
13
- * DashboardServer — Hono-based web server for dashboard
14
- */
18
+ // ── Dashboard Server ─────────────────────────────────────────────────────
15
19
  export class DashboardServer {
16
- constructor(bus, options = {}) {
17
- this.store = null;
18
- this.fsm = null;
19
- this.evaluator = null;
20
- this.detectorTracker = null;
20
+ constructor(options = {}) {
21
+ this.server = null;
21
22
  this.app = new Hono();
22
- this.bus = bus;
23
+ this.bus = options.bus ?? null;
23
24
  this.store = options.store ?? null;
24
25
  this.fsm = options.fsm ?? null;
25
26
  this.evaluator = options.evaluator ?? null;
26
27
  this.detectorTracker = options.detectorTracker ?? null;
27
- this.port = options.port ?? 3000;
28
- this.setupRoutes();
28
+ this.port = options.port ?? 3210;
29
+ this.host = options.host ?? '0.0.0.0';
30
+ this.projectDir = options.projectDir ?? process.cwd();
31
+ this.scaleDir = options.scaleDir ?? join(this.projectDir, '.scale');
32
+ this.setupMiddleware();
33
+ this.setupSPA();
34
+ this.setupAPI();
35
+ this.setupSSE();
36
+ this.setupWriteOps();
29
37
  }
30
- setupRoutes() {
31
- // CORS for cross-origin requests
38
+ // ── Middleware ────────────────────────────────────────────────────────
39
+ setupMiddleware() {
32
40
  this.app.use('*', cors());
33
- // Static files for frontend
34
- this.app.use('/static/*', serveStatic({ root: './src/dashboard/static' }));
35
- // Health check
36
- this.app.get('/health', (c) => c.json({ status: 'ok', timestamp: Date.now() }));
37
- // Main dashboard state
38
- this.app.get('/api/state', async (c) => {
39
- const state = await this.getDashboardState();
40
- return c.json(state);
41
+ }
42
+ // ── SPA Serves ───────────────────────────────────────────────────────
43
+ setupSPA() {
44
+ // Resolve SPA dir: try dist/spa first, fall back to src/spa
45
+ const distSpa = join(__dirname, 'spa');
46
+ const srcSpa = join(__dirname, '..', '..', 'src', 'dashboard', 'spa');
47
+ const spaDir = existsSync(distSpa) ? distSpa : srcSpa;
48
+ const mimeTypes = {
49
+ '.html': 'text/html; charset=utf-8',
50
+ '.js': 'application/javascript; charset=utf-8',
51
+ '.css': 'text/css; charset=utf-8',
52
+ '.json': 'application/json',
53
+ '.png': 'image/png',
54
+ '.svg': 'image/svg+xml',
55
+ '.ico': 'image/x-icon',
56
+ };
57
+ // Serve SPA static files
58
+ this.app.get('/spa/*', async (c) => {
59
+ const path = c.req.path.replace('/spa/', '') || 'index.html';
60
+ const filePath = join(spaDir, path);
61
+ if (!existsSync(filePath) || !statSync(filePath).isFile()) {
62
+ return c.notFound();
63
+ }
64
+ const ext = extname(filePath);
65
+ const contentType = mimeTypes[ext] ?? 'application/octet-stream';
66
+ const content = readFileSync(filePath);
67
+ return new Response(content, {
68
+ headers: { 'Content-Type': contentType, 'Cache-Control': 'no-cache' },
69
+ });
41
70
  });
42
- // Artifact tree
43
- this.app.get('/api/artifacts', async (c) => {
44
- const tree = await this.getArtifactTree();
45
- return c.json(tree);
71
+ // Root redirect to SPA
72
+ this.app.get('/', (c) => c.redirect('/spa/'));
73
+ // Legacy views (backward compat)
74
+ const distViews = join(__dirname, 'views');
75
+ const srcViews = join(__dirname, '..', '..', 'src', 'dashboard', 'views');
76
+ const viewsDir = existsSync(distViews) ? distViews : srcViews;
77
+ this.app.get('/legacy/:view', (c) => {
78
+ const view = c.req.param('view');
79
+ const viewMap = {
80
+ 'artifacts': 'artifact-flow.html',
81
+ 'sessions': 'session-timeline.html',
82
+ 'knowledge': 'knowledge-graph.html',
83
+ 'evolution': 'evolution-metrics.html',
84
+ 'agents': 'agent-stats.html',
85
+ 'topology': 'topology.html',
86
+ };
87
+ const viewFile = viewMap[view];
88
+ if (!viewFile)
89
+ return c.notFound();
90
+ try {
91
+ const content = readFileSync(join(viewsDir, viewFile), 'utf-8');
92
+ return c.html(content);
93
+ }
94
+ catch {
95
+ return c.notFound();
96
+ }
46
97
  });
98
+ // Health check
99
+ this.app.get('/health', (c) => c.json({ status: 'ok', timestamp: Date.now(), version: '2.0.0' }));
100
+ }
101
+ // ── API Routes ───────────────────────────────────────────────────────
102
+ setupAPI() {
103
+ // Full dashboard state
104
+ this.app.get('/api/state', async (c) => c.json(await this.getDashboardState()));
105
+ // Artifact tree
106
+ this.app.get('/api/artifacts', async (c) => c.json(await this.getArtifactTree()));
47
107
  // Evolution metrics
48
- this.app.get('/api/evolution', async (c) => {
49
- const metrics = await this.getEvolutionMetrics();
50
- return c.json(metrics);
51
- });
108
+ this.app.get('/api/evolution', async (c) => c.json(await this.getEvolutionMetrics()));
52
109
  // Detector stats
53
- this.app.get('/api/detectors', async (c) => {
54
- const stats = this.getDetectorStats();
55
- return c.json(stats);
56
- });
110
+ this.app.get('/api/detectors', (c) => c.json(this.getDetectorStats()));
57
111
  // Recent events
58
112
  this.app.get('/api/events', async (c) => {
59
113
  const limit = parseInt(c.req.query('limit') ?? '50');
60
- const events = await this.getRecentEvents(limit);
61
- return c.json(events);
114
+ return c.json(await this.getRecentEvents(limit));
115
+ });
116
+ // Auto-defect stats
117
+ this.app.get('/api/auto-defects', async (c) => c.json(await this.getAutoDefectStats()));
118
+ // Governance metrics (aggregated from MetricsAggregator)
119
+ this.app.get('/api/metrics', (c) => {
120
+ try {
121
+ const metrics = aggregateGovernanceMetrics({
122
+ projectDir: this.projectDir,
123
+ scaleDir: this.scaleDir,
124
+ sinceDays: parseInt(c.req.query('days') ?? '7'),
125
+ });
126
+ return c.json(metrics);
127
+ }
128
+ catch (e) {
129
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);
130
+ }
62
131
  });
63
- // AutoDefect statistics
64
- this.app.get('/api/auto-defects', async (c) => {
65
- const stats = await this.getAutoDefectStats();
66
- return c.json(stats);
132
+ // Topology graph
133
+ this.app.get('/api/topology', (c) => c.json(this.getTopology()));
134
+ // Guided tour
135
+ this.app.get('/api/topology/tour', (c) => c.json(generateTour(this.getTopology())));
136
+ // Domain mapping
137
+ this.app.get('/api/topology/domains', (c) => {
138
+ const graph = this.getTopology();
139
+ return c.json(mapDomains(graph));
67
140
  });
68
- // ── Write Operations ──────────────────────────────────────────────
141
+ // Available documents in .scale/
142
+ this.app.get('/api/documents', (c) => {
143
+ return c.json(this.listDocuments());
144
+ });
145
+ // Serve a document by path
146
+ this.app.get('/api/documents/*', (c) => {
147
+ const docPath = c.req.path.replace('/api/documents/', '');
148
+ return this.serveDocument(docPath, c);
149
+ });
150
+ // Available FSM actions for artifact
151
+ this.app.get('/api/artifacts/:id/actions', async (c) => {
152
+ if (!this.fsm)
153
+ return c.json({ error: 'FSM not available' }, 503);
154
+ try {
155
+ const actions = await this.fsm.availableActions(c.req.param('id'));
156
+ return c.json({ id: c.req.param('id'), actions });
157
+ }
158
+ catch (e) {
159
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);
160
+ }
161
+ });
162
+ }
163
+ // ── SSE (Server-Sent Events) ─────────────────────────────────────────
164
+ setupSSE() {
165
+ this.app.get('/api/stream', (c) => {
166
+ return streamSSE(c, async (stream) => {
167
+ // Send initial state
168
+ const state = await this.getDashboardState();
169
+ await stream.writeSSE({ data: JSON.stringify({ type: 'init', state }), event: 'init' });
170
+ // Subscribe to EventBus for real-time updates
171
+ let alive = true;
172
+ const heartbeat = setInterval(async () => {
173
+ if (!alive)
174
+ return;
175
+ try {
176
+ await stream.writeSSE({ data: '{}', event: 'heartbeat' });
177
+ }
178
+ catch {
179
+ alive = false;
180
+ }
181
+ }, 30000);
182
+ // Listen for events
183
+ const unsub = this.bus?.on('*', async (event) => {
184
+ if (!alive)
185
+ return;
186
+ try {
187
+ await stream.writeSSE({
188
+ data: JSON.stringify({
189
+ type: 'event',
190
+ event: {
191
+ type: event.type,
192
+ timestamp: event.timestamp,
193
+ artifactId: event.artifactId,
194
+ },
195
+ }),
196
+ event: 'event',
197
+ });
198
+ }
199
+ catch {
200
+ alive = false;
201
+ }
202
+ });
203
+ // Wait until client disconnects
204
+ stream.onAbort(() => {
205
+ alive = false;
206
+ clearInterval(heartbeat);
207
+ unsub?.unsubscribe();
208
+ });
209
+ // Keep alive
210
+ while (alive) {
211
+ await new Promise(r => setTimeout(r, 1000));
212
+ }
213
+ });
214
+ });
215
+ }
216
+ // ── Write Operations ─────────────────────────────────────────────────
217
+ setupWriteOps() {
69
218
  // Artifact transition
70
219
  this.app.post('/api/artifacts/:id/transition', async (c) => {
71
- if (!this.fsm || !this.store) {
220
+ if (!this.fsm || !this.store)
72
221
  return c.json({ error: 'FSM or store not available' }, 503);
73
- }
74
222
  const id = c.req.param('id');
75
223
  const body = await c.req.json();
76
- if (!body.action) {
224
+ if (!body.action)
77
225
  return c.json({ error: 'Missing required field: action' }, 400);
78
- }
79
226
  try {
80
227
  const artifact = await this.store.get(id);
81
228
  if (!artifact)
82
229
  return c.json({ error: `Artifact not found: ${id}` }, 404);
83
- // Check available actions first
84
230
  const available = await this.fsm.availableActions(id);
85
231
  if (!available.includes(body.action)) {
86
232
  return c.json({
@@ -92,94 +238,64 @@ export class DashboardServer {
92
238
  actor: { kind: 'system', component: 'dashboard' },
93
239
  reason: body.reason ?? `Dashboard transition: ${body.action}`,
94
240
  });
95
- if (!result.success) {
96
- return c.json({
97
- error: 'Transition blocked by guards',
98
- blockedBy: result.blockedBy,
99
- }, 422);
100
- }
101
- return c.json({
102
- success: true,
103
- artifact: result.artifact,
104
- effectsExecuted: result.effectsExecuted,
105
- });
106
- }
107
- catch (e) {
108
- return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);
109
- }
110
- });
111
- // Get available transitions for an artifact
112
- this.app.get('/api/artifacts/:id/actions', async (c) => {
113
- if (!this.fsm)
114
- return c.json({ error: 'FSM not available' }, 503);
115
- const id = c.req.param('id');
116
- try {
117
- const actions = await this.fsm.availableActions(id);
118
- return c.json({ id, actions });
241
+ if (!result.success)
242
+ return c.json({ error: 'Transition blocked', blockedBy: result.blockedBy }, 422);
243
+ return c.json({ success: true, artifact: result.artifact, effectsExecuted: result.effectsExecuted });
119
244
  }
120
245
  catch (e) {
121
246
  return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);
122
247
  }
123
248
  });
124
- // Lesson approve (PROPOSED → APPROVED)
249
+ // Lesson approve
125
250
  this.app.post('/api/lessons/:id/approve', async (c) => {
126
- if (!this.fsm || !this.store) {
251
+ if (!this.fsm || !this.store)
127
252
  return c.json({ error: 'FSM or store not available' }, 503);
128
- }
129
253
  const id = c.req.param('id');
130
254
  try {
131
255
  const artifact = await this.store.get(id);
132
- if (!artifact || artifact.type !== 'Lesson') {
256
+ if (!artifact || artifact.type !== 'Lesson')
133
257
  return c.json({ error: `Lesson not found: ${id}` }, 404);
134
- }
135
- if (artifact.status !== 'PROPOSED') {
258
+ if (artifact.status !== 'PROPOSED')
136
259
  return c.json({ error: `Lesson is "${artifact.status}", can only approve from PROPOSED` }, 400);
137
- }
138
260
  const body = await c.req.json().catch(() => ({}));
139
261
  const result = await this.fsm.transition(id, 'review', {
140
262
  actor: { kind: 'system', component: 'dashboard' },
141
263
  reason: body.reason ?? 'Approved via dashboard',
142
264
  });
143
- if (!result.success) {
265
+ if (!result.success)
144
266
  return c.json({ error: 'Transition blocked', blockedBy: result.blockedBy }, 422);
145
- }
146
267
  return c.json({ success: true, artifact: result.artifact });
147
268
  }
148
269
  catch (e) {
149
270
  return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);
150
271
  }
151
272
  });
152
- // Lesson reject (PROPOSED → REJECTED)
273
+ // Lesson reject
153
274
  this.app.post('/api/lessons/:id/reject', async (c) => {
154
- if (!this.fsm || !this.store) {
275
+ if (!this.fsm || !this.store)
155
276
  return c.json({ error: 'FSM or store not available' }, 503);
156
- }
157
277
  const id = c.req.param('id');
158
278
  try {
159
279
  const artifact = await this.store.get(id);
160
- if (!artifact || artifact.type !== 'Lesson') {
280
+ if (!artifact || artifact.type !== 'Lesson')
161
281
  return c.json({ error: `Lesson not found: ${id}` }, 404);
162
- }
163
- if (artifact.status !== 'PROPOSED') {
282
+ if (artifact.status !== 'PROPOSED')
164
283
  return c.json({ error: `Lesson is "${artifact.status}", can only reject from PROPOSED` }, 400);
165
- }
166
284
  const body = await c.req.json().catch(() => ({}));
167
285
  const result = await this.fsm.transition(id, 'reject', {
168
286
  actor: { kind: 'system', component: 'dashboard' },
169
287
  reason: body.reason ?? 'Rejected via dashboard',
170
288
  });
171
- if (!result.success) {
289
+ if (!result.success)
172
290
  return c.json({ error: 'Transition blocked', blockedBy: result.blockedBy }, 422);
173
- }
174
291
  return c.json({ success: true, artifact: result.artifact });
175
292
  }
176
293
  catch (e) {
177
294
  return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);
178
295
  }
179
296
  });
180
- // Index page - serve static HTML
181
- this.app.get('/', (c) => c.html(this.getIndexHtml()));
182
297
  }
298
+ // ── Data Collection ──────────────────────────────────────────────────
183
299
  async getDashboardState() {
184
300
  const [artifacts, evolutionMetrics, detectorStats, autoDefectStats, recentEvents] = await Promise.all([
185
301
  this.getArtifactTree(),
@@ -188,67 +304,41 @@ export class DashboardServer {
188
304
  this.getAutoDefectStats(),
189
305
  this.getRecentEvents(20),
190
306
  ]);
191
- return {
192
- artifacts,
193
- evolutionMetrics,
194
- detectorStats,
195
- autoDefectStats,
196
- recentEvents,
197
- timestamp: Date.now(),
198
- };
307
+ return { artifacts, evolutionMetrics, detectorStats, autoDefectStats, recentEvents, timestamp: Date.now() };
199
308
  }
200
309
  async getArtifactTree() {
201
310
  if (!this.store)
202
311
  return [];
203
312
  const artifacts = await this.store.query({});
204
- const nodes = [];
205
- // Build parent-child relationships
206
313
  const byId = new Map();
207
314
  for (const a of artifacts) {
208
- const node = {
209
- id: a.id,
210
- type: a.type,
211
- title: a.title,
212
- status: a.status,
213
- version: a.version,
214
- children: [],
315
+ byId.set(a.id, {
316
+ id: a.id, type: a.type, title: a.title, status: a.status, version: a.version, children: [],
215
317
  gates: a.gates?.map((g) => ({ name: g.name, required: g.required, passed: g.passed })),
216
- };
217
- byId.set(a.id, node);
318
+ });
218
319
  }
219
- // Connect children to parents
220
320
  for (const a of artifacts) {
221
- if (a.parents && a.parents.length > 0) {
222
- for (const parentId of a.parents) {
223
- const parent = byId.get(parentId);
224
- if (parent) {
225
- const child = byId.get(a.id);
226
- if (child)
227
- parent.children.push(child);
228
- }
321
+ if (a.parents?.length) {
322
+ for (const pid of a.parents) {
323
+ const parent = byId.get(pid);
324
+ const child = byId.get(a.id);
325
+ if (parent && child)
326
+ parent.children.push(child);
229
327
  }
230
328
  }
231
329
  }
232
- // Root nodes have no parents
233
- for (const a of artifacts) {
234
- if (!a.parents || a.parents.length === 0) {
235
- const node = byId.get(a.id);
236
- if (node)
237
- nodes.push(node);
238
- }
239
- }
240
- return nodes;
330
+ return artifacts
331
+ .filter(a => !a.parents?.length)
332
+ .map(a => byId.get(a.id))
333
+ .filter(Boolean);
241
334
  }
242
335
  async getEvolutionMetrics() {
243
- if (!this.evaluator)
244
- return null;
245
- return await this.evaluator.evaluate();
336
+ return this.evaluator?.evaluate() ?? null;
246
337
  }
247
338
  getDetectorStats() {
248
339
  if (!this.detectorTracker)
249
340
  return [];
250
- const allStats = this.detectorTracker.getAllStats();
251
- return allStats.map(s => ({
341
+ return this.detectorTracker.getAllStats().map(s => ({
252
342
  name: s.detectorName,
253
343
  totalTriggers: s.totalTriggers,
254
344
  bySeverity: s.bySeverity,
@@ -258,70 +348,112 @@ export class DashboardServer {
258
348
  async getAutoDefectStats() {
259
349
  if (!this.store)
260
350
  return null;
261
- // Query all Defect artifacts
262
351
  const defects = await this.store.query({ type: 'Defect' });
263
- const autoCreated = defects.filter(d => {
264
- const payload = d.payload;
265
- return payload.autoCreated === true;
266
- });
267
- // Count by rootCauseCategory
352
+ const autoCreated = defects.filter(d => d.payload?.autoCreated === true);
268
353
  const byRootCause = {};
269
354
  const bySeverity = {};
270
355
  for (const d of autoCreated) {
271
- const payload = d.payload;
272
- const rootCause = payload.rootCauseCategory ?? 'unknown';
273
- const severity = payload.severity ?? 'unknown';
274
- byRootCause[rootCause] = (byRootCause[rootCause] ?? 0) + 1;
275
- bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
356
+ const p = d.payload;
357
+ byRootCause[p.rootCauseCategory ?? 'unknown'] = (byRootCause[p.rootCauseCategory ?? 'unknown'] ?? 0) + 1;
358
+ bySeverity[p.severity ?? 'unknown'] = (bySeverity[p.severity ?? 'unknown'] ?? 0) + 1;
276
359
  }
277
- // Recent defects (last 10)
278
- const recentDefects = autoCreated
279
- .slice(-10)
280
- .reverse()
281
- .map(d => {
282
- const payload = d.payload;
360
+ const recentDefects = autoCreated.slice(-10).reverse().map(d => {
361
+ const p = d.payload;
283
362
  return {
284
- id: d.id,
285
- title: d.title,
286
- rootCause: payload.rootCauseCategory ?? 'unknown',
287
- severity: payload.severity ?? 'unknown',
288
- detector: payload.detector ?? 'unknown',
289
- createdAt: d.createdAt ?? (payload.timestamp ?? 0),
363
+ id: d.id, title: d.title,
364
+ rootCause: p.rootCauseCategory ?? 'unknown',
365
+ severity: p.severity ?? 'unknown',
366
+ detector: p.detector ?? 'unknown',
367
+ createdAt: d.createdAt ?? (p.timestamp ?? 0),
290
368
  };
291
369
  });
292
- return {
293
- totalDefects: defects.length,
294
- autoCreatedCount: autoCreated.length,
295
- byRootCause,
296
- bySeverity,
297
- recentDefects,
298
- };
370
+ return { totalDefects: defects.length, autoCreatedCount: autoCreated.length, byRootCause, bySeverity, recentDefects };
299
371
  }
300
372
  async getRecentEvents(limit) {
301
- // Get recent events from EventBus via query
373
+ if (!this.bus)
374
+ return [];
302
375
  const events = await this.bus.query({ limit });
303
376
  return events.map(e => ({
304
- type: e.type,
305
- timestamp: e.timestamp,
306
- artifactId: e.artifactId,
377
+ type: e.type, timestamp: e.timestamp, artifactId: e.artifactId,
307
378
  data: e.payload,
308
379
  }));
309
380
  }
310
- start() {
311
- console.log(`Dashboard server starting on port ${this.port}`);
312
- // @ts-expect-error Bun runtime API - types not available in npm package
313
- Bun.serve({
314
- port: this.port,
315
- fetch: this.app.fetch,
316
- });
381
+ getTopology() {
382
+ const raw = dumpCodeGraphData({ projectDir: this.projectDir });
383
+ return classifyLayers(raw);
384
+ }
385
+ listDocuments() {
386
+ const docs = [];
387
+ const scanDir = (dir, prefix) => {
388
+ if (!existsSync(dir))
389
+ return;
390
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
391
+ const fullPath = join(dir, entry.name);
392
+ const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
393
+ if (entry.isDirectory()) {
394
+ scanDir(fullPath, relPath);
395
+ }
396
+ else if (entry.isFile() && /\.(html|md|json)$/.test(entry.name)) {
397
+ const stat = statSync(fullPath);
398
+ docs.push({ name: entry.name, path: relPath, type: extname(entry.name).slice(1), size: stat.size });
399
+ }
400
+ }
401
+ };
402
+ // Scan common doc locations
403
+ scanDir(join(this.scaleDir, 'docs'), '.scale/docs');
404
+ scanDir(join(this.scaleDir, 'artifacts'), '.scale/artifacts');
405
+ scanDir(join(this.projectDir, 'docs'), 'docs');
406
+ return docs;
407
+ }
408
+ serveDocument(docPath, c) {
409
+ // docPath already includes prefix (e.g., 'docs/foo.md' or '.scale/docs/foo.md')
410
+ // Try direct resolution from project root and scale root
411
+ const searchDirs = [
412
+ this.projectDir,
413
+ this.scaleDir,
414
+ ];
415
+ for (const dir of searchDirs) {
416
+ const fullPath = join(dir, docPath);
417
+ if (existsSync(fullPath) && statSync(fullPath).isFile()) {
418
+ const ext = extname(fullPath);
419
+ const contentType = ext === '.html' ? 'text/html; charset=utf-8' : ext === '.json' ? 'application/json' : 'text/plain; charset=utf-8';
420
+ return new Response(readFileSync(fullPath), { headers: { 'Content-Type': contentType } });
421
+ }
422
+ }
423
+ return c.json({ error: 'Document not found' }, 404);
424
+ }
425
+ // ── Lifecycle ────────────────────────────────────────────────────────
426
+ async start() {
427
+ try {
428
+ const { serve } = await import('@hono/node-server');
429
+ this.server = serve({
430
+ fetch: this.app.fetch,
431
+ port: this.port,
432
+ hostname: this.host,
433
+ });
434
+ logger.info({ port: this.port, host: this.host }, 'Dashboard 2.0 started');
435
+ }
436
+ catch {
437
+ // Fallback: Bun runtime
438
+ // @ts-expect-error Bun runtime API
439
+ if (typeof Bun !== 'undefined') {
440
+ // @ts-expect-error Bun runtime API
441
+ Bun.serve({ port: this.port, fetch: this.app.fetch });
442
+ logger.info({ port: this.port }, 'Dashboard 2.0 started (Bun)');
443
+ }
444
+ else {
445
+ throw new Error('No compatible runtime found. Install @hono/node-server for Node.js.');
446
+ }
447
+ }
317
448
  }
318
449
  stop() {
319
- // Bun server stops automatically when process exits
320
- console.log('Dashboard server stopped');
450
+ this.server?.close();
451
+ this.server = null;
452
+ logger.info('Dashboard 2.0 stopped');
321
453
  }
322
- getIndexHtml() {
323
- const htmlPath = join(__dirname, 'index.html');
324
- return readFileSync(htmlPath, 'utf-8');
454
+ /** Get the underlying Hono app (for testing or embedding) */
455
+ getApp() {
456
+ return this.app;
325
457
  }
326
458
  }
327
459
  //# sourceMappingURL=DashboardServer.js.map