@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.
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/api/DashboardHttpConfig.d.ts +28 -0
- package/dist/api/DashboardHttpConfig.js +110 -0
- package/dist/api/DashboardHttpConfig.js.map +1 -0
- package/dist/api/cli.js +102 -11
- package/dist/api/cli.js.map +1 -1
- package/dist/api/http.d.ts +1 -0
- package/dist/api/http.js +52 -0
- package/dist/api/http.js.map +1 -0
- package/dist/artifact/types.d.ts +5 -0
- package/dist/artifact/types.js.map +1 -1
- package/dist/bootstrap/DependencyBootstrap.d.ts +1 -0
- package/dist/bootstrap/DependencyBootstrap.js +14 -3
- package/dist/bootstrap/DependencyBootstrap.js.map +1 -1
- package/dist/cli/cortexApplyCommand.d.ts +26 -0
- package/dist/cli/cortexApplyCommand.js +74 -0
- package/dist/cli/cortexApplyCommand.js.map +1 -0
- package/dist/cli/cortexCandidateCommands.d.ts +42 -0
- package/dist/cli/cortexCandidateCommands.js +119 -0
- package/dist/cli/cortexCandidateCommands.js.map +1 -0
- package/dist/cli/cortexCommands.d.ts +31 -0
- package/dist/cli/cortexCommands.js +102 -17
- package/dist/cli/cortexCommands.js.map +1 -1
- package/dist/cli/engineBootstrap.d.ts +1 -1
- package/dist/cli/engineBootstrap.js +2 -0
- package/dist/cli/engineBootstrap.js.map +1 -1
- package/dist/cli/evalCommands.js +1 -0
- package/dist/cli/evalCommands.js.map +1 -1
- package/dist/cli/phaseCommands.d.ts +28 -0
- package/dist/cli/phaseCommands.js +148 -9
- package/dist/cli/phaseCommands.js.map +1 -1
- package/dist/cli/runtimeSkillCommands.js +12 -2
- package/dist/cli/runtimeSkillCommands.js.map +1 -1
- package/dist/cli/shieldCommands.d.ts +1 -0
- package/dist/cli/shieldCommands.js +20 -7
- package/dist/cli/shieldCommands.js.map +1 -1
- package/dist/cli/workflowEvidenceCommands.d.ts +120 -0
- package/dist/cli/workflowEvidenceCommands.js +228 -2
- package/dist/cli/workflowEvidenceCommands.js.map +1 -1
- package/dist/cortex/AutoFixEventObservations.d.ts +11 -0
- package/dist/cortex/AutoFixEventObservations.js +72 -0
- package/dist/cortex/AutoFixEventObservations.js.map +1 -0
- package/dist/cortex/GateEvidenceObservations.d.ts +22 -0
- package/dist/cortex/GateEvidenceObservations.js +179 -0
- package/dist/cortex/GateEvidenceObservations.js.map +1 -0
- package/dist/cortex/GovernanceMetrics.d.ts +2 -0
- package/dist/cortex/GovernanceMetrics.js +112 -22
- package/dist/cortex/GovernanceMetrics.js.map +1 -1
- package/dist/cortex/InstinctApplicationRecorder.d.ts +28 -0
- package/dist/cortex/InstinctApplicationRecorder.js +145 -0
- package/dist/cortex/InstinctApplicationRecorder.js.map +1 -0
- package/dist/cortex/InstinctCandidateAudit.d.ts +3 -0
- package/dist/cortex/InstinctCandidateAudit.js +39 -0
- package/dist/cortex/InstinctCandidateAudit.js.map +1 -0
- package/dist/cortex/InstinctCandidateReview.d.ts +32 -0
- package/dist/cortex/InstinctCandidateReview.js +125 -0
- package/dist/cortex/InstinctCandidateReview.js.map +1 -0
- package/dist/cortex/InstinctExtractor.d.ts +1 -0
- package/dist/cortex/InstinctExtractor.js +24 -17
- package/dist/cortex/InstinctExtractor.js.map +1 -1
- package/dist/cortex/InstinctRuntimeEvidence.d.ts +14 -0
- package/dist/cortex/InstinctRuntimeEvidence.js +120 -0
- package/dist/cortex/InstinctRuntimeEvidence.js.map +1 -0
- package/dist/cortex/InstinctStore.d.ts +31 -4
- package/dist/cortex/InstinctStore.js +120 -20
- package/dist/cortex/InstinctStore.js.map +1 -1
- package/dist/cortex/SessionInjector.d.ts +1 -0
- package/dist/cortex/SessionInjector.js +54 -4
- package/dist/cortex/SessionInjector.js.map +1 -1
- package/dist/dashboard/DashboardServer.d.ts +237 -0
- package/dist/dashboard/DashboardServer.js +1083 -19
- 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 +16 -0
- package/dist/env/EnvironmentDoctor.js +12 -7
- package/dist/env/EnvironmentDoctor.js.map +1 -1
- package/dist/eval/WorkflowEval.d.ts +9 -0
- package/dist/eval/WorkflowEval.js +348 -2
- package/dist/eval/WorkflowEval.js.map +1 -1
- package/dist/memory/MemoryBrain.d.ts +13 -0
- package/dist/memory/MemoryBrain.js +47 -0
- package/dist/memory/MemoryBrain.js.map +1 -1
- package/dist/memory/MemoryFabric.d.ts +14 -1
- package/dist/memory/MemoryFabric.js +72 -8
- package/dist/memory/MemoryFabric.js.map +1 -1
- package/dist/memory/MemoryLearning.d.ts +1 -0
- package/dist/memory/MemoryLearning.js +6 -3
- package/dist/memory/MemoryLearning.js.map +1 -1
- package/dist/memory/MemoryProviders.d.ts +8 -1
- package/dist/memory/MemoryProviders.js +143 -29
- package/dist/memory/MemoryProviders.js.map +1 -1
- package/dist/runtime/AiOsRuntime.d.ts +14 -1
- package/dist/runtime/AiOsRuntime.js +59 -3
- package/dist/runtime/AiOsRuntime.js.map +1 -1
- package/dist/runtime/RuntimeDoctor.js +3 -1
- package/dist/runtime/RuntimeDoctor.js.map +1 -1
- package/dist/runtime/RuntimeEvidenceLedger.d.ts +6 -0
- package/dist/runtime/RuntimeEvidenceLedger.js +52 -1
- package/dist/runtime/RuntimeEvidenceLedger.js.map +1 -1
- package/dist/runtime/SessionLedger.d.ts +2 -0
- package/dist/runtime/SessionLedger.js +4 -0
- package/dist/runtime/SessionLedger.js.map +1 -1
- package/dist/setup/SetupVerification.js +53 -5
- package/dist/setup/SetupVerification.js.map +1 -1
- package/dist/shield/PolicyCompiler.js +73 -12
- package/dist/shield/PolicyCompiler.js.map +1 -1
- package/dist/shield/ProtectedPaths.js +4 -2
- package/dist/shield/ProtectedPaths.js.map +1 -1
- package/dist/skills/SkillCatalog.d.ts +2 -0
- package/dist/skills/SkillCatalog.js +8 -0
- package/dist/skills/SkillCatalog.js.map +1 -1
- package/dist/skills/SkillDoctor.d.ts +19 -2
- package/dist/skills/SkillDoctor.js +163 -13
- package/dist/skills/SkillDoctor.js.map +1 -1
- package/dist/tools/SafeCommandRunner.d.ts +1 -0
- package/dist/tools/SafeCommandRunner.js +1 -0
- package/dist/tools/SafeCommandRunner.js.map +1 -1
- package/dist/tools/ToolCapabilityRegistry.js +25 -3
- package/dist/tools/ToolCapabilityRegistry.js.map +1 -1
- package/dist/tools/ToolOrchestrator.js +21 -0
- package/dist/tools/ToolOrchestrator.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workflow/AgentLoopReadiness.d.ts +103 -0
- package/dist/workflow/AgentLoopReadiness.js +371 -0
- package/dist/workflow/AgentLoopReadiness.js.map +1 -0
- package/dist/workflow/EcosystemReadinessGate.d.ts +46 -0
- package/dist/workflow/EcosystemReadinessGate.js +126 -0
- package/dist/workflow/EcosystemReadinessGate.js.map +1 -0
- package/dist/workflow/EngineeringStandards.js +48 -3
- package/dist/workflow/EngineeringStandards.js.map +1 -1
- package/dist/workflow/GateCatalog.js +9 -0
- package/dist/workflow/GateCatalog.js.map +1 -1
- package/dist/workflow/GovernanceTemplatePacks.js +2 -26
- package/dist/workflow/GovernanceTemplatePacks.js.map +1 -1
- package/dist/workflow/GovernanceTemplates.js +8 -1
- package/dist/workflow/GovernanceTemplates.js.map +1 -1
- package/dist/workflow/ReleaseDeploymentLedger.d.ts +63 -0
- package/dist/workflow/ReleaseDeploymentLedger.js +154 -0
- package/dist/workflow/ReleaseDeploymentLedger.js.map +1 -0
- package/dist/workflow/ReviewAnalyzer.js +50 -3
- package/dist/workflow/ReviewAnalyzer.js.map +1 -1
- package/dist/workflow/SessionPreamble.d.ts +7 -0
- package/dist/workflow/SessionPreamble.js +48 -9
- package/dist/workflow/SessionPreamble.js.map +1 -1
- package/dist/workflow/VerificationCommands.d.ts +1 -0
- package/dist/workflow/VerificationCommands.js.map +1 -1
- package/dist/workflow/VerificationProfile.d.ts +5 -0
- package/dist/workflow/VerificationProfile.js +26 -0
- package/dist/workflow/VerificationProfile.js.map +1 -1
- package/dist/workflow/VerificationSchema.d.ts +3 -0
- package/dist/workflow/VerificationSchema.js +6 -0
- package/dist/workflow/VerificationSchema.js.map +1 -1
- package/dist/workflow/WorkflowEffectiveness.d.ts +97 -0
- package/dist/workflow/WorkflowEffectiveness.js +302 -0
- package/dist/workflow/WorkflowEffectiveness.js.map +1 -0
- package/dist/workflow/WorkflowEffectivenessRenderer.d.ts +2 -0
- package/dist/workflow/WorkflowEffectivenessRenderer.js +67 -0
- package/dist/workflow/WorkflowEffectivenessRenderer.js.map +1 -0
- package/dist/workflow/WorkflowEffectivenessScoring.d.ts +6 -0
- package/dist/workflow/WorkflowEffectivenessScoring.js +243 -0
- package/dist/workflow/WorkflowEffectivenessScoring.js.map +1 -0
- package/dist/workflow/gates/GateSystem.d.ts +16 -0
- package/dist/workflow/gates/GateSystem.js +208 -41
- package/dist/workflow/gates/GateSystem.js.map +1 -1
- package/dist/workflow/gates/MetaGovernanceGates.js +269 -8
- package/dist/workflow/gates/MetaGovernanceGates.js.map +1 -1
- package/docs/reference/cli.md +2 -1
- package/docs/start/agent-governance-demo.md +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 +9 -1
- package/docs/workflow/templates/github-actions-scale-preflight.yml +4 -1
- package/package.json +10 -3
- 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
|
-
//
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
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 : '';
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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(
|
|
404
|
-
scanDir(join(
|
|
405
|
-
scanDir(join(
|
|
406
|
-
|
|
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
|