@massu/core 1.6.1 → 1.6.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
4
4
  "type": "module",
5
5
  "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 72 total), 55+ workflow commands, 11 agents, 20+ patterns",
6
6
  "main": "src/server.ts",
package/src/db.ts CHANGED
@@ -6,14 +6,37 @@ import { dirname, join } from 'path';
6
6
  import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
7
7
  import { getResolvedPaths } from './config.ts';
8
8
 
9
+ /**
10
+ * Thrown by `getCodeGraphDb()` when `.codegraph/codegraph.db` is missing.
11
+ *
12
+ * Caught at the JSON-RPC dispatch layer (server.ts) and translated to a
13
+ * structured `-32001` error response carrying a remedy hint and the resolved
14
+ * DB path. The thrown error is INTERNAL; user-facing copy lives in the
15
+ * dispatcher's error envelope.
16
+ *
17
+ * @see `docs/plans/2026-05-10-server-lazy-db-deps.md` P-C-001 + P-A-004
18
+ */
19
+ export class CodegraphDbNotInitializedError extends Error {
20
+ readonly dbPath: string;
21
+ constructor(dbPath: string) {
22
+ super(`CodeGraph database not found at ${dbPath}`);
23
+ this.name = 'CodegraphDbNotInitializedError';
24
+ this.dbPath = dbPath;
25
+ }
26
+ }
27
+
9
28
  /**
10
29
  * Connection to CodeGraph's read-only SQLite database.
11
30
  * We NEVER write to this DB - it belongs to vanilla CodeGraph.
31
+ *
32
+ * Throws `CodegraphDbNotInitializedError` (internal signal) when the DB is
33
+ * missing. The MCP dispatcher catches and translates to a structured
34
+ * JSON-RPC error pointing at `npx @colbymchenry/codegraph init`.
12
35
  */
