@hongmaple0820/scale-engine 0.49.0 → 0.50.2

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 (183) hide show
  1. package/README.en.md +2 -2
  2. package/README.md +2 -2
  3. package/dist/api/DashboardHttpConfig.d.ts +28 -0
  4. package/dist/api/DashboardHttpConfig.js +110 -0
  5. package/dist/api/DashboardHttpConfig.js.map +1 -0
  6. package/dist/api/cli.js +102 -11
  7. package/dist/api/cli.js.map +1 -1
  8. package/dist/api/http.d.ts +1 -0
  9. package/dist/api/http.js +52 -0
  10. package/dist/api/http.js.map +1 -0
  11. package/dist/artifact/types.d.ts +5 -0
  12. package/dist/artifact/types.js.map +1 -1
  13. package/dist/bootstrap/DependencyBootstrap.d.ts +1 -0
  14. package/dist/bootstrap/DependencyBootstrap.js +14 -3
  15. package/dist/bootstrap/DependencyBootstrap.js.map +1 -1
  16. package/dist/cli/cortexApplyCommand.d.ts +26 -0
  17. package/dist/cli/cortexApplyCommand.js +74 -0
  18. package/dist/cli/cortexApplyCommand.js.map +1 -0
  19. package/dist/cli/cortexCandidateCommands.d.ts +42 -0
  20. package/dist/cli/cortexCandidateCommands.js +119 -0
  21. package/dist/cli/cortexCandidateCommands.js.map +1 -0
  22. package/dist/cli/cortexCommands.d.ts +31 -0
  23. package/dist/cli/cortexCommands.js +102 -17
  24. package/dist/cli/cortexCommands.js.map +1 -1
  25. package/dist/cli/engineBootstrap.d.ts +1 -1
  26. package/dist/cli/engineBootstrap.js +2 -0
  27. package/dist/cli/engineBootstrap.js.map +1 -1
  28. package/dist/cli/evalCommands.js +1 -0
  29. package/dist/cli/evalCommands.js.map +1 -1
  30. package/dist/cli/phaseCommands.d.ts +28 -0
  31. package/dist/cli/phaseCommands.js +148 -9
  32. package/dist/cli/phaseCommands.js.map +1 -1
  33. package/dist/cli/runtimeSkillCommands.js +12 -2
  34. package/dist/cli/runtimeSkillCommands.js.map +1 -1
  35. package/dist/cli/shieldCommands.d.ts +1 -0
  36. package/dist/cli/shieldCommands.js +20 -7
  37. package/dist/cli/shieldCommands.js.map +1 -1
  38. package/dist/cli/workflowEvidenceCommands.d.ts +120 -0
  39. package/dist/cli/workflowEvidenceCommands.js +228 -2
  40. package/dist/cli/workflowEvidenceCommands.js.map +1 -1
  41. package/dist/cortex/AutoFixEventObservations.d.ts +11 -0
  42. package/dist/cortex/AutoFixEventObservations.js +72 -0
  43. package/dist/cortex/AutoFixEventObservations.js.map +1 -0
  44. package/dist/cortex/GateEvidenceObservations.d.ts +22 -0
  45. package/dist/cortex/GateEvidenceObservations.js +179 -0
  46. package/dist/cortex/GateEvidenceObservations.js.map +1 -0
  47. package/dist/cortex/GovernanceMetrics.d.ts +2 -0
  48. package/dist/cortex/GovernanceMetrics.js +112 -22
  49. package/dist/cortex/GovernanceMetrics.js.map +1 -1
  50. package/dist/cortex/InstinctApplicationRecorder.d.ts +28 -0
  51. package/dist/cortex/InstinctApplicationRecorder.js +145 -0
  52. package/dist/cortex/InstinctApplicationRecorder.js.map +1 -0
  53. package/dist/cortex/InstinctCandidateAudit.d.ts +3 -0
  54. package/dist/cortex/InstinctCandidateAudit.js +39 -0
  55. package/dist/cortex/InstinctCandidateAudit.js.map +1 -0
  56. package/dist/cortex/InstinctCandidateReview.d.ts +32 -0
  57. package/dist/cortex/InstinctCandidateReview.js +125 -0
  58. package/dist/cortex/InstinctCandidateReview.js.map +1 -0
  59. package/dist/cortex/InstinctExtractor.d.ts +1 -0
  60. package/dist/cortex/InstinctExtractor.js +24 -17
  61. package/dist/cortex/InstinctExtractor.js.map +1 -1
  62. package/dist/cortex/InstinctRuntimeEvidence.d.ts +14 -0
  63. package/dist/cortex/InstinctRuntimeEvidence.js +120 -0
  64. package/dist/cortex/InstinctRuntimeEvidence.js.map +1 -0
  65. package/dist/cortex/InstinctStore.d.ts +31 -4
  66. package/dist/cortex/InstinctStore.js +120 -20
  67. package/dist/cortex/InstinctStore.js.map +1 -1
  68. package/dist/cortex/SessionInjector.d.ts +1 -0
  69. package/dist/cortex/SessionInjector.js +54 -4
  70. package/dist/cortex/SessionInjector.js.map +1 -1
  71. package/dist/dashboard/DashboardServer.d.ts +237 -0
  72. package/dist/dashboard/DashboardServer.js +1083 -19
  73. package/dist/dashboard/DashboardServer.js.map +1 -1
  74. package/dist/dashboard/spa/assets/index-VYBCLBje.js +11 -0
  75. package/dist/dashboard/spa/assets/index-VhwY_ac1.css +1 -0
  76. package/dist/dashboard/spa/assets/naive-ui-BQy2AJkt.js +3340 -0
  77. package/dist/dashboard/spa/assets/vendor-BPU6aOYA.js +3 -0
  78. package/dist/dashboard/spa/assets/vue-CQQMb5Wi.js +17 -0
  79. package/dist/dashboard/spa/index.html +16 -0
  80. package/dist/env/EnvironmentDoctor.js +12 -7
  81. package/dist/env/EnvironmentDoctor.js.map +1 -1
  82. package/dist/eval/WorkflowEval.d.ts +9 -0
  83. package/dist/eval/WorkflowEval.js +348 -2
  84. package/dist/eval/WorkflowEval.js.map +1 -1
  85. package/dist/memory/MemoryBrain.d.ts +13 -0
  86. package/dist/memory/MemoryBrain.js +47 -0
  87. package/dist/memory/MemoryBrain.js.map +1 -1
  88. package/dist/memory/MemoryFabric.d.ts +14 -1
  89. package/dist/memory/MemoryFabric.js +72 -8
  90. package/dist/memory/MemoryFabric.js.map +1 -1
  91. package/dist/memory/MemoryLearning.d.ts +1 -0
  92. package/dist/memory/MemoryLearning.js +6 -3
  93. package/dist/memory/MemoryLearning.js.map +1 -1
  94. package/dist/memory/MemoryProviders.d.ts +8 -1
  95. package/dist/memory/MemoryProviders.js +143 -29
  96. package/dist/memory/MemoryProviders.js.map +1 -1
  97. package/dist/runtime/AiOsRuntime.d.ts +14 -1
  98. package/dist/runtime/AiOsRuntime.js +59 -3
  99. package/dist/runtime/AiOsRuntime.js.map +1 -1
  100. package/dist/runtime/RuntimeDoctor.js +3 -1
  101. package/dist/runtime/RuntimeDoctor.js.map +1 -1
  102. package/dist/runtime/RuntimeEvidenceLedger.d.ts +6 -0
  103. package/dist/runtime/RuntimeEvidenceLedger.js +52 -1
  104. package/dist/runtime/RuntimeEvidenceLedger.js.map +1 -1
  105. package/dist/runtime/SessionLedger.d.ts +2 -0
  106. package/dist/runtime/SessionLedger.js +4 -0
  107. package/dist/runtime/SessionLedger.js.map +1 -1
  108. package/dist/setup/SetupVerification.js +53 -5
  109. package/dist/setup/SetupVerification.js.map +1 -1
  110. package/dist/shield/PolicyCompiler.js +73 -12
  111. package/dist/shield/PolicyCompiler.js.map +1 -1
  112. package/dist/shield/ProtectedPaths.js +4 -2
  113. package/dist/shield/ProtectedPaths.js.map +1 -1
  114. package/dist/skills/SkillCatalog.d.ts +2 -0
  115. package/dist/skills/SkillCatalog.js +8 -0
  116. package/dist/skills/SkillCatalog.js.map +1 -1
  117. package/dist/skills/SkillDoctor.d.ts +19 -2
  118. package/dist/skills/SkillDoctor.js +163 -13
  119. package/dist/skills/SkillDoctor.js.map +1 -1
  120. package/dist/tools/SafeCommandRunner.d.ts +1 -0
  121. package/dist/tools/SafeCommandRunner.js +1 -0
  122. package/dist/tools/SafeCommandRunner.js.map +1 -1
  123. package/dist/tools/ToolCapabilityRegistry.js +25 -3
  124. package/dist/tools/ToolCapabilityRegistry.js.map +1 -1
  125. package/dist/tools/ToolOrchestrator.js +21 -0
  126. package/dist/tools/ToolOrchestrator.js.map +1 -1
  127. package/dist/version.d.ts +1 -1
  128. package/dist/version.js +1 -1
  129. package/dist/workflow/AgentLoopReadiness.d.ts +103 -0
  130. package/dist/workflow/AgentLoopReadiness.js +371 -0
  131. package/dist/workflow/AgentLoopReadiness.js.map +1 -0
  132. package/dist/workflow/EcosystemReadinessGate.d.ts +46 -0
  133. package/dist/workflow/EcosystemReadinessGate.js +126 -0
  134. package/dist/workflow/EcosystemReadinessGate.js.map +1 -0
  135. package/dist/workflow/EngineeringStandards.js +48 -3
  136. package/dist/workflow/EngineeringStandards.js.map +1 -1
  137. package/dist/workflow/GateCatalog.js +9 -0
  138. package/dist/workflow/GateCatalog.js.map +1 -1
  139. package/dist/workflow/GovernanceTemplatePacks.js +2 -26
  140. package/dist/workflow/GovernanceTemplatePacks.js.map +1 -1
  141. package/dist/workflow/GovernanceTemplates.js +8 -1
  142. package/dist/workflow/GovernanceTemplates.js.map +1 -1
  143. package/dist/workflow/ReleaseDeploymentLedger.d.ts +63 -0
  144. package/dist/workflow/ReleaseDeploymentLedger.js +154 -0
  145. package/dist/workflow/ReleaseDeploymentLedger.js.map +1 -0
  146. package/dist/workflow/ReviewAnalyzer.js +50 -3
  147. package/dist/workflow/ReviewAnalyzer.js.map +1 -1
  148. package/dist/workflow/SessionPreamble.d.ts +7 -0
  149. package/dist/workflow/SessionPreamble.js +48 -9
  150. package/dist/workflow/SessionPreamble.js.map +1 -1
  151. package/dist/workflow/VerificationCommands.d.ts +1 -0
  152. package/dist/workflow/VerificationCommands.js.map +1 -1
  153. package/dist/workflow/VerificationProfile.d.ts +5 -0
  154. package/dist/workflow/VerificationProfile.js +26 -0
  155. package/dist/workflow/VerificationProfile.js.map +1 -1
  156. package/dist/workflow/VerificationSchema.d.ts +3 -0
  157. package/dist/workflow/VerificationSchema.js +6 -0
  158. package/dist/workflow/VerificationSchema.js.map +1 -1
  159. package/dist/workflow/WorkflowEffectiveness.d.ts +97 -0
  160. package/dist/workflow/WorkflowEffectiveness.js +302 -0
  161. package/dist/workflow/WorkflowEffectiveness.js.map +1 -0
  162. package/dist/workflow/WorkflowEffectivenessRenderer.d.ts +2 -0
  163. package/dist/workflow/WorkflowEffectivenessRenderer.js +67 -0
  164. package/dist/workflow/WorkflowEffectivenessRenderer.js.map +1 -0
  165. package/dist/workflow/WorkflowEffectivenessScoring.d.ts +6 -0
  166. package/dist/workflow/WorkflowEffectivenessScoring.js +243 -0
  167. package/dist/workflow/WorkflowEffectivenessScoring.js.map +1 -0
  168. package/dist/workflow/gates/GateSystem.d.ts +16 -0
  169. package/dist/workflow/gates/GateSystem.js +208 -41
  170. package/dist/workflow/gates/GateSystem.js.map +1 -1
  171. package/dist/workflow/gates/MetaGovernanceGates.js +269 -8
  172. package/dist/workflow/gates/MetaGovernanceGates.js.map +1 -1
  173. package/docs/reference/cli.md +2 -1
  174. package/docs/start/agent-governance-demo.md +1 -1
  175. package/docs/workflow/ASSESSMENT_INDEX.md +326 -0
  176. package/docs/workflow/COMPARATIVE_ANALYSIS.md +422 -0
  177. package/docs/workflow/EXECUTIVE_SUMMARY.md +310 -0
  178. package/docs/workflow/IMPROVEMENT_CHECKLIST.md +518 -0
  179. package/docs/workflow/IMPROVEMENT_ROADMAP.md +707 -0
  180. package/docs/workflow/README.md +9 -1
  181. package/docs/workflow/templates/github-actions-scale-preflight.yml +4 -1
  182. package/package.json +10 -3
  183. package/scripts/workflow/run-vitest.mjs +123 -0
