@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.
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/api/http.js +3 -1
- package/dist/api/http.js.map +1 -1
- package/dist/cli/cortexCommands.d.ts +16 -0
- package/dist/cli/cortexCommands.js +47 -4
- package/dist/cli/cortexCommands.js.map +1 -1
- package/dist/cortex/InstinctStore.d.ts +13 -1
- package/dist/cortex/InstinctStore.js +90 -11
- package/dist/cortex/InstinctStore.js.map +1 -1
- package/dist/cortex/SessionInjector.js +39 -2
- package/dist/cortex/SessionInjector.js.map +1 -1
- package/dist/dashboard/DashboardServer.d.ts +158 -0
- package/dist/dashboard/DashboardServer.js +753 -13
- package/dist/dashboard/DashboardServer.js.map +1 -1
- package/dist/dashboard/spa/assets/index-VYBCLBje.js +11 -0
- package/dist/dashboard/spa/assets/index-VhwY_ac1.css +1 -0
- package/dist/dashboard/spa/assets/naive-ui-BQy2AJkt.js +3340 -0
- package/dist/dashboard/spa/assets/vendor-BPU6aOYA.js +3 -0
- package/dist/dashboard/spa/assets/vue-CQQMb5Wi.js +17 -0
- package/dist/dashboard/spa/index.html +15 -462
- package/dist/memory/MemoryFabric.d.ts +13 -1
- package/dist/memory/MemoryFabric.js +60 -0
- package/dist/memory/MemoryFabric.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/workflow/ASSESSMENT_INDEX.md +326 -0
- package/docs/workflow/COMPARATIVE_ANALYSIS.md +422 -0
- package/docs/workflow/EXECUTIVE_SUMMARY.md +310 -0
- package/docs/workflow/IMPROVEMENT_CHECKLIST.md +518 -0
- package/docs/workflow/IMPROVEMENT_ROADMAP.md +707 -0
- package/docs/workflow/README.md +8 -0
- package/package.json +6 -2
- package/dist/dashboard/spa/app.js +0 -515
- package/dist/dashboard/spa/components/DataTable.js +0 -53
- package/dist/dashboard/spa/components/EventStream.js +0 -66
- package/dist/dashboard/spa/components/LoadingState.js +0 -39
- package/dist/dashboard/spa/components/MetricCard.js +0 -30
- package/dist/dashboard/spa/components/Panel.js +0 -27
- package/dist/dashboard/spa/components/StatusBadge.js +0 -51
- package/dist/dashboard/spa/i18n.js +0 -767
- package/dist/dashboard/spa/pages/costs.js +0 -522
- package/dist/dashboard/spa/pages/documents.js +0 -540
- package/dist/dashboard/spa/pages/knowledge.js +0 -457
- package/dist/dashboard/spa/pages/monitoring.js +0 -361
- package/dist/dashboard/spa/pages/overview.js +0 -301
- package/dist/dashboard/spa/pages/topology-renderers.js +0 -251
- package/dist/dashboard/spa/pages/topology.js +0 -370
- package/dist/dashboard/spa/pages/workflow-renderers.js +0 -239
- 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
|
-
//
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
const
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|