@hongmaple0820/scale-engine 0.50.1 → 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 (50) hide show
  1. package/README.en.md +2 -2
  2. package/README.md +2 -2
  3. package/dist/api/http.js +3 -1
  4. package/dist/api/http.js.map +1 -1
  5. package/dist/cli/cortexCommands.d.ts +16 -0
  6. package/dist/cli/cortexCommands.js +47 -4
  7. package/dist/cli/cortexCommands.js.map +1 -1
  8. package/dist/cortex/InstinctStore.d.ts +13 -1
  9. package/dist/cortex/InstinctStore.js +90 -11
  10. package/dist/cortex/InstinctStore.js.map +1 -1
  11. package/dist/cortex/SessionInjector.js +39 -2
  12. package/dist/cortex/SessionInjector.js.map +1 -1
  13. package/dist/dashboard/DashboardServer.d.ts +158 -0
  14. package/dist/dashboard/DashboardServer.js +753 -13
  15. package/dist/dashboard/DashboardServer.js.map +1 -1
  16. package/dist/dashboard/spa/assets/index-VYBCLBje.js +11 -0
  17. package/dist/dashboard/spa/assets/index-VhwY_ac1.css +1 -0
  18. package/dist/dashboard/spa/assets/naive-ui-BQy2AJkt.js +3340 -0
  19. package/dist/dashboard/spa/assets/vendor-BPU6aOYA.js +3 -0
  20. package/dist/dashboard/spa/assets/vue-CQQMb5Wi.js +17 -0
  21. package/dist/dashboard/spa/index.html +15 -462
  22. package/dist/memory/MemoryFabric.d.ts +13 -1
  23. package/dist/memory/MemoryFabric.js +60 -0
  24. package/dist/memory/MemoryFabric.js.map +1 -1
  25. package/dist/version.d.ts +1 -1
  26. package/dist/version.js +1 -1
  27. package/docs/workflow/ASSESSMENT_INDEX.md +326 -0
  28. package/docs/workflow/COMPARATIVE_ANALYSIS.md +422 -0
  29. package/docs/workflow/EXECUTIVE_SUMMARY.md +310 -0
  30. package/docs/workflow/IMPROVEMENT_CHECKLIST.md +518 -0
  31. package/docs/workflow/IMPROVEMENT_ROADMAP.md +707 -0
  32. package/docs/workflow/README.md +8 -0
  33. package/package.json +6 -2
  34. package/dist/dashboard/spa/app.js +0 -515
  35. package/dist/dashboard/spa/components/DataTable.js +0 -53
  36. package/dist/dashboard/spa/components/EventStream.js +0 -66
  37. package/dist/dashboard/spa/components/LoadingState.js +0 -39
  38. package/dist/dashboard/spa/components/MetricCard.js +0 -30
  39. package/dist/dashboard/spa/components/Panel.js +0 -27
  40. package/dist/dashboard/spa/components/StatusBadge.js +0 -51
  41. package/dist/dashboard/spa/i18n.js +0 -767
  42. package/dist/dashboard/spa/pages/costs.js +0 -522
  43. package/dist/dashboard/spa/pages/documents.js +0 -540
  44. package/dist/dashboard/spa/pages/knowledge.js +0 -457
  45. package/dist/dashboard/spa/pages/monitoring.js +0 -361
  46. package/dist/dashboard/spa/pages/overview.js +0 -301
  47. package/dist/dashboard/spa/pages/topology-renderers.js +0 -251
  48. package/dist/dashboard/spa/pages/topology.js +0 -370
  49. package/dist/dashboard/spa/pages/workflow-renderers.js +0 -239
  50. package/dist/dashboard/spa/pages/workflow.js +0 -217
@@ -8,6 +8,7 @@ import { streamSSE } from 'hono/streaming';
8
8
  import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
9
9
  import { basename, join, dirname, extname, resolve } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
+ import Database from 'better-sqlite3';
11
12
  import { MemoryBrain } from '../memory/MemoryBrain.js';
12
13
  import { inspectMemoryProviders, recallMemoryProviders, } from '../memory/MemoryProviders.js';
13
14
  import { dumpCodeGraphData } from '../codegraph/CodeIntelligence.js';
@@ -17,6 +18,9 @@ import { generateTour } from '../topology/TourGenerator.js';
17
18
  import { aggregateGovernanceMetrics } from './MetricsAggregator.js';
18
19
  import { logger } from '../core/logger.js';
19
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';
20
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
21
25
  const MEMORY_REVIEW_ACTIONS = ['approve', 'reject', 'stale', 'restore'];
22
26
  // ── Dashboard Server ─────────────────────────────────────────────────────
@@ -54,10 +58,11 @@ export class DashboardServer {
54
58
  }
55
59
  // ── SPA Serves ───────────────────────────────────────────────────────