@@ -6,15 +6,23 @@ import { Hono } from 'hono';
6
6
  import { cors } from 'hono/cors';
7
7
  import { streamSSE } from 'hono/streaming';
8
8
  import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
9
- import { join, dirname, extname } from 'node:path';
9
+ import { basename, join, dirname, extname, resolve } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
+ import Database from 'better-sqlite3';
12
+ import { MemoryBrain } from '../memory/MemoryBrain.js';
13
+ import { inspectMemoryProviders, recallMemoryProviders, } from '../memory/MemoryProviders.js';
11
14
  import { dumpCodeGraphData } from '../codegraph/CodeIntelligence.js';
12
15
  import { classifyLayers } from '../topology/LayerClassifier.js';
13
16
  import { mapDomains } from '../topology/DomainMapper.js';
14
17
  import { generateTour } from '../topology/TourGenerator.js';
15
18
  import { aggregateGovernanceMetrics } from './MetricsAggregator.js';
16
19
  import { logger } from '../core/logger.js';
20
+ import { RuntimeEvidenceLedger } from '../runtime/RuntimeEvidenceLedger.js';
21
+ import { optimizeCodingPrompt, } from '../prompts/PromptOptimizer.js';
22
+ import { PhasePromptRegistry } from '../prompts/PhasePromptRegistry.js';
23
+ import { listVisualVibeTemplates } from '../prompts/VibeTemplateGallery.js';
17
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
25
+ const MEMORY_REVIEW_ACTIONS = ['approve', 'reject', 'stale', 'restore'];
18
26
  // ── Dashboard Server ─────────────────────────────────────────────────────
