@massu/core 1.6.1 → 1.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
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-11T05:57:55.925Z.
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
package/src/server.ts CHANGED
@@ -14,11 +14,13 @@
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';
17
+ import type Database from 'better-sqlite3';
18
+ import { getCodeGraphDb, getDataDb, CodegraphDbNotInitializedError } from './db.ts';
19
+ import { getConfig, getResolvedPaths } from './config.ts';
19
20
  import { getToolDefinitions, handleToolCall } from './tools.ts';
20
21
  import { getMemoryDb, pruneOldConversationTurns, pruneOldObservations } from './memory-db.ts';
21
22
  import { getCurrentTier } from './license.ts';
23
+ import { getToolDbNeeds, UnknownToolError, type DbNeed } from './tool-db-needs.ts';
22
24
 
23
25
  const __dirname = dirname(fileURLToPath(import.meta.url));
24
26
  const PKG_VERSION = (() => {
@@ -44,14 +46,53 @@ interface JsonRpcResponse {
44
46
  error?: { code: number; message: string; data?: unknown };
45
47
  }
46
48
 
47
- // Server state
48
- let codegraphDb: ReturnType<typeof getCodeGraphDb> | null = null;
49
- let dataDb: ReturnType<typeof getDataDb> | null = null;
49
+ // === Server state: lazy per-tool DB resolution ===
50
+ //
51
+ // Per plan-1.6.2-server-lazy-db-deps: DBs are opened ONLY when the
52
+ // currently-dispatched tool declares it needs them in `TOOL_DB_NEEDS`.
53
+ // Connections are cached at module scope so subsequent tool calls reuse
54
+ // the open handle without re-opening (CodeGraph is read-only — safe to
55
+ // share; Data DB has WAL journal — single-writer is fine).
56
+ //
57
+ // PRIOR DESIGN (eliminated 2026-05-10): `getDb()` eagerly opened BOTH
58
+ // CodeGraph + Data on every `tools/call`, even for memory/audit/knowledge
59
+ // tools that don't need codegraph. Missing `.codegraph/codegraph.db`
60
+ // broke ALL tools. See `docs/plans/2026-05-10-server-lazy-db-deps.md`.
50
61
 
51
- function getDb() {
52
- if (!codegraphDb) codegraphDb = getCodeGraphDb();
53
- if (!dataDb) dataDb = getDataDb();
54
- return { codegraphDb, dataDb: dataDb };
62
+ let codegraphDbCache: Database.Database | null = null;
63
+ let dataDbCache: Database.Database | null = null;
64
+
65
+ /**
66
+ * Resolve the SQLite connections a tool needs, opening cached singletons
67
+ * lazily. Memory DB and Knowledge DB are opened per-call by their routed
68
+ * handlers (existing pattern in tools.ts) — only CodeGraph + Data are
69
+ * cached here.
70
+ *
71
+ * @throws {CodegraphDbNotInitializedError} when tool needs codegraph but
72
+ * `.codegraph/codegraph.db` is missing. Caller (handleRequest) catches
73
+ * and translates to a structured `-32001` JSON-RPC error.
74
+ */
75
+ function resolveDbsForTool(toolName: string): {
76
+ needs: readonly DbNeed[];
77
+ dataDb?: Database.Database;
78
+ codegraphDb?: Database.Database;
79
+ } {
80
+ const needs = getToolDbNeeds(toolName, getConfig().toolPrefix);
81
+
82
+ let dataDbResolved: Database.Database | undefined;
83
+ let codegraphDbResolved: Database.Database | undefined;
84
+
85
+ if (needs.includes('data')) {
86
+ if (!dataDbCache) dataDbCache = getDataDb();
87
+ dataDbResolved = dataDbCache;
88
+ }
89
+
90
+ if (needs.includes('codegraph')) {
91
+ if (!codegraphDbCache) codegraphDbCache = getCodeGraphDb(); // throws CodegraphDbNotInitializedError on missing
92
+ codegraphDbResolved = codegraphDbCache;
93
+ }
94
+
95
+ return { needs, dataDb: dataDbResolved, codegraphDb: codegraphDbResolved };
55
96
  }
56
97
 
57
98
  async function handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse> {
@@ -93,14 +134,50 @@ async function handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse>
93
134
  const toolName = (params as { name: string })?.name;
94
135
  const toolArgs = (params as { arguments?: Record<string, unknown> })?.arguments ?? {};
95
136
 
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
- };
137
+ // Lazy per-tool DB resolution. Throws if tool needs codegraph and
138
+ // .codegraph/codegraph.db is missing; caught below and translated
139
+ // to a structured -32001 error preserving the request id.
140
+ try {
141
+ const { dataDb: lDb, codegraphDb: cgDb } = resolveDbsForTool(toolName);
142
+ const result = await handleToolCall(toolName, toolArgs, lDb, cgDb);
143
+ return {
144
+ jsonrpc: '2.0',
145
+ id: id ?? null,
146
+ result,
147
+ };
148
+ } catch (err) {
149
+ if (err instanceof CodegraphDbNotInitializedError) {
150
+ return {
151
+ jsonrpc: '2.0',
152
+ id: id ?? null,
153
+ error: {
154
+ code: -32001,
155
+ message: `Tool requires CodeGraph database which is not initialized for this repo`,
156
+ data: {
157
+ remedy: 'npx @colbymchenry/codegraph@0.7.4 init . && npx @colbymchenry/codegraph@0.7.4 index .',
158
+ codegraphDbPath: err.dbPath,
159
+ tool: toolName,
160
+ },
161
+ },
162
+ };
163
+ }
164
+ if (err instanceof UnknownToolError) {
165
+ return {
166
+ jsonrpc: '2.0',
167
+ id: id ?? null,
168
+ error: {
169
+ code: -32602,
170
+ message: `Unknown tool: ${err.toolName}`,
171
+ data: {
172
+ remedy: 'Tool not registered in TOOL_DB_NEEDS manifest. See packages/core/src/tool-db-needs.ts.',
173
+ tool: toolName,
174
+ },
175
+ },
176
+ };
177
+ }
178
+ // Other errors propagate to the outer catch in the stdio handler
179
+ throw err;
180
+ }
104
181
  }
105
182
 
106
183
  case 'ping': {
@@ -172,22 +249,45 @@ process.stdin.on('data', async (chunk: string) => {
172
249
 
173
250
  if (!line) continue;
174
251
 
252
+ // Two-phase error handling: separate JSON-parse failures (genuine
253
+ // -32700) from request-processing failures (-32603 Internal error,
254
+ // preserving the request id when parseable).
255
+ let request: JsonRpcRequest | null = null;
175
256
  try {
176
- const request = JSON.parse(line) as JsonRpcRequest;
177
- const response = await handleRequest(request);
257
+ request = JSON.parse(line) as JsonRpcRequest;
258
+ } catch (parseError) {
259
+ // Real JSON parse failure — -32700 per JSON-RPC §5.1, id MUST be null
260
+ // because we couldn't extract one.
261
+ const errorResponse: JsonRpcResponse = {
262
+ jsonrpc: '2.0',
263
+ id: null,
264
+ error: {
265
+ code: -32700,
266
+ message: `Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
267
+ },
268
+ };
269
+ process.stdout.write(JSON.stringify(errorResponse) + '\n');
270
+ continue;
271
+ }
178
272
 
273
+ try {
274
+ const response = await handleRequest(request);
179
275
  // Don't send responses for notifications (no id)
180
276
  if (request.id !== undefined) {
181
277
  const responseStr = JSON.stringify(response);
182
278
  process.stdout.write(responseStr + '\n');
183
279
  }
184
280
  } catch (error) {
281
+ // Request-processing failure — propagate the request id (not null).
282
+ // -32603 Internal error per JSON-RPC §5.1. Specific subclasses
283
+ // (codegraph-not-init, unknown-tool) are caught earlier in the
284
+ // tools/call handler and translated to structured -32001/-32602.
185
285
  const errorResponse: JsonRpcResponse = {
186
286
  jsonrpc: '2.0',
187
- id: null,
287
+ id: request.id ?? null,
188
288
  error: {
189
- code: -32700,
190
- message: `Parse error: ${error instanceof Error ? error.message : String(error)}`,
289
+ code: -32603,
290
+ message: `Internal error: ${error instanceof Error ? error.message : String(error)}`,
191
291
  },
192
292
  };
193
293
  process.stdout.write(JSON.stringify(errorResponse) + '\n');
@@ -196,9 +296,10 @@ process.stdin.on('data', async (chunk: string) => {
196
296
  });
197
297
 
198
298
  process.stdin.on('end', () => {
199
- // Clean up database connections
200
- if (codegraphDb) codegraphDb.close();
201
- if (dataDb) dataDb.close();
299
+ // Clean up cached DB connections (Memory + Knowledge are per-call,
300
+ // already closed in their routing branches).
301
+ if (codegraphDbCache) codegraphDbCache.close();
302
+ if (dataDbCache) dataDbCache.close();
202
303
  process.exit(0);
203
304
  });
204
305
 
@@ -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
+ }
package/src/tools.ts CHANGED
@@ -75,14 +75,26 @@ function stripPrefix(name: string): string {
75
75
 
76
76
  /**
77
77
  * Ensure indexes are built and up-to-date.
78
+ *
78
79
  * Lazy initialization: only rebuilds if stale.
80
+ *
81
+ * **Codegraph-optional**: `codegraphDb` is now optional. When omitted, the
82
+ * JS index section (imports, tRPC, pages, middleware — all derived from
83
+ * CodeGraph AST data) is skipped. The Python index section (which only
84
+ * reads Python source files via tree-sitter into Data DB) still runs.
85
+ * This supports the lazy-per-tool-DB design from
86
+ * `plan-1.6.2-server-lazy-db-deps`: Python tools and any tool that needs
87
+ * fresh Python indexes can call `ensureIndexes(dataDb)` without requiring
88
+ * a CodeGraph DB.
79
89
  */
80
- function ensureIndexes(dataDb: Database.Database, codegraphDb: Database.Database, force: boolean = false): string {
90
+ function ensureIndexes(dataDb: Database.Database, codegraphDb?: Database.Database, force: boolean = false): string {
81
91
  const results: string[] = [];
82
92
  const config = getConfig();
83
93
 
84
- // JS indexes
85
- if (force || isDataStale(dataDb, codegraphDb)) {
94
+ // JS indexes — require CodeGraph DB. Skipped when codegraphDb is undefined
95
+ // (e.g., a Python tool or memory tool triggered ensureIndexes for its
96
+ // own Python-index needs without needing JS indexes).
97
+ if (codegraphDb !== undefined && (force || isDataStale(dataDb, codegraphDb))) {
86
98
  const importCount = buildImportIndex(dataDb, codegraphDb);
87
99
  results.push(`Import edges: ${importCount}`);
88
100
 
@@ -259,24 +271,65 @@ export function getToolDefinitions(): ToolDefinition[] {
259
271
  ]);
260
272
  }
261
273
 
274
+ /**
275
+ * Defensive assertion: a routing branch that consumes Data DB requires it
276
+ * to have been resolved by the dispatcher. Should never fire — the manifest
277
+ * declares which tools need Data DB and `server.ts:resolveDbsForTool`
278
+ * resolves accordingly. A throw here indicates a manifest/code drift.
279
+ */
280
+ function assertDataDb(dataDb: Database.Database | undefined, toolName: string): Database.Database {
281
+ if (!dataDb) {
282
+ throw new Error(`Internal: tool "${toolName}" routed to a Data-DB branch but dispatcher did not resolve Data DB. Check TOOL_DB_NEEDS manifest entry includes 'data'.`);
283
+ }
284
+ return dataDb;
285
+ }
286
+
287
+ /**
288
+ * Defensive assertion mirror of {@link assertDataDb} for CodeGraph DB.
289
+ */
290
+ function assertCodegraphDb(codegraphDb: Database.Database | undefined, toolName: string): Database.Database {
291
+ if (!codegraphDb) {
292
+ throw new Error(`Internal: tool "${toolName}" routed to a CodeGraph branch but dispatcher did not resolve CodeGraph DB. Check TOOL_DB_NEEDS manifest entry includes 'codegraph'.`);
293
+ }
294
+ return codegraphDb;
295
+ }
296
+
262
297
  /**
263
298
  * Handle a tool call and return the result.
299
+ *
300
+ * **Pre-dispatch ordering invariant (P-A-002c, plan-1.6.2-server-lazy-db-deps)**:
301
+ * 1. Tier gate (`isToolAllowed`) — checked BEFORE any DB access so
302
+ * free-tier users cannot provoke DB errors on paid tools.
303
+ * 2. Per-family routing (memory/observability/sentinel/knowledge/...) —
304
+ * each family opens ONLY the DB connections its handler needs.
305
+ * 3. Code-intel families (Python, core JS) invoke `ensureIndexes` at
306
+ * the top of their branch with the DBs they have. ensureIndexes is
307
+ * NEVER called unconditionally for memory/audit/knowledge/etc.
308
+ *
309
+ * **DB params**: `dataDb` and `codegraphDb` are OPTIONAL — the dispatcher
310
+ * (`server.ts:resolveDbsForTool`) resolves them based on the tool's entry
311
+ * in `TOOL_DB_NEEDS`. Memory/Knowledge DBs are opened per-call inside
312
+ * their routing branches (existing pattern, unchanged).
313
+ *
314
+ * **Defensive checks**: branches that require a DB call `assertDataDb`
315
+ * or `assertCodegraphDb` first. These should never fire — `TOOL_DB_NEEDS`
316
+ * guarantees the dispatcher resolved the right DBs — but the runtime
317
+ * check catches manifest/code drift loudly instead of silently passing
318
+ * `undefined` into handlers.
264
319
  */
265
320
  export async function handleToolCall(
266
321
  name: string,
267
322
  args: Record<string, unknown>,
268
- dataDb: Database.Database,
269
- codegraphDb: Database.Database
323
+ dataDb?: Database.Database,
324
+ codegraphDb?: Database.Database
270
325
  ): Promise<ToolResult> {
271
- // P3-017: Tier gate — check before any routing
326
+ // P3-017: Tier gate — check before any routing (ordering invariant step 1)
272
327
  const userTier = await getCurrentTier();
273
328
  const requiredTier = getToolTier(name);
274
329
  if (!isToolAllowed(name, userTier)) {
275
330
  return text(`This tool requires ${requiredTier} tier. Current tier: ${userTier}. Upgrade at https://massu.ai/pricing`);
276
331
  }
277
332
 
278
- // Ensure indexes are built before any tool call
279
- const syncMessage = ensureIndexes(dataDb, codegraphDb);
280
333
  const pfx = prefix();
281
334
 
282
335
  try {
@@ -307,7 +360,7 @@ export async function handleToolCall(
307
360
 
308
361
  // Route sentinel tools to sentinel handler
309
362
  if (name.startsWith(pfx + '_sentinel_')) {
310
- return handleSentinelToolCall(name, args, dataDb);
363
+ return handleSentinelToolCall(name, args, assertDataDb(dataDb, name));
311
364
  }
312
365
 
313
366
  // Route analytics layer tools
@@ -375,9 +428,13 @@ export async function handleToolCall(
375
428
  finally { knowledgeDb.close(); }
376
429
  }
377
430
 
378
- // Route Python tools (uses dataDb, not memDb)
431
+ // Route Python tools (uses dataDb only; codegraphDb not required).
432
+ // ensureIndexes runs WITHOUT codegraphDb — only the Python-index section
433
+ // rebuilds (against dataDb). JS-index section is skipped (no codegraph).
379
434
  if (isPythonTool(name)) {
380
- return handlePythonToolCall(name, args, dataDb);
435
+ const pyDataDb = assertDataDb(dataDb, name);
436
+ ensureIndexes(pyDataDb);
437
+ return handlePythonToolCall(name, args, pyDataDb);
381
438
  }
382
439
 
383
440
  // Route license tools
@@ -387,22 +444,51 @@ export async function handleToolCall(
387
444
  finally { memDb.close(); }
388
445
  }
389
446
 
390
- // Match core tools by base name
447
+ // Match core tools by base name. Each codegraph-dependent core tool
448
+ // asserts both DBs and runs ensureIndexes (full JS + Python rebuild
449
+ // when stale).
391
450
  const baseName = stripPrefix(name);
392
451
  switch (baseName) {
393
- case 'sync':
394
- return handleSync(dataDb, codegraphDb);
395
- case 'context':
396
- return handleContext(args.file as string, dataDb, codegraphDb);
397
- case 'trpc_map':
398
- return handleTrpcMap(args, dataDb);
399
- case 'coupling_check':
400
- return handleCouplingCheck(args, dataDb, codegraphDb);
401
- case 'impact':
402
- return handleImpact(args.file as string, dataDb, codegraphDb);
403
- case 'domains':
404
- return handleDomains(args, dataDb, codegraphDb);
452
+ case 'sync': {
453
+ const d = assertDataDb(dataDb, name);
454
+ const c = assertCodegraphDb(codegraphDb, name);
455
+ return handleSync(d, c);
456
+ }
457
+ case 'context': {
458
+ const d = assertDataDb(dataDb, name);
459
+ const c = assertCodegraphDb(codegraphDb, name);
460
+ ensureIndexes(d, c);
461
+ return handleContext(args.file as string, d, c);
462
+ }
463
+ case 'trpc_map': {
464
+ const d = assertDataDb(dataDb, name);
465
+ // trpc_map needs the tRPC index in Data DB. The index is built by
466
+ // ensureIndexes' JS section, which requires CodeGraph DB. If
467
+ // codegraphDb is provided (manifest declares 'codegraph'), rebuild
468
+ // the index; otherwise read whatever stale index exists.
469
+ ensureIndexes(d, codegraphDb);
470
+ return handleTrpcMap(args, d);
471
+ }
472
+ case 'coupling_check': {
473
+ const d = assertDataDb(dataDb, name);
474
+ const c = assertCodegraphDb(codegraphDb, name);
475
+ ensureIndexes(d, c);
476
+ return handleCouplingCheck(args, d, c);
477
+ }
478
+ case 'impact': {
479
+ const d = assertDataDb(dataDb, name);
480
+ const c = assertCodegraphDb(codegraphDb, name);
481
+ ensureIndexes(d, c);
482
+ return handleImpact(args.file as string, d, c);
483
+ }
484
+ case 'domains': {
485
+ const d = assertDataDb(dataDb, name);
486
+ const c = assertCodegraphDb(codegraphDb, name);
487
+ ensureIndexes(d, c);
488
+ return handleDomains(args, d, c);
489
+ }
405
490
  case 'schema':
491
+ // Filesystem-only; no DB access.
406
492
  return handleSchema(args);
407
493
  default:
408
494
  return text(`Unknown tool: ${name}`);