56
60
  setupSPA() {
57
- // Resolve SPA dir: try dist/spa first, fall back to src/spa
58
- const distSpa = join(__dirname, 'spa');
59
- const srcSpa = join(__dirname, '..', '..', 'src', 'dashboard', 'spa');
60
- 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 : '';
61
66
  const mimeTypes = {
62
67
  '.html': 'text/html; charset=utf-8',
63
68
  '.js': 'application/javascript; charset=utf-8',
@@ -67,12 +72,18 @@ export class DashboardServer {
67
72
  '.svg': 'image/svg+xml',
68
73
  '.ico': 'image/x-icon',
69
74
  };
70
- // Serve SPA static files
71
- this.app.get('/spa/*', async (c) => {
72
- const path = c.req.path.replace('/spa/', '') || 'index.html';
73
- 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);
74
78
  if (!existsSync(filePath) || !statSync(filePath).isFile()) {
75
- 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
+ });
76
87
  }
77
88
  const ext = extname(filePath);
78
89
  const contentType = mimeTypes[ext] ?? 'application/octet-stream';
@@ -80,9 +91,20 @@ export class DashboardServer {
80
91
  return new Response(content, {
81
92
  headers: { 'Content-Type': contentType, 'Cache-Control': 'no-cache' },
82
93
  });
83
- });
84
- // Root redirect to SPA
85
- 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);
86
108
  this.app.get('/favicon.ico', () => new Response(null, {
87
109
  status: 204,
88
110
  headers: { 'Cache-Control': 'public, max-age=86400' },
@@ -125,6 +147,8 @@ export class DashboardServer {
125
147
  const limit = parsePositiveInt(c.req.query('limit'), 100);
126
148
  return c.json(this.getProjectsSummary(sinceDays, limit));
127
149
  });
150
+ this.app.get('/api/dashboard/capabilities', (c) => c.json(this.getDashboardCapabilityReport()));
151
+ this.app.get('/api/capabilities', (c) => c.json(this.getDashboardCapabilityReport()));
128
152
  // Full dashboard state
129
153
  this.app.get('/api/state', async (c) => c.json(await this.getDashboardState()));
130
154
  // Artifact tree
@@ -181,6 +205,34 @@ export class DashboardServer {
181
205
  const provider = c.req.query('provider')?.trim() || undefined;
182
206
  return c.json(await this.getKnowledgeReport({ query, limit, includeProviders, runRecall, provider }));
183
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
+ });
184
236
  // Available FSM actions for artifact
185
237
  this.app.get('/api/artifacts/:id/actions', async (c) => {
186
238
  if (!this.fsm)
@@ -536,6 +588,179 @@ export class DashboardServer {
536
588
  warnings,
537
589
  };
538
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
+ }
539
764
  getGovernanceMetricSummary(project, sinceDays, warnings) {
540
765
  try {
541
766
  const metrics = aggregateGovernanceMetrics({
@@ -594,6 +819,13 @@ export class DashboardServer {
594
819
  }
595
820
  listDocumentsFor(projectDir, scaleDir) {
596
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
+ };
597
829
  const scanDir = (dir, prefix) => {
598
830
  if (!existsSync(dir))
599
831
  return;
@@ -612,8 +844,13 @@ export class DashboardServer {
612
844
  // Scan common doc locations
613
845
  scanDir(join(scaleDir, 'docs'), '.scale/docs');
614
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');
615
849
  scanDir(join(projectDir, 'docs'), 'docs');
616
- return docs;
850
+ addFile('graphify-out/GRAPH_REPORT.md');
851
+ addFile('graphify-out/graph.json');
852
+ addFile('graphify-out/manifest.json');
853
+ return dedupeDocuments(docs);
617
854
  }
618
855
  serveDocument(docPath, c) {
619
856
  // docPath already includes prefix (e.g., 'docs/foo.md' or '.scale/docs/foo.md')
@@ -688,7 +925,283 @@ export class DashboardServer {
688
925
  brain?.close();
689
926
  }
690
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
+ }
691
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
+ }
692
1205
  async start() {
693
1206
  try {
694
1207
  const { serve } = await import('@hono/node-server');
@@ -764,11 +1277,188 @@ function parsePositiveInt(value, fallback) {
764
1277
  const parsed = Number(value);
765
1278
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
766
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
+ }
767
1298
  function normalizeMemoryReviewAction(value) {
768
1299
  if (!value)
769
1300
  return null;
770
1301
  return MEMORY_REVIEW_ACTIONS.includes(value) ? value : null;
771
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
+ }
772
1462
  function countBy(items, selector) {
773
1463
  const counts = {};
774
1464
  for (const item of items) {
@@ -780,4 +1470,54 @@ function countBy(items, selector) {
780
1470
  function sum(items, selector) {
781
1471
  return items.reduce((total, item) => total + selector(item), 0);
782
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
+ }
783
1523
  //# sourceMappingURL=DashboardServer.js.map