19
27
  export class DashboardServer {
20
28
  constructor(options = {}) {
@@ -27,8 +35,17 @@ export class DashboardServer {
27
35
  this.detectorTracker = options.detectorTracker ?? null;
28
36
  this.port = options.port ?? 3210;
29
37
  this.host = options.host ?? '0.0.0.0';
30
- this.projectDir = options.projectDir ?? process.cwd();
31
- this.scaleDir = options.scaleDir ?? join(this.projectDir, '.scale');
38
+ this.projectDir = resolve(options.projectDir ?? process.cwd());
39
+ this.scaleDir = resolve(options.scaleDir ?? join(this.projectDir, '.scale'));
40
+ this.currentProject = normalizeProjectSummary({
41
+ id: options.currentProjectId,
42
+ name: options.projectName,
43
+ projectDir: this.projectDir,
44
+ scaleDir: this.scaleDir,
45
+ url: options.projectUrl,
46
+ current: true,
47
+ });
48
+ this.projects = normalizeProjectList(options.projects, this.currentProject);
32
49
  this.setupMiddleware();
33
50
  this.setupSPA();
34
51
  this.setupAPI();
@@ -41,10 +58,11 @@ export class DashboardServer {
41
58
  }
42
59
  // ── SPA Serves ───────────────────────────────────────────────────────
43
60
  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;
61
+ // Vue dashboard is the canonical UI and is served from the dashboard root.
62
+ const packagedVueSpa = join(__dirname, 'spa');
63
+ const projectVueSpa = join(__dirname, '..', '..', 'dist', 'dashboard', 'spa');
64
+ const hasVueDashboard = (dir) => existsSync(join(dir, 'index.html')) && existsSync(join(dir, 'assets'));
65
+ const vueSpaDir = hasVueDashboard(packagedVueSpa) ? packagedVueSpa : hasVueDashboard(projectVueSpa) ? projectVueSpa : '';
48
66
  const mimeTypes = {
49
67
  '.html': 'text/html; charset=utf-8',
50
68
  '.js': 'application/javascript; charset=utf-8',
@@ -54,12 +72,18 @@ export class DashboardServer {
54
72
  '.svg': 'image/svg+xml',
55
73
  '.ico': 'image/x-icon',
56
74
  };
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);
75
+ const serveStatic = (c, prefix, dir, fallbackToIndex = false) => {
76
+ const path = c.req.path.replace(prefix, '') || 'index.html';
77
+ const filePath = join(dir, path);
61
78
  if (!existsSync(filePath) || !statSync(filePath).isFile()) {
62
- return c.notFound();
79
+ if (!fallbackToIndex)
80
+ return c.notFound();
81
+ const indexPath = join(dir, 'index.html');
82
+ if (!existsSync(indexPath))
83
+ return c.notFound();
84
+ return new Response(readFileSync(indexPath), {
85
+ headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' },
86
+ });
63
87
  }
64
88
  const ext = extname(filePath);
65
89
  const contentType = mimeTypes[ext] ?? 'application/octet-stream';
@@ -67,9 +91,24 @@ export class DashboardServer {
67
91
  return new Response(content, {
68
92
  headers: { 'Content-Type': contentType, 'Cache-Control': 'no-cache' },
69
93
  });
70
- });
71
- // Root redirect to SPA
72
- this.app.get('/', (c) => c.redirect('/spa/'));
94
+ };
95
+ const serveVueSpa = (c) => {
96
+ if (existsSync(vueSpaDir))
97
+ return serveStatic(c, '/', vueSpaDir, true);
98
+ return c.html('<!doctype html><html><head><title>SCALE Engine Dashboard</title></head><body><main id="app">Run npm run build to generate the Vue dashboard.</main></body></html>', 503);
99
+ };
100
+ this.app.get('/assets/*', (c) => existsSync(vueSpaDir) ? serveStatic(c, '/', vueSpaDir) : c.notFound());
101
+ // Backward-compatible preview URLs from the migration period.
102
+ this.app.get('/spa', (c) => c.redirect('/'));
103
+ this.app.get('/spa/*', (c) => c.redirect('/'));
104
+ this.app.get('/vue', (c) => c.redirect('/'));
105
+ this.app.get('/vue/*', (c) => c.redirect('/'));
106
+ // Root Vue dashboard.
107
+ this.app.get('/', serveVueSpa);
108
+ this.app.get('/favicon.ico', () => new Response(null, {
109
+ status: 204,
110
+ headers: { 'Cache-Control': 'public, max-age=86400' },
111
+ }));
73
112
  // Legacy views (backward compat)
74
113
  const distViews = join(__dirname, 'views');
75
114
  const srcViews = join(__dirname, '..', '..', 'src', 'dashboard', 'views');
@@ -100,6 +139,16 @@ export class DashboardServer {
100
139
  }
101
140
  // ── API Routes ───────────────────────────────────────────────────────
102
141
  setupAPI() {
142
+ // Project metadata for multi-project dashboard launchers
143
+ this.app.get('/api/project', (c) => c.json(this.currentProject));
144
+ this.app.get('/api/projects', (c) => c.json(this.projects));
145
+ this.app.get('/api/projects/summary', (c) => {
146
+ const sinceDays = parsePositiveInt(c.req.query('days'), 7);
147
+ const limit = parsePositiveInt(c.req.query('limit'), 100);
148
+ return c.json(this.getProjectsSummary(sinceDays, limit));
149
+ });
150
+ this.app.get('/api/dashboard/capabilities', (c) => c.json(this.getDashboardCapabilityReport()));
151
+ this.app.get('/api/capabilities', (c) => c.json(this.getDashboardCapabilityReport()));
103
152
  // Full dashboard state
104
153
  this.app.get('/api/state', async (c) => c.json(await this.getDashboardState()));
105
154
  // Artifact tree
@@ -147,6 +196,43 @@ export class DashboardServer {
147
196
  const docPath = c.req.path.replace('/api/documents/', '');
148
197
  return this.serveDocument(docPath, c);
149
198
  });
199
+ // Memory/knowledge view. Provider recall is explicit to keep page load cheap.
200
+ this.app.get('/api/knowledge', async (c) => {
201
+ const query = (c.req.query('query') ?? '').trim();
202
+ const limit = parsePositiveInt(c.req.query('limit'), 20);
203
+ const includeProviders = c.req.query('providers') !== 'false';
204
+ const runRecall = c.req.query('recall') === '1' || c.req.query('recall') === 'true';
205
+ const provider = c.req.query('provider')?.trim() || undefined;
206
+ return c.json(await this.getKnowledgeReport({ query, limit, includeProviders, runRecall, provider }));
207
+ });
208
+ // Repo-native knowledge base: docs, SQLite knowledge, Graphify graph, and gbrain visualization.
209
+ this.app.get('/api/knowledge-base', (c) => c.json(this.getKnowledgeBaseReport()));
210
+ this.app.get('/api/prompts', (c) => c.json(this.getPromptStudioReport()));
211
+ this.app.post('/api/prompts/optimize', async (c) => {
212
+ const body = await c.req.json().catch(() => ({}));
213
+ const rawPrompt = String(body.rawPrompt ?? body.input ?? body.prompt ?? '').trim();
214
+ if (!rawPrompt)
215
+ return c.json({ error: 'Prompt input is required.' }, 400);
216
+ try {
217
+ const result = optimizeCodingPrompt({
218
+ rawPrompt,
219
+ title: typeof body.title === 'string' ? body.title : undefined,
220
+ language: normalizeDashboardPromptLanguage(body.language),
221
+ level: typeof body.level === 'string' ? body.level : undefined,
222
+ files: toStringArray(body.files),
223
+ services: toStringArray(body.services ?? body.service),
224
+ successCriteria: toStringArray(body.successCriteria ?? body['success-criteria']),
225
+ });
226
+ return c.json({
227
+ project: this.currentProject,
228
+ generatedAt: Date.now(),
229
+ result,
230
+ });
231
+ }
232
+ catch (e) {
233
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
234
+ }
235
+ });
150
236
  // Available FSM actions for artifact
151
237
  this.app.get('/api/artifacts/:id/actions', async (c) => {
152
238
  if (!this.fsm)
@@ -294,8 +380,79 @@ export class DashboardServer {
294
380
  return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);
295
381
  }
296
382
  });
383
+ this.app.post('/api/knowledge/local/:id/review', async (c) => {
384
+ const id = c.req.param('id');
385
+ const body = await c.req.json().catch(() => ({}));
386
+ const action = normalizeMemoryReviewAction(body.action);
387
+ if (!action) {
388
+ return c.json({
389
+ error: 'Invalid memory review action',
390
+ allowedActions: MEMORY_REVIEW_ACTIONS,
391
+ }, 400);
392
+ }
393
+ let brain = null;
394
+ try {
395
+ brain = new MemoryBrain({ projectDir: this.projectDir, scaleDir: this.scaleDir });
396
+ const report = brain.review(id, action, {
397
+ reason: body.reason ?? `Dashboard memory review: ${action}`,
398
+ actor: 'dashboard',
399
+ });
400
+ if (!report.ok || !report.node) {
401
+ return c.json({
402
+ error: report.warnings[0] ?? 'Memory review transition blocked',
403
+ warnings: report.warnings,
404
+ action,
405
+ previousStatus: report.previousStatus,
406
+ node: report.node,
407
+ }, report.node ? 422 : 404);
408
+ }
409
+ const evidence = this.recordMemoryReviewEvidence({
410
+ action,
411
+ node: report.node,
412
+ previousStatus: report.previousStatus,
413
+ reason: body.reason,
414
+ });
415
+ return c.json({
416
+ success: true,
417
+ action,
418
+ previousStatus: report.previousStatus,
419
+ node: report.node,
420
+ warnings: report.warnings,
421
+ evidence,
422
+ });
423
+ }
424
+ catch (e) {
425
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);
426
+ }
427
+ finally {
428
+ brain?.close();
429
+ }
430
+ });
297
431
  }
298
432
  // ── Data Collection ──────────────────────────────────────────────────
