@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.
- package/README.en.md +2 -2
- package/README.md +3 -3
- package/dist/api/cli.js +24 -2
- package/dist/api/cli.js.map +1 -1
- package/dist/api/mcp.js +86 -0
- package/dist/api/mcp.js.map +1 -1
- package/dist/codegraph/CodeIntelligence.d.ts +67 -0
- package/dist/codegraph/CodeIntelligence.js +457 -5
- package/dist/codegraph/CodeIntelligence.js.map +1 -1
- package/dist/cortex/SessionInjector.d.ts +1 -0
- package/dist/cortex/SessionInjector.js +33 -0
- package/dist/cortex/SessionInjector.js.map +1 -1
- package/dist/dashboard/DashboardServer.d.ts +33 -13
- package/dist/dashboard/DashboardServer.js +314 -182
- package/dist/dashboard/DashboardServer.js.map +1 -1
- package/dist/dashboard/index.d.ts +2 -2
- package/dist/dashboard/index.js +1 -1
- package/dist/dashboard/index.js.map +1 -1
- package/dist/dashboard/server.d.ts +8 -22
- package/dist/dashboard/server.js +2 -83
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/memory/MemoryBrain.d.ts +22 -0
- package/dist/memory/MemoryBrain.js +183 -4
- package/dist/memory/MemoryBrain.js.map +1 -1
- package/dist/memory/MemoryProviders.d.ts +6 -1
- package/dist/memory/MemoryProviders.js +190 -6
- package/dist/memory/MemoryProviders.js.map +1 -1
- package/dist/setup/SetupWizard.js +21 -7
- package/dist/setup/SetupWizard.js.map +1 -1
- package/dist/skills/SkillRepository.js +64 -1
- package/dist/skills/SkillRepository.js.map +1 -1
- package/dist/topology/DomainMapper.d.ts +23 -0
- package/dist/topology/DomainMapper.js +179 -0
- package/dist/topology/DomainMapper.js.map +1 -0
- package/dist/topology/LayerClassifier.d.ts +8 -0
- package/dist/topology/LayerClassifier.js +109 -0
- package/dist/topology/LayerClassifier.js.map +1 -0
- package/dist/topology/TourGenerator.d.ts +18 -0
- package/dist/topology/TourGenerator.js +120 -0
- package/dist/topology/TourGenerator.js.map +1 -0
- package/dist/topology/index.d.ts +3 -0
- package/dist/topology/index.js +4 -0
- package/dist/topology/index.js.map +1 -0
- package/docs/README.md +3 -0
- package/docs/architecture/README.md +248 -0
- package/docs/migration/v0.38-to-v0.44.md +232 -0
- package/docs/reference/cli.md +234 -0
- package/package.json +6 -5
- package/docs/EXTERNAL_REFERENCES.md +0 -66
- package/docs/SKILL-REPOSITORY.md +0 -57
- package/docs/SKILL_RADAR.md +0 -135
- package/docs/THIRD_PARTY_SKILLS.md +0 -114
|
@@ -1,86 +1,232 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dashboard Server —
|
|
3
|
-
*
|
|
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 {
|
|
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(
|
|
17
|
-
this.
|
|
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 ??
|
|
28
|
-
this.
|
|
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
|
-
|
|
31
|
-
|
|
38
|
+
// ── Middleware ────────────────────────────────────────────────────────
|
|
39
|
+
setupMiddleware() {
|
|
32
40
|
this.app.use('*', cors());
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
//
|
|
43
|
-
this.app.get('/
|
|
44
|
-
|
|
45
|
-
|
|
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',
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
//
|
|
64
|
-
this.app.get('/api/
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
222
|
-
for (const
|
|
223
|
-
const parent = byId.get(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
320
|
-
|
|
450
|
+
this.server?.close();
|
|
451
|
+
this.server = null;
|
|
452
|
+
logger.info('Dashboard 2.0 stopped');
|
|
321
453
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
return
|
|
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
|