13
36
  export function getCodeGraphDb(): Database.Database {
14
37
  const dbPath = getResolvedPaths().codegraphDbPath;
15
38
  if (!existsSync(dbPath)) {
16
- throw new Error(`CodeGraph database not found at ${dbPath}. Run 'npx @colbymchenry/codegraph sync' first.`);
39
+ throw new CodegraphDbNotInitializedError(dbPath);
17
40
  }
18
41
  const db = new Database(dbPath, { readonly: true });
19
42
  db.pragma('journal_mode = WAL');
@@ -1,4 +1,4 @@
1
- // AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-10T21:58:17.622Z.
1
+ // AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-11T22:54:34.471Z.
2
2
  // Source pem: packages/core/security/registry-pubkey.pem
3
3
  // RAW-bytes sha256: 3b6226d036c472e533110d11a7d0cd2773ce1d7d4f1003517d5bd69c5418ed4c
4
4
  // DO NOT EDIT — regenerate via `node scripts/bundle-pubkey.mjs` or
@@ -0,0 +1,225 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ /**
5
+ * MCP server dispatch logic — pure, factory-based, no module-level mutable state.
6
+ *
7
+ * Production (`server.ts`) calls `createDispatcher()` once at startup and wires
8
+ * its `processLine` into stdin. Tests call `createDispatcher()` per test for
9
+ * fresh DB cache state (no test bleed).
10
+ *
11
+ * Three error envelopes live here (CR-12 / plan-1.6.2-server-lazy-db-deps):
12
+ * -32001 Tool needs CodeGraph but `.codegraph/codegraph.db` is missing
13
+ * (CodegraphDbNotInitializedError → structured remedy data)
14
+ * -32602 Tool not registered in `TOOL_DB_NEEDS` manifest
15
+ * (UnknownToolError → points at tool-db-needs.ts)
16
+ * -32603 Other internal errors raised by handleToolCall — request id
17
+ * is preserved (NOT id:null, which is reserved for -32700 parse
18
+ * failures per JSON-RPC §5.1).
19
+ */
20
+
21
+ import type Database from 'better-sqlite3';
22
+ import { getCodeGraphDb, getDataDb, CodegraphDbNotInitializedError } from './db.ts';
23
+ import { getConfig } from './config.ts';
24
+ import { getToolDefinitions, handleToolCall } from './tools.ts';
25
+ import { getToolDbNeeds, UnknownToolError, type DbNeed } from './tool-db-needs.ts';
26
+
27
+ export interface JsonRpcRequest {
28
+ jsonrpc: '2.0';
29
+ id?: number | string;
30
+ method: string;
31
+ params?: Record<string, unknown>;
32
+ }
33
+
34
+ export interface JsonRpcResponse {
35
+ jsonrpc: '2.0';
36
+ id: number | string | null;
37
+ result?: unknown;
38
+ error?: { code: number; message: string; data?: unknown };
39
+ }
40
+
41
+ /** Per-line dispatch result. `emit=false` when the request was a notification (no id). */
42
+ export interface ProcessLineResult {
43
+ response: JsonRpcResponse;
44
+ emit: boolean;
45
+ }
46
+
47
+ export interface DispatcherOptions {
48
+ /** Version string surfaced in `initialize.result.serverInfo.version`. */
49
+ serverInfoVersion: string;
50
+ }
51
+
52
+ export interface Dispatcher {
53
+ handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse>;
54
+ processLine(line: string): Promise<ProcessLineResult | null>;
55
+ closeCachedDbs(): void;
56
+ }
57
+
58
+ export function createDispatcher(options: DispatcherOptions): Dispatcher {
59
+ let codegraphDbCache: Database.Database | null = null;
60
+ let dataDbCache: Database.Database | null = null;
61
+
62
+ function resolveDbsForTool(toolName: string): {
63
+ needs: readonly DbNeed[];
64
+ dataDb?: Database.Database;
65
+ codegraphDb?: Database.Database;
66
+ } {
67
+ const needs = getToolDbNeeds(toolName, getConfig().toolPrefix);
68
+
69
+ let dataDbResolved: Database.Database | undefined;
70
+ let codegraphDbResolved: Database.Database | undefined;
71
+
72
+ if (needs.includes('data')) {
73
+ if (!dataDbCache) dataDbCache = getDataDb();
74
+ dataDbResolved = dataDbCache;
75
+ }
76
+
77
+ if (needs.includes('codegraph')) {
78
+ // Throws CodegraphDbNotInitializedError when .codegraph/codegraph.db is missing.
79
+ if (!codegraphDbCache) codegraphDbCache = getCodeGraphDb();
80
+ codegraphDbResolved = codegraphDbCache;
81
+ }
82
+
83
+ return { needs, dataDb: dataDbResolved, codegraphDb: codegraphDbResolved };
84
+ }
85
+
86
+ async function handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse> {
87
+ const { method, params, id } = request;
88
+
89
+ switch (method) {
90
+ case 'initialize': {
91
+ return {
92
+ jsonrpc: '2.0',
93
+ id: id ?? null,
94
+ result: {
95
+ protocolVersion: '2024-11-05',
96
+ capabilities: { tools: {} },
97
+ serverInfo: {
98
+ name: getConfig().toolPrefix || 'massu',
99
+ version: options.serverInfoVersion,
100
+ },
101
+ },
102
+ };
103
+ }
104
+
105
+ case 'notifications/initialized': {
106
+ return { jsonrpc: '2.0', id: id ?? null, result: {} };
107
+ }
108
+
109
+ case 'tools/list': {
110
+ const tools = getToolDefinitions();
111
+ return { jsonrpc: '2.0', id: id ?? null, result: { tools } };
112
+ }
113
+
114
+ case 'tools/call': {
115
+ const toolName = (params as { name?: string })?.name ?? '';
116
+ const toolArgs = (params as { arguments?: Record<string, unknown> })?.arguments ?? {};
117
+
118
+ try {
119
+ const { dataDb, codegraphDb } = resolveDbsForTool(toolName);
120
+ const result = await handleToolCall(toolName, toolArgs, dataDb, codegraphDb);
121
+ return { jsonrpc: '2.0', id: id ?? null, result };
122
+ } catch (err) {
123
+ if (err instanceof CodegraphDbNotInitializedError) {
124
+ return {
125
+ jsonrpc: '2.0',
126
+ id: id ?? null,
127
+ error: {
128
+ code: -32001,
129
+ message: 'Tool requires CodeGraph database which is not initialized for this repo',
130
+ data: {
131
+ remedy: 'npx @colbymchenry/codegraph@0.7.4 init . && npx @colbymchenry/codegraph@0.7.4 index .',
132
+ codegraphDbPath: err.dbPath,
133
+ tool: toolName,
134
+ },
135
+ },
136
+ };
137
+ }
138
+ if (err instanceof UnknownToolError) {
139
+ return {
140
+ jsonrpc: '2.0',
141
+ id: id ?? null,
142
+ error: {
143
+ code: -32602,
144
+ message: `Unknown tool: ${err.toolName}`,
145
+ data: {
146
+ remedy: 'Tool not registered in TOOL_DB_NEEDS manifest. See packages/core/src/tool-db-needs.ts.',
147
+ tool: toolName,
148
+ },
149
+ },
150
+ };
151
+ }
152
+ throw err;
153
+ }
154
+ }
155
+
156
+ case 'ping': {
157
+ return { jsonrpc: '2.0', id: id ?? null, result: {} };
158
+ }
159
+
160
+ default: {
161
+ return {
162
+ jsonrpc: '2.0',
163
+ id: id ?? null,
164
+ error: { code: -32601, message: `Method not found: ${method}` },
165
+ };
166
+ }
167
+ }
168
+ }
169
+
170
+ async function processLine(line: string): Promise<ProcessLineResult | null> {
171
+ const trimmed = line.trim();
172
+ if (!trimmed) return null;
173
+
174
+ let request: JsonRpcRequest;
175
+ try {
176
+ request = JSON.parse(trimmed) as JsonRpcRequest;
177
+ } catch (parseError) {
178
+ // JSON-RPC §5.1: parse failure → -32700 + id:null (no id is extractable).
179
+ return {
180
+ response: {
181
+ jsonrpc: '2.0',
182
+ id: null,
183
+ error: {
184
+ code: -32700,
185
+ message: `Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
186
+ },
187
+ },
188
+ emit: true,
189
+ };
190
+ }
191
+
192
+ try {
193
+ const response = await handleRequest(request);
194
+ // Notifications (no id) MUST NOT receive a response per JSON-RPC §4.1.
195
+ return { response, emit: request.id !== undefined };
196
+ } catch (error) {
197
+ // Request-processing failure: -32603 with the request id preserved.
198
+ // Specific subclasses (-32001/-32602) are handled inside tools/call.
199
+ return {
200
+ response: {
201
+ jsonrpc: '2.0',
202
+ id: request.id ?? null,
203
+ error: {
204
+ code: -32603,
205
+ message: `Internal error: ${error instanceof Error ? error.message : String(error)}`,
206
+ },
207
+ },
208
+ emit: true,
209
+ };
210
+ }
211
+ }
212
+
213
+ function closeCachedDbs(): void {
214
+ if (codegraphDbCache) {
215
+ codegraphDbCache.close();
216
+ codegraphDbCache = null;
217
+ }
218
+ if (dataDbCache) {
219
+ dataDbCache.close();
220
+ dataDbCache = null;
221
+ }
222
+ }
223
+
224
+ return { handleRequest, processLine, closeCachedDbs };
225
+ }
package/src/server.ts CHANGED
@@ -14,11 +14,9 @@
14
14
  import { readFileSync } from 'fs';
15
15
  import { resolve, dirname } from 'path';
16
16
  import { fileURLToPath } from 'url';
17
- import { getCodeGraphDb, getDataDb } from './db.ts';
18
- import { getConfig } from './config.ts';
19
- import { getToolDefinitions, handleToolCall } from './tools.ts';
20
17
  import { getMemoryDb, pruneOldConversationTurns, pruneOldObservations } from './memory-db.ts';
21
18
  import { getCurrentTier } from './license.ts';
19
+ import { createDispatcher } from './server-dispatch.ts';
22
20
 
23
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
24
22
  const PKG_VERSION = (() => {
@@ -30,92 +28,7 @@ const PKG_VERSION = (() => {
30
28
  }
31
29
  })();
32
30
 
33
- interface JsonRpcRequest {
34
- jsonrpc: '2.0';
35
- id?: number | string;
36
- method: string;
37
- params?: Record<string, unknown>;
38
- }
39
-
40
- interface JsonRpcResponse {
41
- jsonrpc: '2.0';
42
- id: number | string | null;
43
- result?: unknown;
44
- error?: { code: number; message: string; data?: unknown };
45
- }
46
-
47
- // Server state
48
- let codegraphDb: ReturnType<typeof getCodeGraphDb> | null = null;
49
- let dataDb: ReturnType<typeof getDataDb> | null = null;
50
-
51
- function getDb() {
52
- if (!codegraphDb) codegraphDb = getCodeGraphDb();
53
- if (!dataDb) dataDb = getDataDb();
54
- return { codegraphDb, dataDb: dataDb };
55
- }
56
-
57
- async function handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse> {
58
- const { method, params, id } = request;
59
-
60
- switch (method) {
61
- case 'initialize': {
62
- return {
63
- jsonrpc: '2.0',
64
- id: id ?? null,
65
- result: {
66
- protocolVersion: '2024-11-05',
67
- capabilities: {
68
- tools: {},
69
- },
70
- serverInfo: {
71
- name: getConfig().toolPrefix || 'massu',
72
- version: PKG_VERSION,
73
- },
74
- },
75
- };
76
- }
77
-
78
- case 'notifications/initialized': {
79
- // Client acknowledges initialization - no response needed for notifications
80
- return { jsonrpc: '2.0', id: id ?? null, result: {} };
81
- }
82
-
83
- case 'tools/list': {
84
- const tools = getToolDefinitions();
85
- return {
86
- jsonrpc: '2.0',
87
- id: id ?? null,
88
- result: { tools },
89
- };
90
- }
91
-
92
- case 'tools/call': {
93
- const toolName = (params as { name: string })?.name;
94
- const toolArgs = (params as { arguments?: Record<string, unknown> })?.arguments ?? {};
95
-
96
- const { codegraphDb: cgDb, dataDb: lDb } = getDb();
97
- const result = await handleToolCall(toolName, toolArgs, lDb, cgDb);
98
-
99
- return {
100
- jsonrpc: '2.0',
101
- id: id ?? null,
102
- result,
103
- };
104
- }
105
-
106
- case 'ping': {
107
- return { jsonrpc: '2.0', id: id ?? null, result: {} };
108
- }
109
-
110
- default: {
111
- return {
112
- jsonrpc: '2.0',
113
- id: id ?? null,
114
- error: { code: -32601, message: `Method not found: ${method}` },
115
- };
116
- }
117
- }
118
- }
31
+ const dispatcher = createDispatcher({ serverInfoVersion: PKG_VERSION });
119
32
 
120
33
  // === Startup: prune stale memory data (non-blocking) ===
121
34
 
@@ -167,38 +80,20 @@ process.stdin.on('data', async (chunk: string) => {
167
80
  // Process complete messages (newline-delimited JSON-RPC)
168
81
  let newlineIndex: number;
169
82
  while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
170
- const line = buffer.slice(0, newlineIndex).trim();
83
+ const line = buffer.slice(0, newlineIndex);
171
84
  buffer = buffer.slice(newlineIndex + 1);
172
85
 
173
- if (!line) continue;
174
-
175
- try {
176
- const request = JSON.parse(line) as JsonRpcRequest;
177
- const response = await handleRequest(request);
178
-
179
- // Don't send responses for notifications (no id)
180
- if (request.id !== undefined) {
181
- const responseStr = JSON.stringify(response);
182
- process.stdout.write(responseStr + '\n');
183
- }
184
- } catch (error) {
185
- const errorResponse: JsonRpcResponse = {
186
- jsonrpc: '2.0',
187
- id: null,
188
- error: {
189
- code: -32700,
190
- message: `Parse error: ${error instanceof Error ? error.message : String(error)}`,
191
- },
192
- };
193
- process.stdout.write(JSON.stringify(errorResponse) + '\n');
86
+ const result = await dispatcher.processLine(line);
87
+ if (result && result.emit) {
88
+ process.stdout.write(JSON.stringify(result.response) + '\n');
194
89
  }
195
90
  }
196
91
  });
197
92
 
198
93
  process.stdin.on('end', () => {
199
- // Clean up database connections
200
- if (codegraphDb) codegraphDb.close();
201
- if (dataDb) dataDb.close();
94
+ // Close cached CodeGraph + Data connections. Memory + Knowledge are
95
+ // per-call (closed inside their routing branches in tools.ts).
96
+ dispatcher.closeCachedDbs();
202
97
  process.exit(0);
203
98
  });
204
99
 
@@ -0,0 +1,226 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ /**
5
+ * Per-tool SQLite database dependency manifest.
6
+ *
7
+ * **Role**: SOLE source of truth declaring which SQLite connections each MCP
8
+ * tool needs. The dispatcher (`server.ts` → `tools.ts:handleToolCall`) reads
9
+ * this map to lazy-resolve connections, opening ONLY the DBs a tool requires.
10
+ *
11
+ * **Why this exists**:
12
+ * Before plan `plan-1.6.2-server-lazy-db-deps`, the dispatcher eagerly opened
13
+ * BOTH CodeGraph + Data DBs on every tool/call (see legacy `server.ts:51-55,96`
14
+ * and `tools.ts:279`). When `.codegraph/codegraph.db` was missing, EVERY tool
15
+ * call failed — even memory/audit/knowledge tools that have no codegraph
16
+ * dependency. This manifest makes that bug class structurally impossible:
17
+ * a missing peripheral DB only blocks the tools that need it.
18
+ *
19
+ * **Structural drift-prevention (3 layers)**:
20
+ * - L1: TypeScript compile time — exhaustiveness check via `keyof TOOL_DB_NEEDS`.
21
+ * - L2: `tool-db-needs-completeness.test.ts` — TypeScript AST walk of every
22
+ * tool module verifies declared needs match actual DB access pattern.
23
+ * Aliasing/destructuring renames cannot bypass the AST walk.
24
+ * - L3: `scripts/massu-pattern-scanner.sh` Check 14 — grep-level enforcement
25
+ * that every tool in `getToolDefinitions()` has a manifest entry.
26
+ *
27
+ * **Adding a new MCP tool**:
28
+ * 1. Register in `tools.ts` (CR-11).
29
+ * 2. Add an entry here. Missing entries throw `UnknownToolError` at first
30
+ * dispatch AND fail L2 + L3 above.
31
+ *
32
+ * @see `docs/plans/2026-05-10-server-lazy-db-deps.md` (`plan-1.6.2-server-lazy-db-deps`)
33
+ */
34
+
35
+ /** SQLite connections the MCP server can resolve for a tool call. */
36
+ export type DbNeed = 'codegraph' | 'data' | 'memory' | 'knowledge';
37
+
38
+ /**
39
+ * Custom error thrown when `getToolDbNeeds()` is called with a tool name that
40
+ * isn't in the manifest. Caught at the JSON-RPC layer and translated to a
41
+ * structured `-32602` (Invalid params) error to the client.
42
+ */
43
+ export class UnknownToolError extends Error {
44
+ readonly toolName: string;
45
+ constructor(toolName: string) {
46
+ super(`Tool not registered in TOOL_DB_NEEDS manifest: ${toolName}. Add an entry to packages/core/src/tool-db-needs.ts.`);
47
+ this.name = 'UnknownToolError';
48
+ this.toolName = toolName;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Per-tool DB-need declarations. Keys are tool SHORT-NAMES (without the
54
+ * `${toolPrefix}_` prefix). Values are the SQLite connections the handler
55
+ * (or its routed module) actually accesses.
56
+ *
57
+ * Sourced from exhaustive grep of `packages/core/src/{*-tools,analytics,
58
+ * cost-tracker,prompt-analyzer,audit-trail,validation-engine,adr-generator,
59
+ * security-scorer,dependency-scorer,team-knowledge,regression-detector,
60
+ * python-tools,license}.ts` on 2026-05-10. Verified line citations in
61
+ * `docs/plans/2026-05-10-server-lazy-db-deps.md §1.4`.
62
+ */
63
+ export const TOOL_DB_NEEDS = {
64
+ // === Core code-intel tools (tools.ts:393-406) ===
65
+ // Use CodeGraph DB (read-only AST) + Data DB (Massu's import/trpc/sentinel
66
+ // tables). All call `ensureIndexes` directly or via shared infrastructure.
67
+ sync: ['codegraph', 'data'],
68
+ context: ['codegraph', 'data'],
69
+ coupling_check: ['codegraph', 'data'],
70
+ impact: ['codegraph', 'data'],
71
+ domains: ['codegraph', 'data'],
72
+
73
+ // `trpc_map` reads only Data DB (tRPC index lives there); no CodeGraph access.
74
+ trpc_map: ['data'],
75
+
76
+ // `schema` reads filesystem (Prisma schema files); no DB access at all.
77
+ schema: [],
78
+
79
+ // === Memory tools (memory-tools.ts) ===
80
+ // Routed via `name.startsWith(pfx + '_memory_')` at tools.ts:284-290.
81
+ // Handler opens memory DB per-call (with try/finally close).
82
+ memory_search: ['memory'],
83
+ memory_timeline: ['memory'],
84
+ memory_detail: ['memory'],
85
+ memory_sessions: ['memory'],
86
+ memory_failures: ['memory'],
87
+ memory_ingest: ['memory'],
88
+ memory_backfill: ['memory'],
89
+
90
+ // === Observability tools (observability-tools.ts) ===
91
+ // Routed via `isObservabilityTool(name)` at tools.ts:294-300. Memory DB only.
92
+ session_replay: ['memory'],
93
+ session_stats: ['memory'],
94
+ tool_patterns: ['memory'],
95
+ prompt_analysis: ['memory'],
96
+
97
+ // === Docs tools (docs-tools.ts) ===
98
+ // Routed via `name.startsWith(pfx + '_docs_')` at tools.ts:303-306.
99
+ // No DB access — pure filesystem traversal.
100
+ docs_audit: [],
101
+ docs_coverage: [],
102
+
103
+ // === Sentinel registry tools (sentinel-tools.ts:180-184) ===
104
+ // Routed via `name.startsWith(pfx + '_sentinel_')` at tools.ts:308.
105
+ // Handler signature: `(name, args, dataDb)` — Data DB only.
106
+ sentinel_register: ['data'],
107
+ sentinel_validate: ['data'],
108
+ sentinel_search: ['data'],
109
+ sentinel_detail: ['data'],
110
+ sentinel_impact: ['data'],
111
+ sentinel_parity: ['data'],
112
+
113
+ // === Knowledge layer tools (knowledge-tools.ts) ===
114
+ // Routed via `isKnowledgeTool(name)` at tools.ts:372-376. Primary DB is
115
+ // `knowledgeDb` (separate SQLite file). Handlers ALSO call `getDataDb()`
116
+ // (knowledge-tools.ts:1187,1275) and `getMemoryDb()` (knowledge-tools.ts:1332)
117
+ // for cross-DB joins — declare all three so the AST completeness test
118
+ // (P-B-002) verifies the full access pattern.
119
+ knowledge_search: ['knowledge', 'data', 'memory'],
120
+ knowledge_pattern: ['knowledge', 'data', 'memory'],
121
+ knowledge_rule: ['knowledge', 'data', 'memory'],
122
+ knowledge_correct: ['knowledge', 'data', 'memory'],
123
+ knowledge_incident: ['knowledge', 'data', 'memory'],
124
+ knowledge_plan: ['knowledge', 'data', 'memory'],
125
+ knowledge_command: ['knowledge', 'data', 'memory'],
126
+ knowledge_gaps: ['knowledge', 'data', 'memory'],
127
+ knowledge_verification: ['knowledge', 'data', 'memory'],
128
+ knowledge_effectiveness: ['knowledge', 'data', 'memory'],
129
+ knowledge_graph: ['knowledge', 'data', 'memory'],
130
+ knowledge_schema_check: ['knowledge', 'data', 'memory'],
131
+
132
+ // === Analytics / quality (analytics.ts) ===
133
+ // Routed via `isAnalyticsTool(name)`. Memory DB only.
134
+ quality_score: ['memory'],
135
+ quality_report: ['memory'],
136
+ quality_trend: ['memory'],
137
+
138
+ // === Cost tracker (cost-tracker.ts) ===
139
+ cost_session: ['memory'],
140
+ cost_feature: ['memory'],
141
+ cost_trend: ['memory'],
142
+
143
+ // === Prompt analyzer (prompt-analyzer.ts) ===
144
+ prompt_effectiveness: ['memory'],
145
+ prompt_suggestions: ['memory'],
146
+
147
+ // === Audit trail (audit-trail.ts) ===
148
+ audit_chain: ['memory'],
149
+ audit_log: ['memory'],
150
+ audit_report: ['memory'],
151
+
152
+ // === Validation engine (validation-engine.ts) ===
153
+ validation_check: ['memory'],
154
+ validation_report: ['memory'],
155
+
156
+ // === ADR generator (adr-generator.ts) ===
157
+ adr_create: ['memory'],
158
+ adr_list: ['memory'],
159
+ adr_detail: ['memory'],
160
+
161
+ // === Security scorer (security-scorer.ts) ===
162
+ security_score: ['memory'],
163
+ security_heatmap: ['memory'],
164
+ security_trend: ['memory'],
165
+
166
+ // === Dependency scorer (dependency-scorer.ts) ===
167
+ dep_score: ['memory'],
168
+ dep_alternatives: ['memory'],
169
+
170
+ // === Team knowledge (team-knowledge.ts) ===
171
+ team_expertise: ['memory'],
172
+ team_conflicts: ['memory'],
173
+ team_search: ['memory'],
174
+
175
+ // === Regression detector (regression-detector.ts) ===
176
+ regression_risk: ['memory'],
177
+ feature_health: ['memory'],
178
+
179
+ // === Python code-intel tools (python-tools.ts) ===
180
+ // Routed via `isPythonTool(name)` at tools.ts:379-381. Data DB only.
181
+ py_imports: ['data'],
182
+ py_routes: ['data'],
183
+ py_models: ['data'],
184
+ py_migrations: ['data'],
185
+ py_coupling: ['data'],
186
+ py_context: ['data'],
187
+ py_impact: ['data'],
188
+ py_domains: ['data'],
189
+
190
+ // === License tool (license.ts) ===
191
+ license_status: ['memory'],
192
+ } as const satisfies Readonly<Record<string, readonly DbNeed[]>>;
193
+
194
+ /**
195
+ * Configured tool-prefix-stripping helper. Pulled from the runtime config
196
+ * so this module stays project-prefix-agnostic.
197
+ */
198
+ function stripConfiguredPrefix(toolName: string, prefix: string): string {
199
+ const pfx = `${prefix}_`;
200
+ return toolName.startsWith(pfx) ? toolName.slice(pfx.length) : toolName;
201
+ }
202
+
203
+ /**
204
+ * Look up the DB needs for a tool by its full name (with prefix). Strips the
205
+ * configured prefix and consults `TOOL_DB_NEEDS`. Throws `UnknownToolError`
206
+ * for tool names not in the manifest — the dispatcher MUST catch this and
207
+ * translate to a structured JSON-RPC error.
208
+ *
209
+ * @param toolName Full tool name including prefix (e.g., `"massu_memory_search"`)
210
+ * @param prefix Tool prefix (e.g., `"massu"`) — read from config at dispatch time
211
+ * @returns Array of DB connections the tool requires (may be empty)
212
+ * @throws {UnknownToolError} if `stripPrefix(toolName)` not in the manifest
213
+ */
214
+ export function getToolDbNeeds(toolName: string, prefix: string): readonly DbNeed[] {
215
+ const shortName = stripConfiguredPrefix(toolName, prefix);
216
+ const needs = (TOOL_DB_NEEDS as Record<string, readonly DbNeed[]>)[shortName];
217
+ if (needs === undefined) {
218
+ throw new UnknownToolError(toolName);
219
+ }
220
+ return needs;
221
+ }
222
+
223
+ /** Convenience predicate: does a tool need CodeGraph DB? */
224
+ export function toolNeedsCodegraph(toolName: string, prefix: string): boolean {
225
+ return getToolDbNeeds(toolName, prefix).includes('codegraph');
226
+ }