433
+ recordMemoryReviewEvidence(input) {
434
+ const ledger = new RuntimeEvidenceLedger({
435
+ projectDir: this.projectDir,
436
+ scaleDir: this.scaleDir,
437
+ });
438
+ return ledger.record({
439
+ taskId: 'dashboard-memory-review',
440
+ kind: 'manual',
441
+ status: 'passed',
442
+ title: `Dashboard memory review: ${input.action} ${input.node.id}`,
443
+ summary: `Memory node ${input.node.id} transitioned from ${input.previousStatus ?? 'unknown'} to ${input.node.status}.`,
444
+ artifacts: ['.scale/memory/brain.sqlite', ...input.node.evidencePaths],
445
+ metadata: {
446
+ action: input.action,
447
+ nodeId: input.node.id,
448
+ previousStatus: input.previousStatus,
449
+ nextStatus: input.node.status,
450
+ reason: input.reason,
451
+ source: 'dashboard',
452
+ resolutionKey: `memory-review:${input.node.id}`,
453
+ },
454
+ });
455
+ }
299
456
  async getDashboardState() {
300
457
  const [artifacts, evolutionMetrics, detectorStats, autoDefectStats, recentEvents] = await Promise.all([
301
458
  this.getArtifactTree(),
@@ -382,8 +539,293 @@ export class DashboardServer {
382
539
  const raw = dumpCodeGraphData({ projectDir: this.projectDir });
383
540
  return classifyLayers(raw);
384
541
  }
542
+ getProjectsSummary(sinceDays, limit) {
543
+ const projects = this.projects.slice(0, limit).map(project => this.getProjectOverview(project, sinceDays));
544
+ const warnings = projects.flatMap(project => project.warnings.map(warning => `${project.project.name}: ${warning}`));
545
+ return {
546
+ generatedAt: Date.now(),
547
+ sinceDays,
548
+ currentProjectId: this.currentProject.id,
549
+ totals: {
550
+ projects: projects.length,
551
+ readyProjects: projects.filter(project => project.health === 'ready').length,
552
+ warningProjects: projects.filter(project => project.health === 'warning').length,
553
+ missingProjects: projects.filter(project => project.health === 'missing').length,
554
+ documents: sum(projects, project => project.documents.total),
555
+ localMemoryNodes: sum(projects, project => project.knowledge.total),
556
+ activeMemoryNodes: sum(projects, project => project.knowledge.active),
557
+ commandRuns: sum(projects, project => project.metrics.commandRuns),
558
+ failedCommandRuns: sum(projects, project => project.metrics.failedCommandRuns),
559
+ gateFailures: sum(projects, project => project.metrics.gateFailures),
560
+ },
561
+ projects,
562
+ warnings,
563
+ };
564
+ }
565
+ getProjectOverview(project, sinceDays) {
566
+ const warnings = [];
567
+ const scaleDirExists = existsSync(project.scaleDir);
568
+ if (!scaleDirExists)
569
+ warnings.push('.scale directory is missing');
570
+ const documents = this.listDocumentsFor(project.projectDir, project.scaleDir);
571
+ const knowledge = this.getLocalKnowledgeSummary(project, warnings);
572
+ const metrics = this.getGovernanceMetricSummary(project, sinceDays, warnings);
573
+ const health = !scaleDirExists
574
+ ? 'missing'
575
+ : warnings.length > 0
576
+ ? 'warning'
577
+ : 'ready';
578
+ return {
579
+ project,
580
+ health,
581
+ scaleDirExists,
582
+ documents: {
583
+ total: documents.length,
584
+ byType: countBy(documents, document => document.type),
585
+ },
586
+ knowledge,
587
+ metrics,
588
+ warnings,
589
+ };
590
+ }
591
+ getDashboardCapabilityReport() {
592
+ const warnings = [];
593
+ let metrics = null;
594
+ try {
595
+ metrics = aggregateGovernanceMetrics({
596
+ projectDir: this.projectDir,
597
+ scaleDir: this.scaleDir,
598
+ sinceDays: 7,
599
+ });
600
+ }
601
+ catch (error) {
602
+ warnings.push(`metrics aggregation failed: ${error instanceof Error ? error.message : String(error)}`);
603
+ }
604
+ const scaleDirExists = existsSync(this.scaleDir);
605
+ const documents = this.listDocuments();
606
+ const memoryDb = join(this.scaleDir, 'memory', 'brain.sqlite');
607
+ const modelUsageFile = join(this.scaleDir, 'model-usage', 'usage.jsonl');
608
+ const runtimeEvidenceDir = join(this.scaleDir, 'evidence', 'runtime');
609
+ const commandRunsDir = join(this.scaleDir, 'evidence', 'command-runs');
610
+ const promptReport = this.getPromptStudioReport();
611
+ const knowledgeBaseReport = this.getKnowledgeBaseReport();
612
+ const runtimeEvidenceCount = countMatchingFiles(runtimeEvidenceDir, file => file.endsWith('.json'));
613
+ const commandRunCount = metrics?.commandRuns.total ?? countMatchingFiles(commandRunsDir, file => file.endsWith('.json'));
614
+ const modelUsageCount = metrics?.modelUsage.totalRecords ?? 0;
615
+ const memoryCount = this.getLocalKnowledgeSummary(this.currentProject, warnings).total;
616
+ const busAvailable = Boolean(this.bus);
617
+ const artifactTransitions = Boolean(this.fsm && this.store);
618
+ const dataSources = [
619
+ {
620
+ id: 'project-scale-dir',
621
+ title: 'Project .scale state',
622
+ description: 'Workspace governance state and local evidence root.',
623
+ status: scaleDirExists ? 'ready' : 'missing',
624
+ refreshMode: 'snapshot',
625
+ source: this.scaleDir,
626
+ count: scaleDirExists ? 1 : 0,
627
+ lastUpdated: latestMtime(this.scaleDir),
628
+ emptyReason: scaleDirExists ? undefined : 'The project has no .scale directory yet.',
629
+ action: scaleDirExists ? undefined : 'Run scale bootstrap or initialize the workflow in this project.',
630
+ },
631
+ {
632
+ id: 'runtime-evidence',
633
+ title: 'Runtime evidence ledger',
634
+ description: 'Pass/fail/resolved records produced by governed workflow runs.',
635
+ status: runtimeEvidenceCount > 0 ? 'ready' : 'missing',
636
+ refreshMode: 'polling',
637
+ source: runtimeEvidenceDir,
638
+ count: runtimeEvidenceCount,
639
+ lastUpdated: latestMtime(runtimeEvidenceDir),
640
+ emptyReason: runtimeEvidenceCount > 0 ? undefined : 'No runtime evidence JSON files were found.',
641
+ action: runtimeEvidenceCount > 0 ? undefined : 'Run a governed verify/preflight command that records runtime evidence.',
642
+ },
643
+ {
644
+ id: 'command-runs',
645
+ title: 'Command run evidence',
646
+ description: 'Recorded shell/tool executions, pass rate, and output compression savings.',
647
+ status: commandRunCount > 0 ? 'ready' : 'missing',
648
+ refreshMode: 'polling',
649
+ source: commandRunsDir,
650
+ count: commandRunCount,
651
+ lastUpdated: latestMtime(commandRunsDir),
652
+ emptyReason: commandRunCount > 0 ? undefined : 'No command-run evidence files were found.',
653
+ action: commandRunCount > 0 ? undefined : 'Run commands through the governed runtime or preflight pipeline.',
654
+ },
655
+ {
656
+ id: 'model-usage',
657
+ title: 'Model usage ledger',
658
+ description: 'Actual model token usage and cache savings from scale token record/report.',
659
+ status: modelUsageCount > 0 ? 'ready' : 'missing',
660
+ refreshMode: 'polling',
661
+ source: modelUsageFile,
662
+ count: modelUsageCount,
663
+ lastUpdated: latestMtime(modelUsageFile),
664
+ emptyReason: modelUsageCount > 0 ? undefined : 'No model usage ledger is present, so token/cost charts are empty.',
665
+ action: modelUsageCount > 0 ? undefined : 'Record provider usage with scale token record, then refresh the dashboard.',
666
+ },
667
+ {
668
+ id: 'memory-brain',
669
+ title: 'gbrain memory',
670
+ description: 'Local gbrain memory nodes used by the knowledge page.',
671
+ status: existsSync(memoryDb) ? (memoryCount > 0 ? 'ready' : 'partial') : 'missing',
672
+ refreshMode: 'polling',
673
+ source: memoryDb,
674
+ count: memoryCount,
675
+ lastUpdated: latestMtime(memoryDb),
676
+ emptyReason: existsSync(memoryDb) ? (memoryCount > 0 ? undefined : 'The memory database exists but has no nodes.') : 'No local gbrain database exists.',
677
+ action: memoryCount > 0 ? undefined : 'Capture or approve project memory through the memory workflow.',
678
+ },
679
+ {
680
+ id: 'knowledge-base',
681
+ title: 'Knowledge base',
682
+ description: 'Repo-native knowledge documents, SQLite knowledge entries, Karpathy guidance, and Graphify knowledge graph.',
683
+ status: knowledgeBaseReport.summary.documents + knowledgeBaseReport.summary.entries + knowledgeBaseReport.summary.graphNodes > 0 ? 'ready' : 'missing',
684
+ refreshMode: 'polling',
685
+ source: `${join(this.scaleDir, 'knowledge.db')} + ${join(this.projectDir, 'graphify-out')} + knowledge docs`,
686
+ count: knowledgeBaseReport.summary.documents + knowledgeBaseReport.summary.entries,
687
+ lastUpdated: latestMtimeForDocuments(knowledgeBaseReport.documents, this.projectDir, this.scaleDir)
688
+ ?? latestMtime(join(this.scaleDir, 'knowledge.db'))
689
+ ?? latestMtime(join(this.projectDir, 'graphify-out')),
690
+ emptyReason: knowledgeBaseReport.summary.documents + knowledgeBaseReport.summary.entries + knowledgeBaseReport.summary.graphNodes > 0
691
+ ? undefined
692
+ : 'No knowledge docs, knowledge.db entries, or Graphify graph were found.',
693
+ action: knowledgeBaseReport.summary.documents + knowledgeBaseReport.summary.entries + knowledgeBaseReport.summary.graphNodes > 0
694
+ ? undefined
695
+ : 'Add knowledge docs, run the knowledge ingestion flow, or generate graphify-out/graph.json.',
696
+ },
697
+ {
698
+ id: 'documents',
699
+ title: 'Documents and prototypes',
700
+ description: 'Markdown, JSON, and HTML artifacts available for preview, copy, and download.',
701
+ status: documents.length > 0 ? 'ready' : 'missing',
702
+ refreshMode: 'polling',
703
+ source: `${this.projectDir}/docs + ${this.scaleDir}/docs + ${this.scaleDir}/artifacts + knowledge graph entry docs`,
704
+ count: documents.length,
705
+ lastUpdated: latestMtimeForDocuments(documents, this.projectDir, this.scaleDir),
706
+ emptyReason: documents.length > 0 ? undefined : 'No previewable docs or HTML prototypes were found.',
707
+ action: documents.length > 0 ? undefined : 'Generate or add docs/artifacts under docs, .scale/docs, or .scale/artifacts.',
708
+ },
709
+ {
710
+ id: 'prompt-studio',
711
+ title: 'Prompt Studio',
712
+ description: 'Built-in vibe coding templates, prompt packs, and optimizer API.',
713
+ status: promptReport.summary.vibeTemplates + promptReport.summary.phasePrompts > 0 ? 'ready' : 'missing',
714
+ refreshMode: 'snapshot',
715
+ source: 'src/prompts + .scale/prompts',
716
+ count: promptReport.summary.vibeTemplates + promptReport.summary.phasePrompts + promptReport.summary.packs,
717
+ emptyReason: promptReport.summary.vibeTemplates + promptReport.summary.phasePrompts > 0 ? undefined : 'No prompt templates were discovered.',
718
+ action: promptReport.summary.vibeTemplates + promptReport.summary.phasePrompts > 0 ? undefined : 'Add project prompts under .scale/prompts or use built-in templates.',
719
+ },
720
+ {
721
+ id: 'event-stream',
722
+ title: 'Realtime event stream',
723
+ description: 'Server-sent events used to refresh live runtime changes.',
724
+ status: busAvailable ? 'ready' : 'partial',
725
+ refreshMode: busAvailable ? 'sse' : 'polling',
726
+ source: '/api/stream',
727
+ count: busAvailable ? 1 : 0,
728
+ emptyReason: busAvailable ? undefined : 'The dashboard server is running heartbeat-only SSE because no runtime EventBus was injected.',
729
+ action: busAvailable ? undefined : 'Start the dashboard from an embedded runtime or wire an EventBus into serve.',
730
+ },
731
+ {
732
+ id: 'artifact-fsm',
733
+ title: 'Workflow artifact transitions',
734
+ description: 'Dashboard write path for artifact actions and lesson review transitions.',
735
+ status: artifactTransitions ? 'ready' : 'partial',
736
+ refreshMode: 'manual',
737
+ source: '/api/artifacts/:id/actions + /api/artifacts/:id/transition',
738
+ count: artifactTransitions ? 1 : 0,
739
+ emptyReason: artifactTransitions ? undefined : 'The HTTP dashboard was started without artifact store/FSM injection.',
740
+ action: artifactTransitions ? undefined : 'Wire the serve entrypoint to an artifact store and FSM before enabling dashboard transitions.',
741
+ },
742
+ ];
743
+ const summary = summarizeDataSources(dataSources);
744
+ return {
745
+ project: this.currentProject,
746
+ generatedAt: Date.now(),
747
+ summary,
748
+ realtime: {
749
+ status: busAvailable ? 'ready' : 'partial',
750
+ mode: busAvailable ? 'event-bus' : 'heartbeat-only',
751
+ busAvailable,
752
+ heartbeatOnly: !busAvailable,
753
+ refreshIntervalMs: 30000,
754
+ },
755
+ writeOps: {
756
+ artifactTransitions,
757
+ memoryReview: existsSync(memoryDb),
758
+ promptOptimization: true,
759
+ },
760
+ dataSources,
761
+ warnings: [...warnings, ...promptReport.warnings, ...knowledgeBaseReport.warnings],
762
+ };
763
+ }
764
+ getGovernanceMetricSummary(project, sinceDays, warnings) {
765
+ try {
766
+ const metrics = aggregateGovernanceMetrics({
767
+ projectDir: project.projectDir,
768
+ scaleDir: project.scaleDir,
769
+ sinceDays,
770
+ });
771
+ const commandRuns = metrics.commandRuns.total;
772
+ return {
773
+ available: true,
774
+ commandRuns,
775
+ failedCommandRuns: metrics.commandRuns.failed,
776
+ commandPassRate: commandRuns > 0 ? metrics.commandRuns.passed / commandRuns : 0,
777
+ gateFailures: metrics.gateFailures.failed,
778
+ recentTasks: metrics.taskMetrics.recentTasks,
779
+ recentFirstPassRate: metrics.taskMetrics.recentFirstPassRate,
780
+ };
781
+ }
782
+ catch (error) {
783
+ warnings.push(`governance metrics failed: ${error instanceof Error ? error.message : String(error)}`);
784
+ return {
785
+ available: false,
786
+ commandRuns: 0,
787
+ failedCommandRuns: 0,
788
+ commandPassRate: 0,
789
+ gateFailures: 0,
790
+ recentTasks: 0,
791
+ recentFirstPassRate: 0,
792
+ };
793
+ }
794
+ }
795
+ getLocalKnowledgeSummary(project, warnings) {
796
+ const dbPath = join(project.scaleDir, 'memory', 'brain.sqlite');
797
+ if (!existsSync(dbPath))
798
+ return { available: false, total: 0, active: 0 };
799
+ let brain = null;
800
+ try {
801
+ brain = new MemoryBrain({ projectDir: project.projectDir, scaleDir: project.scaleDir });
802
+ const nodes = brain.list();
803
+ return {
804
+ available: true,
805
+ total: nodes.length,
806
+ active: nodes.filter(node => node.status === 'active').length,
807
+ };
808
+ }
809
+ catch (error) {
810
+ warnings.push(`local memory brain failed: ${error instanceof Error ? error.message : String(error)}`);
811
+ return { available: false, total: 0, active: 0 };
812
+ }
813
+ finally {
814
+ brain?.close();
815
+ }
816
+ }
385
817
  listDocuments() {
818
+ return this.listDocumentsFor(this.projectDir, this.scaleDir);
819
+ }
820
+ listDocumentsFor(projectDir, scaleDir) {
386
821
  const docs = [];
822
+ const addFile = (path) => {
823
+ const fullPath = join(projectDir, path);
824
+ if (!existsSync(fullPath) || !statSync(fullPath).isFile())
825
+ return;
826
+ const stat = statSync(fullPath);
827
+ docs.push({ name: basename(path), path, type: extname(path).slice(1), size: stat.size });
828
+ };
387
829
  const scanDir = (dir, prefix) => {
388
830
  if (!existsSync(dir))
389
831
  return;
@@ -400,10 +842,15 @@ export class DashboardServer {
400
842
  }
401
843
  };
402
844
  // 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;
845
+ scanDir(join(scaleDir, 'docs'), '.scale/docs');
846
+ scanDir(join(scaleDir, 'artifacts'), '.scale/artifacts');
847
+ scanDir(join(scaleDir, 'knowledge'), '.scale/knowledge');
848
+ scanDir(join(scaleDir, 'graphify-knowledge', 'entries'), '.scale/graphify-knowledge/entries');
849
+ scanDir(join(projectDir, 'docs'), 'docs');
850
+ addFile('graphify-out/GRAPH_REPORT.md');
851
+ addFile('graphify-out/graph.json');
852
+ addFile('graphify-out/manifest.json');
853
+ return dedupeDocuments(docs);
407
854
  }
408
855
  serveDocument(docPath, c) {
409
856
  // docPath already includes prefix (e.g., 'docs/foo.md' or '.scale/docs/foo.md')
@@ -422,7 +869,339 @@ export class DashboardServer {
422
869
  }
423
870
  return c.json({ error: 'Document not found' }, 404);
424
871
  }
872
+ async getKnowledgeReport(options) {
873
+ const warnings = [];
874
+ const local = this.getLocalKnowledge(options.query, options.limit, warnings);
875
+ let providers;
876
+ let recall;
877
+ if (options.includeProviders) {
878
+ try {
879
+ providers = inspectMemoryProviders({ projectDir: this.projectDir, scaleDir: this.scaleDir });
880
+ }
881
+ catch (error) {
882
+ warnings.push(`memory provider status failed: ${error instanceof Error ? error.message : String(error)}`);
883
+ }
884
+ }
885
+ if (options.runRecall && options.query.length > 0) {
886
+ try {
887
+ recall = await recallMemoryProviders({
888
+ projectDir: this.projectDir,
889
+ scaleDir: this.scaleDir,
890
+ query: options.query,
891
+ limit: options.limit,
892
+ includeCandidates: true,
893
+ provider: options.provider,
894
+ });
895
+ }
896
+ catch (error) {
897
+ warnings.push(`memory provider recall failed: ${error instanceof Error ? error.message : String(error)}`);
898
+ }
899
+ }
900
+ return { project: this.currentProject, local, providers, recall, warnings };
901
+ }
902
+ getLocalKnowledge(query, limit, warnings) {
903
+ const dbPath = join(this.scaleDir, 'memory', 'brain.sqlite');
904
+ if (!existsSync(dbPath))
905
+ return { available: false, total: 0, byStatus: {}, nodes: [] };
906
+ let brain = null;
907
+ try {
908
+ brain = new MemoryBrain({ projectDir: this.projectDir, scaleDir: this.scaleDir });
909
+ const allNodes = brain.list();
910
+ const nodes = query
911
+ ? brain.query(query, { limit }).nodes
912
+ : allNodes.slice(0, limit);
913
+ return {
914
+ available: true,
915
+ total: allNodes.length,
916
+ byStatus: countBy(allNodes, node => node.status),
917
+ nodes,
918
+ };
919
+ }
920
+ catch (error) {
921
+ warnings.push(`local memory brain failed: ${error instanceof Error ? error.message : String(error)}`);
922
+ return { available: false, total: 0, byStatus: {}, nodes: [] };
923
+ }
924
+ finally {
925
+ brain?.close();
926
+ }
927
+ }
928
+ getKnowledgeBaseReport() {
929
+ const warnings = [];
930
+ const documents = this.listKnowledgeDocuments();
931
+ const entries = this.listKnowledgeEntries(warnings);
932
+ const graph = this.getGraphifyKnowledgeGraph();
933
+ const memoryGraph = this.getGbrainMemoryGraph(warnings);
934
+ if (graph.status === 'error' && graph.emptyReason)
935
+ warnings.push(graph.emptyReason);
936
+ if (memoryGraph.status === 'error' && memoryGraph.emptyReason)
937
+ warnings.push(memoryGraph.emptyReason);
938
+ return {
939
+ project: this.currentProject,
940
+ generatedAt: Date.now(),
941
+ summary: {
942
+ documents: documents.length,
943
+ entries: entries.length,
944
+ graphNodes: graph.nodeCount,
945
+ graphEdges: graph.edgeCount,
946
+ memoryNodes: memoryGraph.nodeCount,
947
+ memoryEdges: memoryGraph.edgeCount,
948
+ },
949
+ documents,
950
+ documentTree: buildDocumentTree(documents),
951
+ entries,
952
+ graph,
953
+ memoryGraph,
954
+ exports: {
955
+ report: '/api/knowledge-base',
956
+ documents: '/api/documents',
957
+ graph: graph.source,
958
+ memoryGraph: '/api/knowledge',
959
+ },
960
+ warnings,
961
+ };
962
+ }
963
+ listKnowledgeDocuments() {
964
+ const docs = this.listDocuments();
965
+ const specialPaths = [
966
+ 'src/skills/karpathy-guidelines/SKILL.md',
967
+ 'docs/CODE_INTELLIGENCE.md',
968
+ 'docs/MEMORY_FABRIC.md',
969
+ 'docs/MEMORY_BRAIN.md',
970
+ 'docs/THIRD_PARTY_SKILLS.md',
971
+ 'docs/EXTERNAL_REFERENCES.md',
972
+ '.scale/code-intelligence.json',
973
+ '.scale/graph/manifest.json',
974
+ 'graphify-out/GRAPH_REPORT.md',
975
+ 'graphify-out/graph.json',
976
+ ];
977
+ for (const path of specialPaths) {
978
+ const fullPath = join(this.projectDir, path);
979
+ if (!existsSync(fullPath) || !statSync(fullPath).isFile())
980
+ continue;
981
+ const stat = statSync(fullPath);
982
+ docs.push({
983
+ name: basename(path),
984
+ path,
985
+ type: extname(path).slice(1),
986
+ size: stat.size,
987
+ });
988
+ }
989
+ return dedupeDocuments(docs).filter(document => isKnowledgeDocument(document.path));
990
+ }
991
+ listKnowledgeEntries(warnings) {
992
+ const dbPath = join(this.scaleDir, 'knowledge.db');
993
+ if (!existsSync(dbPath))
994
+ return [];
995
+ let db = null;
996
+ try {
997
+ db = new Database(dbPath, { readonly: true, fileMustExist: true });
998
+ const rows = db.prepare(`
999
+ SELECT id, title, content, type, tags, score, createdAt, updatedAt, source
1000
+ FROM knowledge_entries
1001
+ ORDER BY updatedAt DESC
1002
+ LIMIT 200
1003
+ `).all();
1004
+ return rows.map(row => ({
1005
+ id: String(row.id),
1006
+ title: String(row.title || row.id),
1007
+ content: String(row.content || ''),
1008
+ type: String(row.type || 'unknown'),
1009
+ tags: parseStringList(row.tags),
1010
+ score: Number(row.score || 0),
1011
+ createdAt: Number(row.createdAt || 0),
1012
+ updatedAt: Number(row.updatedAt || 0),
1013
+ source: typeof row.source === 'string' ? row.source : undefined,
1014
+ }));
1015
+ }
1016
+ catch (error) {
1017
+ warnings.push(`knowledge.db read failed: ${error instanceof Error ? error.message : String(error)}`);
1018
+ return [];
1019
+ }
1020
+ finally {
1021
+ db?.close();
1022
+ }
1023
+ }
1024
+ getGraphifyKnowledgeGraph() {
1025
+ const graphPath = join(this.projectDir, 'graphify-out', 'graph.json');
1026
+ const reportPath = join(this.projectDir, 'graphify-out', 'GRAPH_REPORT.md');
1027
+ const manifestPath = join(this.scaleDir, 'graph', 'manifest.json');
1028
+ const source = existsSync(graphPath) ? 'graphify-out/graph.json' : existsSync(manifestPath) ? '.scale/graph/manifest.json' : 'graphify-out/graph.json';
1029
+ if (!existsSync(graphPath) && !existsSync(manifestPath)) {
1030
+ return {
1031
+ status: 'missing',
1032
+ source,
1033
+ reportPath: existsSync(reportPath) ? 'graphify-out/GRAPH_REPORT.md' : undefined,
1034
+ nodeCount: 0,
1035
+ edgeCount: 0,
1036
+ nodes: [],
1037
+ edges: [],
1038
+ emptyReason: 'Graphify graph was not found.',
1039
+ };
1040
+ }
1041
+ try {
1042
+ const raw = JSON.parse(readFileSync(existsSync(graphPath) ? graphPath : manifestPath, 'utf-8'));
1043
+ const rawNodes = extractGraphNodes(raw);
1044
+ const rawEdges = extractGraphEdges(raw);
1045
+ const nodes = rawNodes.slice(0, 160).map((node, index) => normalizeGraphNode(node, index, 'graphify'));
1046
+ const nodeIds = new Set(nodes.map(node => node.id));
1047
+ const edges = rawEdges
1048
+ .map(edge => normalizeGraphEdge(edge))
1049
+ .filter(edge => edge && nodeIds.has(edge.source) && nodeIds.has(edge.target))
1050
+ .slice(0, 260);
1051
+ return {
1052
+ status: rawNodes.length > 0 ? 'ready' : 'partial',
1053
+ source,
1054
+ reportPath: existsSync(reportPath) ? 'graphify-out/GRAPH_REPORT.md' : undefined,
1055
+ nodeCount: rawNodes.length,
1056
+ edgeCount: rawEdges.length,
1057
+ nodes,
1058
+ edges,
1059
+ emptyReason: rawNodes.length > 0 ? undefined : 'Graphify graph exists but has no readable nodes.',
1060
+ };
1061
+ }
1062
+ catch (error) {
1063
+ return {
1064
+ status: 'error',
1065
+ source,
1066
+ reportPath: existsSync(reportPath) ? 'graphify-out/GRAPH_REPORT.md' : undefined,
1067
+ nodeCount: 0,
1068
+ edgeCount: 0,
1069
+ nodes: [],
1070
+ edges: [],
1071
+ emptyReason: `Graphify graph read failed: ${error instanceof Error ? error.message : String(error)}`,
1072
+ };
1073
+ }
1074
+ }
1075
+ getGbrainMemoryGraph(warnings) {
1076
+ const dbPath = join(this.scaleDir, 'memory', 'brain.sqlite');
1077
+ if (!existsSync(dbPath)) {
1078
+ return {
1079
+ status: 'missing',
1080
+ source: '.scale/memory/brain.sqlite',
1081
+ nodeCount: 0,
1082
+ edgeCount: 0,
1083
+ nodes: [],
1084
+ edges: [],
1085
+ emptyReason: 'No local gbrain database exists.',
1086
+ };
1087
+ }
1088
+ let brain = null;
1089
+ try {
1090
+ brain = new MemoryBrain({ projectDir: this.projectDir, scaleDir: this.scaleDir });
1091
+ const memories = brain.list();
1092
+ const nodes = [];
1093
+ const edges = [];
1094
+ for (const memory of memories.slice(0, 120)) {
1095
+ const memoryId = `memory:${memory.id}`;
1096
+ nodes.push({
1097
+ id: memoryId,
1098
+ label: memory.title || memory.id,
1099
+ kind: memory.type || 'memory',
1100
+ group: memory.layer || memory.status || 'memory',
1101
+ source: 'gbrain',
1102
+ });
1103
+ const layer = memory.layer || 'unknown-layer';
1104
+ const layerId = `layer:${layer}`;
1105
+ if (!nodes.some(node => node.id === layerId)) {
1106
+ nodes.push({
1107
+ id: layerId,
1108
+ label: layer,
1109
+ kind: 'layer',
1110
+ group: 'memory-layer',
1111
+ source: 'gbrain',
1112
+ });
1113
+ }
1114
+ edges.push({ source: memoryId, target: layerId, label: 'layer' });
1115
+ for (const evidencePath of (memory.evidencePaths || []).slice(0, 4)) {
1116
+ const evidenceId = `evidence:${evidencePath}`;
1117
+ if (!nodes.some(node => node.id === evidenceId)) {
1118
+ nodes.push({
1119
+ id: evidenceId,
1120
+ label: basename(evidencePath),
1121
+ kind: 'evidence',
1122
+ group: 'evidence',
1123
+ source: 'gbrain',
1124
+ path: evidencePath,
1125
+ });
1126
+ }
1127
+ edges.push({ source: memoryId, target: evidenceId, label: 'evidence' });
1128
+ }
1129
+ }
1130
+ return {
1131
+ status: memories.length > 0 ? 'ready' : 'partial',
1132
+ source: '.scale/memory/brain.sqlite',
1133
+ nodeCount: nodes.length,
1134
+ edgeCount: edges.length,
1135
+ nodes,
1136
+ edges: edges.slice(0, 240),
1137
+ emptyReason: memories.length > 0 ? undefined : 'The gbrain database exists but has no memory nodes.',
1138
+ };
1139
+ }
1140
+ catch (error) {
1141
+ warnings.push(`gbrain graph failed: ${error instanceof Error ? error.message : String(error)}`);
1142
+ return {
1143
+ status: 'error',
1144
+ source: '.scale/memory/brain.sqlite',
1145
+ nodeCount: 0,
1146
+ edgeCount: 0,
1147
+ nodes: [],
1148
+ edges: [],
1149
+ emptyReason: `gbrain graph failed: ${error instanceof Error ? error.message : String(error)}`,
1150
+ };
1151
+ }
1152
+ finally {
1153
+ brain?.close();
1154
+ }
1155
+ }
425
1156
  // ── Lifecycle ────────────────────────────────────────────────────────
1157
+ getPromptStudioReport() {
1158
+ const warnings = [];
1159
+ let phasePrompts = [];
1160
+ let packs = [];
1161
+ try {
1162
+ const registry = new PhasePromptRegistry(this.projectDir);
1163
+ phasePrompts = registry.listPrompts().map(prompt => ({
1164
+ ...prompt,
1165
+ source: classifyPromptSource(prompt.id),
1166
+ command: prompt.id.includes(':') ? undefined : `scale vibe --phase ${prompt.phase}`,
1167
+ }));
1168
+ packs = registry.listPacks().map(pack => ({
1169
+ id: pack.id,
1170
+ name: pack.name,
1171
+ description: pack.description,
1172
+ phases: pack.phases,
1173
+ templateIds: pack.templates.map(template => template.id),
1174
+ command: `scale vibe --pack ${pack.id}`,
1175
+ }));
1176
+ }
1177
+ catch (error) {
1178
+ warnings.push(`phase prompt registry failed: ${error instanceof Error ? error.message : String(error)}`);
1179
+ }
1180
+ const vibeTemplates = listVisualVibeTemplates().map(template => ({
1181
+ ...template,
1182
+ command: `scale vibe --template ${template.id}`,
1183
+ }));
1184
+ return {
1185
+ project: this.currentProject,
1186
+ generatedAt: Date.now(),
1187
+ summary: {
1188
+ vibeTemplates: vibeTemplates.length,
1189
+ phasePrompts: phasePrompts.length,
1190
+ packs: packs.length,
1191
+ customPrompts: phasePrompts.filter(prompt => prompt.source !== 'builtin').length,
1192
+ },
1193
+ commands: {
1194
+ vibeIndex: 'scale vibe-index',
1195
+ vibeTemplate: 'scale vibe --template <template-id> --app "<project>"',
1196
+ vibePack: 'scale vibe --pack <pack-id> --app "<project>"',
1197
+ promptOptimize: 'scale prompt optimize --input "<raw prompt>" --json',
1198
+ },
1199
+ vibeTemplates,
1200
+ phasePrompts,
1201
+ packs,
1202
+ warnings,
1203
+ };
1204
+ }
426
1205
  async start() {
427
1206
  try {
428
1207
  const { serve } = await import('@hono/node-server');
@@ -456,4 +1235,289 @@ export class DashboardServer {
456
1235
  return this.app;
457
1236
  }
458
1237
  }
1238
+ function normalizeProjectSummary(input) {
1239
+ const projectDir = resolve(input.projectDir);
1240
+ const scaleDir = resolve(input.scaleDir);
1241
+ const name = input.name?.trim() || basename(projectDir) || 'project';
1242
+ return {
1243
+ id: input.id?.trim() || safeProjectId(name),
1244
+ name,
1245
+ projectDir,
1246
+ scaleDir,
1247
+ url: input.url,
1248
+ current: input.current,
1249
+ };
1250
+ }
1251
+ function normalizeProjectList(projects, currentProject) {
1252
+ const input = projects && projects.length > 0 ? projects : [currentProject];
1253
+ const seen = new Set();
1254
+ const normalized = [];
1255
+ for (const project of input) {
1256
+ const item = normalizeProjectSummary({
1257
+ ...project,
1258
+ current: project.id === currentProject.id || project.projectDir === currentProject.projectDir,
1259
+ });
1260
+ let id = item.id;
1261
+ let suffix = 2;
1262
+ while (seen.has(id))
1263
+ id = `${item.id}-${suffix++}`;
1264
+ seen.add(id);
1265
+ normalized.push({ ...item, id });
1266
+ }
1267
+ if (!normalized.some(project => project.current)) {
1268
+ normalized.unshift(currentProject);
1269
+ }
1270
+ return normalized.map(project => ({ ...project, current: project.projectDir === currentProject.projectDir }));
1271
+ }
1272
+ function safeProjectId(name) {
1273
+ const id = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
1274
+ return id || 'project';
1275
+ }
1276
+ function parsePositiveInt(value, fallback) {
1277
+ const parsed = Number(value);
1278
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
1279
+ }
1280
+ function classifyPromptSource(id) {
1281
+ if (id.startsWith('project:'))
1282
+ return 'project';
1283
+ if (id.startsWith('global:'))
1284
+ return 'global';
1285
+ return 'builtin';
1286
+ }
1287
+ function normalizeDashboardPromptLanguage(value) {
1288
+ const normalized = String(value ?? 'auto').trim().toLowerCase();
1289
+ return normalized === 'zh' || normalized === 'en' || normalized === 'auto' ? normalized : 'auto';
1290
+ }
1291
+ function toStringArray(value) {
1292
+ if (Array.isArray(value))
1293
+ return value.map(item => String(item).trim()).filter(Boolean);
1294
+ if (typeof value === 'string')
1295
+ return value.split(',').map(item => item.trim()).filter(Boolean);
1296
+ return [];
1297
+ }
1298
+ function normalizeMemoryReviewAction(value) {
1299
+ if (!value)
1300
+ return null;
1301
+ return MEMORY_REVIEW_ACTIONS.includes(value) ? value : null;
1302
+ }
1303
+ function dedupeDocuments(documents) {
1304
+ const seen = new Map();
1305
+ for (const document of documents) {
1306
+ if (!seen.has(document.path))
1307
+ seen.set(document.path, document);
1308
+ }
1309
+ return [...seen.values()].sort((left, right) => left.path.localeCompare(right.path));
1310
+ }
1311
+ function buildDocumentTree(documents) {
1312
+ const roots = [];
1313
+ const folderByPath = new Map();
1314
+ const ensureFolder = (parts, depth) => {
1315
+ if (depth >= parts.length)
1316
+ return roots;
1317
+ const path = parts.slice(0, depth + 1).join('/');
1318
+ const parentChildren = depth === 0 ? roots : ensureFolder(parts, depth - 1);
1319
+ let folder = folderByPath.get(path);
1320
+ if (!folder) {
1321
+ folder = { name: parts[depth] || path, path, type: 'folder', children: [] };
1322
+ folderByPath.set(path, folder);
1323
+ parentChildren.push(folder);
1324
+ }
1325
+ return folder.children ?? [];
1326
+ };
1327
+ for (const document of documents) {
1328
+ const parts = document.path.split('/').filter(Boolean);
1329
+ const folderParts = parts.slice(0, -1);
1330
+ const children = folderParts.length > 0 ? ensureFolder(folderParts, folderParts.length - 1) : roots;
1331
+ children.push({
1332
+ name: document.name,
1333
+ path: document.path,
1334
+ type: 'document',
1335
+ size: document.size,
1336
+ docType: document.type,
1337
+ });
1338
+ }
1339
+ const sortTree = (nodes) => {
1340
+ nodes.sort((left, right) => {
1341
+ if (left.type !== right.type)
1342
+ return left.type === 'folder' ? -1 : 1;
1343
+ return left.name.localeCompare(right.name);
1344
+ });
1345
+ for (const node of nodes)
1346
+ sortTree(node.children ?? []);
1347
+ };
1348
+ sortTree(roots);
1349
+ return roots;
1350
+ }
1351
+ function isKnowledgeDocument(path) {
1352
+ const normalized = path.toLowerCase();
1353
+ const keywords = [
1354
+ 'knowledge',
1355
+ 'memory',
1356
+ 'graph',
1357
+ 'code-intelligence',
1358
+ 'code_intelligence',
1359
+ 'karpathy',
1360
+ 'llm',
1361
+ 'context',
1362
+ 'skill',
1363
+ 'workflow',
1364
+ 'governance',
1365
+ ];
1366
+ return normalized.startsWith('.scale/knowledge/')
1367
+ || normalized.startsWith('.scale/graphify-knowledge/')
1368
+ || normalized.startsWith('graphify-out/')
1369
+ || keywords.some(keyword => normalized.includes(keyword));
1370
+ }
1371
+ function parseStringList(value) {
1372
+ if (Array.isArray(value))
1373
+ return value.map(item => String(item).trim()).filter(Boolean);
1374
+ if (typeof value !== 'string')
1375
+ return [];
1376
+ const trimmed = value.trim();
1377
+ if (!trimmed)
1378
+ return [];
1379
+ try {
1380
+ const parsed = JSON.parse(trimmed);
1381
+ if (Array.isArray(parsed))
1382
+ return parsed.map(item => String(item).trim()).filter(Boolean);
1383
+ }
1384
+ catch {
1385
+ // Fall back to comma-separated tags.
1386
+ }
1387
+ return trimmed.split(',').map(item => item.trim()).filter(Boolean);
1388
+ }
1389
+ function extractGraphNodes(raw) {
1390
+ const record = asRecord(raw);
1391
+ const candidates = [
1392
+ record.nodes,
1393
+ asRecord(record.graph).nodes,
1394
+ asRecord(record.elements).nodes,
1395
+ asRecord(record.data).nodes,
1396
+ ];
1397
+ for (const candidate of candidates) {
1398
+ if (!Array.isArray(candidate))
1399
+ continue;
1400
+ return candidate
1401
+ .map(item => asRecord(asRecord(item).data ?? item))
1402
+ .filter(item => Object.keys(item).length > 0);
1403
+ }
1404
+ return [];
1405
+ }
1406
+ function extractGraphEdges(raw) {
1407
+ const record = asRecord(raw);
1408
+ const candidates = [
1409
+ record.edges,
1410
+ record.links,
1411
+ asRecord(record.graph).edges,
1412
+ asRecord(record.graph).links,
1413
+ asRecord(record.elements).edges,
1414
+ asRecord(record.data).edges,
1415
+ ];
1416
+ for (const candidate of candidates) {
1417
+ if (!Array.isArray(candidate))
1418
+ continue;
1419
+ return candidate
1420
+ .map(item => asRecord(asRecord(item).data ?? item))
1421
+ .filter(item => Object.keys(item).length > 0);
1422
+ }
1423
+ return [];
1424
+ }
1425
+ function normalizeGraphNode(node, index, source) {
1426
+ const id = firstString(node.id, node.key, node.name, node.path, node.label) || `${source}:node:${index}`;
1427
+ const label = firstString(node.label, node.name, node.title, node.path, node.id) || id;
1428
+ const kind = firstString(node.kind, node.type, node.category, node.nodeType) || 'node';
1429
+ const group = firstString(node.group, node.layer, node.domain, node.package, node.kind, node.type) || kind;
1430
+ return {
1431
+ id,
1432
+ label,
1433
+ kind,
1434
+ group,
1435
+ source,
1436
+ path: firstString(node.path, node.file, node.filePath),
1437
+ };
1438
+ }
1439
+ function normalizeGraphEdge(edge) {
1440
+ const source = firstString(edge.source, edge.from, edge.src, edge.start, edge.sourceId);
1441
+ const target = firstString(edge.target, edge.to, edge.dst, edge.end, edge.targetId);
1442
+ if (!source || !target)
1443
+ return null;
1444
+ return {
1445
+ source,
1446
+ target,
1447
+ label: firstString(edge.label, edge.type, edge.kind, edge.relation),
1448
+ };
1449
+ }
1450
+ function asRecord(value) {
1451
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
1452
+ }
1453
+ function firstString(...values) {
1454
+ for (const value of values) {
1455
+ if (typeof value === 'string' && value.trim())
1456
+ return value.trim();
1457
+ if (typeof value === 'number' && Number.isFinite(value))
1458
+ return String(value);
1459
+ }
1460
+ return undefined;
1461
+ }
1462
+ function countBy(items, selector) {
1463
+ const counts = {};
1464
+ for (const item of items) {
1465
+ const key = selector(item);
1466
+ counts[key] = (counts[key] ?? 0) + 1;
1467
+ }
1468
+ return counts;
1469
+ }
1470
+ function sum(items, selector) {
1471
+ return items.reduce((total, item) => total + selector(item), 0);
1472
+ }
1473
+ function summarizeDataSources(dataSources) {
1474
+ return {
1475
+ total: dataSources.length,
1476
+ ready: dataSources.filter(source => source.status === 'ready').length,
1477
+ partial: dataSources.filter(source => source.status === 'partial').length,
1478
+ missing: dataSources.filter(source => source.status === 'missing').length,
1479
+ error: dataSources.filter(source => source.status === 'error').length,
1480
+ };
1481
+ }
1482
+ function countMatchingFiles(dir, predicate) {
1483
+ if (!existsSync(dir))
1484
+ return 0;
1485
+ let count = 0;
1486
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1487
+ const absolute = join(dir, entry.name);
1488
+ if (entry.isDirectory()) {
1489
+ count += countMatchingFiles(absolute, predicate);
1490
+ continue;
1491
+ }
1492
+ if (entry.isFile() && predicate(entry.name))
1493
+ count += 1;
1494
+ }
1495
+ return count;
1496
+ }
1497
+ function latestMtime(path) {
1498
+ if (!existsSync(path))
1499
+ return undefined;
1500
+ const stat = statSync(path);
1501
+ if (stat.isFile())
1502
+ return stat.mtimeMs;
1503
+ let latest = stat.mtimeMs;
1504
+ for (const entry of readdirSync(path, { withFileTypes: true })) {
1505
+ const child = latestMtime(join(path, entry.name));
1506
+ if (child && child > latest)
1507
+ latest = child;
1508
+ }
1509
+ return latest;
1510
+ }
1511
+ function latestMtimeForDocuments(documents, projectDir, scaleDir) {
1512
+ let latest;
1513
+ for (const document of documents) {
1514
+ const candidates = [join(projectDir, document.path), join(scaleDir, document.path)];
1515
+ for (const candidate of candidates) {
1516
+ const value = latestMtime(candidate);
1517
+ if (value && (!latest || value > latest))
1518
+ latest = value;
1519
+ }
1520
+ }
1521
+ return latest;
1522
+ }
459
1523
  //# sourceMappingURL=DashboardServer.js.map