@massu/core 1.10.8 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/CHANGELOG.md +1303 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +517 -397
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +66 -18
  16. package/dist/hooks/pre-compact.js +27 -2
  17. package/dist/hooks/pre-delete-check.js +48 -22
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +24 -0
  23. package/dist/hooks/session-start.js +8710 -8460
  24. package/dist/hooks/user-prompt.js +24 -0
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/framework-supports.ts +0 -85
  110. package/src/lib/gitToplevel.ts +0 -22
  111. package/src/lib/hook-registry.ts +0 -43
  112. package/src/lib/installLock.ts +0 -66
  113. package/src/lib/memory-path.ts +0 -49
  114. package/src/lib/pidLiveness.ts +0 -67
  115. package/src/lib/settings-local.ts +0 -110
  116. package/src/lib/sql-table-names.ts +0 -71
  117. package/src/license.ts +0 -547
  118. package/src/lsp/auto-detect.ts +0 -98
  119. package/src/lsp/client.ts +0 -776
  120. package/src/lsp/enrich.ts +0 -127
  121. package/src/lsp/types.ts +0 -221
  122. package/src/memory-db.ts +0 -1614
  123. package/src/memory-file-ingest.ts +0 -128
  124. package/src/memory-tools.ts +0 -424
  125. package/src/middleware-tree.ts +0 -71
  126. package/src/observability-tools.ts +0 -343
  127. package/src/observation-extractor.ts +0 -418
  128. package/src/page-deps.ts +0 -285
  129. package/src/permissions.ts +0 -422
  130. package/src/prompt-analyzer.ts +0 -332
  131. package/src/python/coupling-detector.ts +0 -125
  132. package/src/python/domain-enforcer.ts +0 -84
  133. package/src/python/impact-analyzer.ts +0 -96
  134. package/src/python/import-parser.ts +0 -244
  135. package/src/python/import-resolver.ts +0 -136
  136. package/src/python/migration-indexer.ts +0 -116
  137. package/src/python/migration-parser.ts +0 -332
  138. package/src/python/model-indexer.ts +0 -71
  139. package/src/python/model-parser.ts +0 -279
  140. package/src/python/route-indexer.ts +0 -59
  141. package/src/python/route-parser.ts +0 -317
  142. package/src/python-tools.ts +0 -630
  143. package/src/regression-detector.ts +0 -319
  144. package/src/rules.ts +0 -57
  145. package/src/schema-mapper.ts +0 -232
  146. package/src/security/adapter-origin.ts +0 -130
  147. package/src/security/adapter-verifier.ts +0 -319
  148. package/src/security/atomic-write.ts +0 -164
  149. package/src/security/fetcher.ts +0 -200
  150. package/src/security/install-tracking.ts +0 -319
  151. package/src/security/license-pubkey.generated.ts +0 -30
  152. package/src/security/license-response-verifier.ts +0 -149
  153. package/src/security/local-fingerprint.ts +0 -225
  154. package/src/security/manifest-cache.ts +0 -333
  155. package/src/security/manifest-schema.ts +0 -129
  156. package/src/security/registry-pubkey.generated.ts +0 -35
  157. package/src/security/telemetry.ts +0 -320
  158. package/src/security-scorer.ts +0 -405
  159. package/src/security-utils.ts +0 -133
  160. package/src/sentinel-db.ts +0 -580
  161. package/src/sentinel-scanner.ts +0 -406
  162. package/src/sentinel-tools.ts +0 -513
  163. package/src/sentinel-types.ts +0 -140
  164. package/src/server-dispatch.ts +0 -225
  165. package/src/server.ts +0 -107
  166. package/src/session-archiver.ts +0 -111
  167. package/src/session-state-generator.ts +0 -174
  168. package/src/team-knowledge.ts +0 -407
  169. package/src/tool-db-needs.ts +0 -232
  170. package/src/tools.ts +0 -1206
  171. package/src/transcript-parser.ts +0 -458
  172. package/src/trpc-index.ts +0 -215
  173. package/src/validate-features-runner.ts +0 -107
  174. package/src/validation-engine.ts +0 -358
  175. package/src/watch/daemon.ts +0 -385
  176. package/src/watch/lockfile-detector.ts +0 -65
  177. package/src/watch/paths.ts +0 -279
  178. package/src/watch/state.ts +0 -178
package/src/tools.ts DELETED
@@ -1,1206 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- import { readFileSync, existsSync } from 'fs';
5
- import { resolve, basename } from 'path';
6
- import { ensureWithinRoot } from './security-utils.ts';
7
- import type Database from 'better-sqlite3';
8
- import { matchRules } from './rules.ts';
9
- import { buildImportIndex } from './import-resolver.ts';
10
- import { buildTrpcIndex } from './trpc-index.ts';
11
- import { buildPageDeps, findAffectedPages } from './page-deps.ts';
12
- import { buildMiddlewareTree, isInMiddlewareTree, getMiddlewareTree } from './middleware-tree.ts';
13
- import { classifyFile, classifyRouter, findCrossDomainImports, getFilesInDomain } from './domains.ts';
14
- import { parsePrismaSchema, detectMismatches, findColumnUsageInRouters } from './schema-mapper.ts';
15
- import { isDataStale, updateBuildTimestamp, isPythonDataStale, updatePythonBuildTimestamp } from './db.ts';
16
- import { buildPythonImportIndex } from './python/import-resolver.ts';
17
- import { buildPythonRouteIndex } from './python/route-indexer.ts';
18
- import { buildPythonModelIndex } from './python/model-indexer.ts';
19
- import { buildPythonMigrationIndex } from './python/migration-indexer.ts';
20
- import { buildPythonCouplingIndex } from './python/coupling-detector.ts';
21
- import { getMemoryToolDefinitions, handleMemoryToolCall } from './memory-tools.ts';
22
- import { getMemoryDb } from './memory-db.ts';
23
- import { getDocsToolDefinitions, handleDocsToolCall } from './docs-tools.ts';
24
- import { getObservabilityToolDefinitions, handleObservabilityToolCall, isObservabilityTool } from './observability-tools.ts';
25
- import { getSentinelToolDefinitions, handleSentinelToolCall } from './sentinel-tools.ts';
26
- import { runFeatureScan } from './sentinel-scanner.ts';
27
- import { getAnalyticsToolDefinitions, isAnalyticsTool, handleAnalyticsToolCall } from './analytics.ts';
28
- import { getCostToolDefinitions, isCostTool, handleCostToolCall } from './cost-tracker.ts';
29
- import { getPromptToolDefinitions, isPromptTool, handlePromptToolCall } from './prompt-analyzer.ts';
30
- import { getAuditToolDefinitions, isAuditTool, handleAuditToolCall } from './audit-trail.ts';
31
- import { getValidationToolDefinitions, isValidationTool, handleValidationToolCall } from './validation-engine.ts';
32
- import { getAdrToolDefinitions, isAdrTool, handleAdrToolCall } from './adr-generator.ts';
33
- import { getSecurityToolDefinitions, isSecurityTool, handleSecurityToolCall } from './security-scorer.ts';
34
- import { getDependencyToolDefinitions, isDependencyTool, handleDependencyToolCall } from './dependency-scorer.ts';
35
- import { getTeamToolDefinitions, isTeamTool, handleTeamToolCall } from './team-knowledge.ts';
36
- import { getRegressionToolDefinitions, isRegressionTool, handleRegressionToolCall } from './regression-detector.ts';
37
- import { getKnowledgeToolDefinitions, isKnowledgeTool, handleKnowledgeToolCall } from './knowledge-tools.ts';
38
- import { getKnowledgeDb } from './knowledge-db.ts';
39
- import { getPythonToolDefinitions, isPythonTool, handlePythonToolCall } from './python-tools.ts';
40
- import { getConfig, getProjectRoot, getResolvedPaths } from './config.ts';
41
- import { supportsRouter, supportsOrm } from './lib/framework-supports.ts';
42
- import { getCurrentTier, getToolTier, isToolAllowed, annotateToolDefinitions, getLicenseToolDefinitions, isLicenseTool, handleLicenseToolCall, isCloudFeatureAvailable } from './license.ts';
43
- import { t } from './lib/sql-table-names.ts';
44
-
45
- export interface ToolDefinition {
46
- name: string;
47
- description: string;
48
- inputSchema: Record<string, unknown>;
49
- annotations?: Record<string, unknown>;
50
- }
51
-
52
- export interface ToolResult {
53
- content: { type: 'text'; text: string }[];
54
- }
55
-
56
- /** Get the configured tool prefix (e.g., 'massu' or 'myapp') */
57
- function prefix(): string {
58
- return getConfig().toolPrefix;
59
- }
60
-
61
- /** Prefix a tool name with the configured prefix */
62
- function p(name: string): string {
63
- return `${prefix()}_${name}`;
64
- }
65
-
66
- /**
67
- * Strip the configured prefix from a tool name to get the base name.
68
- * e.g., "massu_sync" -> "sync", "massu_memory_search" -> "memory_search"
69
- */
70
- function stripPrefix(name: string): string {
71
- const pfx = prefix() + '_';
72
- if (name.startsWith(pfx)) {
73
- return name.slice(pfx.length);
74
- }
75
- return name;
76
- }
77
-
78
- /**
79
- * Ensure indexes are built and up-to-date.
80
- *
81
- * Lazy initialization: only rebuilds if stale.
82
- *
83
- * **Codegraph-optional**: `codegraphDb` is now optional. When omitted, the
84
- * JS index section (imports, tRPC, pages, middleware — all derived from
85
- * CodeGraph AST data) is skipped. The Python index section (which only
86
- * reads Python source files via tree-sitter into Data DB) still runs.
87
- * This supports the lazy-per-tool-DB design from
88
- * `plan-1.6.2-server-lazy-db-deps`: Python tools and any tool that needs
89
- * fresh Python indexes can call `ensureIndexes(dataDb)` without requiring
90
- * a CodeGraph DB.
91
- */
92
- function ensureIndexes(dataDb: Database.Database, codegraphDb?: Database.Database, force: boolean = false): string {
93
- const results: string[] = [];
94
- const config = getConfig();
95
-
96
- // JS indexes — require CodeGraph DB. Skipped when codegraphDb is undefined
97
- // (e.g., a Python tool or memory tool triggered ensureIndexes for its
98
- // own Python-index needs without needing JS indexes).
99
- if (codegraphDb !== undefined && (force || isDataStale(dataDb, codegraphDb))) {
100
- const importCount = buildImportIndex(dataDb, codegraphDb);
101
- results.push(`Import edges: ${importCount}`);
102
-
103
- if (supportsRouter('trpc')) {
104
- const trpcStats = buildTrpcIndex(dataDb);
105
- results.push(`tRPC procedures: ${trpcStats.totalProcedures} (${trpcStats.withCallers} with UI, ${trpcStats.withoutCallers} without)`);
106
- }
107
-
108
- const pageCount = buildPageDeps(dataDb, codegraphDb);
109
- results.push(`Page deps: ${pageCount} pages`);
110
-
111
- if (config.paths.middleware) {
112
- const middlewareCount = buildMiddlewareTree(dataDb);
113
- results.push(`Middleware tree: ${middlewareCount} files`);
114
- }
115
-
116
- updateBuildTimestamp(dataDb);
117
- }
118
-
119
- // Python indexes — independent of JS staleness
120
- if (config.python?.root) {
121
- const pythonRoot = config.python.root;
122
- const excludeDirs = config.python.exclude_dirs || ['__pycache__', '.venv', 'venv', '.mypy_cache', '.pytest_cache'];
123
-
124
- if (force || isPythonDataStale(dataDb, resolve(getProjectRoot(), pythonRoot))) {
125
- const pyImports = buildPythonImportIndex(dataDb, pythonRoot, excludeDirs);
126
- results.push(`Python imports: ${pyImports}`);
127
-
128
- const pyRoutes = buildPythonRouteIndex(dataDb, pythonRoot, excludeDirs);
129
- results.push(`Python routes: ${pyRoutes}`);
130
-
131
- const pyModels = buildPythonModelIndex(dataDb, pythonRoot, excludeDirs);
132
- results.push(`Python models: ${pyModels}`);
133
-
134
- if (config.python.alembic_dir) {
135
- const pyMigrations = buildPythonMigrationIndex(dataDb, config.python.alembic_dir);
136
- results.push(`Python migrations: ${pyMigrations}`);
137
- }
138
-
139
- const pyCoupling = buildPythonCouplingIndex(dataDb);
140
- results.push(`Python coupling: ${pyCoupling}`);
141
-
142
- updatePythonBuildTimestamp(dataDb);
143
- }
144
- }
145
-
146
- if (results.length === 0) return 'Indexes are up-to-date.';
147
- return `Indexes rebuilt:\n${results.join('\n')}`;
148
- }
149
-
150
- /**
151
- * Get all tool definitions for the MCP server.
152
- */
153
- export function getToolDefinitions(): ToolDefinition[] {
154
- const config = getConfig();
155
-
156
- return annotateToolDefinitions([
157
- // Memory tools
158
- ...getMemoryToolDefinitions(),
159
- // Observability tools
160
- ...getObservabilityToolDefinitions(),
161
- // Docs tools
162
- ...getDocsToolDefinitions(),
163
- // Sentinel tools (feature registry)
164
- ...getSentinelToolDefinitions(),
165
- // Analytics layer (quality trends, cost tracking, prompt analysis)
166
- ...getAnalyticsToolDefinitions(),
167
- ...getCostToolDefinitions(),
168
- ...getPromptToolDefinitions(),
169
- // Governance layer (audit trail, validation, ADR)
170
- ...getAuditToolDefinitions(),
171
- ...getValidationToolDefinitions(),
172
- ...getAdrToolDefinitions(),
173
- // Security layer (security scoring, dependency risk)
174
- ...getSecurityToolDefinitions(),
175
- ...getDependencyToolDefinitions(),
176
- // Enterprise layer (team knowledge, regression detection)
177
- // P-A-003: team tools are cloud-gated — only listed when cloud.enabled is true.
178
- ...(isCloudFeatureAvailable() ? getTeamToolDefinitions() : []),
179
- ...getRegressionToolDefinitions(),
180
- // Knowledge layer (indexed .claude/ knowledge — rules, patterns, incidents)
181
- ...getKnowledgeToolDefinitions(),
182
- // Python code intelligence tools
183
- ...getPythonToolDefinitions(),
184
- // License tools (always available)
185
- ...getLicenseToolDefinitions(),
186
- // Core tools
187
- {
188
- name: p('sync'),
189
- description: 'Force rebuild all indexes (import edges, tRPC mappings, page deps, middleware tree). Run this after significant code changes.',
190
- inputSchema: {
191
- type: 'object',
192
- properties: {},
193
- required: [],
194
- },
195
- },
196
- {
197
- name: p('context'),
198
- description: 'Get context for a file: applicable rules, pattern warnings, schema mismatch alerts, and whether the file is in the middleware import tree.',
199
- inputSchema: {
200
- type: 'object',
201
- properties: {
202
- file: { type: 'string', description: 'File path relative to project root' },
203
- },
204
- required: ['file'],
205
- },
206
- },
207
- ...(supportsRouter('trpc') ? [
208
- {
209
- name: p('trpc_map'),
210
- description: 'Map tRPC procedures to their UI call sites. Find which components call a router, which procedures have no UI callers, or list all procedures for a router.',
211
- inputSchema: {
212
- type: 'object',
213
- properties: {
214
- router: { type: 'string', description: 'Router name (e.g., "orders")' },
215
- procedure: { type: 'string', description: 'Procedure name to search across all routers' },
216
- uncoupled: { type: 'boolean', description: 'If true, show only procedures with ZERO UI callers' },
217
- },
218
- required: [],
219
- },
220
- },
221
- {
222
- name: p('coupling_check'),
223
- description: 'Automated coupling check. Finds all procedures with zero UI callers and components not rendered in any page.',
224
- inputSchema: {
225
- type: 'object',
226
- properties: {
227
- staged_files: {
228
- type: 'array',
229
- items: { type: 'string' },
230
- description: 'Optional: only check these specific files',
231
- },
232
- },
233
- required: [],
234
- },
235
- },
236
- ] : []),
237
- {
238
- name: p('impact'),
239
- description: 'Full impact analysis for a file: which pages are affected, which database tables are in the chain, middleware tree membership, domain crossings.',
240
- inputSchema: {
241
- type: 'object',
242
- properties: {
243
- file: { type: 'string', description: 'File path relative to project root' },
244
- },
245
- required: ['file'],
246
- },
247
- },
248
- ...(config.domains.length > 0 ? [{
249
- name: p('domains'),
250
- description: 'Domain boundary information. Classify a file into its domain, show cross-domain imports, or list all files in a domain.',
251
- inputSchema: {
252
- type: 'object',
253
- properties: {
254
- file: { type: 'string', description: 'File to classify into a domain' },
255
- crossings: { type: 'boolean', description: 'Show all cross-domain imports (violations highlighted)' },
256
- domain: { type: 'string', description: 'Domain name to list all files for' },
257
- },
258
- required: [],
259
- },
260
- }] : []),
261
- ...(supportsOrm('prisma') ? [{
262
- name: p('schema'),
263
- description: 'Prisma schema cross-reference. Show columns for a table, detect mismatches between code and schema, or verify column references in a file.',
264
- inputSchema: {
265
- type: 'object',
266
- properties: {
267
- table: { type: 'string', description: 'Table/model name to inspect' },
268
- mismatches: { type: 'boolean', description: 'Show all detected column name mismatches' },
269
- verify: { type: 'string', description: 'File path to verify column references against schema' },
270
- },
271
- required: [],
272
- },
273
- }] : []),
274
- ]);
275
- }
276
-
277
- /**
278
- * Defensive assertion: a routing branch that consumes Data DB requires it
279
- * to have been resolved by the dispatcher. Should never fire — the manifest
280
- * declares which tools need Data DB and `server.ts:resolveDbsForTool`
281
- * resolves accordingly. A throw here indicates a manifest/code drift.
282
- */
283
- function assertDataDb(dataDb: Database.Database | undefined, toolName: string): Database.Database {
284
- if (!dataDb) {
285
- 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'.`);
286
- }
287
- return dataDb;
288
- }
289
-
290
- /**
291
- * Defensive assertion mirror of {@link assertDataDb} for CodeGraph DB.
292
- */
293
- function assertCodegraphDb(codegraphDb: Database.Database | undefined, toolName: string): Database.Database {
294
- if (!codegraphDb) {
295
- 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'.`);
296
- }
297
- return codegraphDb;
298
- }
299
-
300
- /**
301
- * Handle a tool call and return the result.
302
- *
303
- * **Pre-dispatch ordering invariant (P-A-002c, plan-1.6.2-server-lazy-db-deps)**:
304
- * 1. Tier gate (`isToolAllowed`) — checked BEFORE any DB access so
305
- * free-tier users cannot provoke DB errors on paid tools.
306
- * 2. Per-family routing (memory/observability/sentinel/knowledge/...) —
307
- * each family opens ONLY the DB connections its handler needs.
308
- * 3. Code-intel families (Python, core JS) invoke `ensureIndexes` at
309
- * the top of their branch with the DBs they have. ensureIndexes is
310
- * NEVER called unconditionally for memory/audit/knowledge/etc.
311
- *
312
- * **DB params**: `dataDb` and `codegraphDb` are OPTIONAL — the dispatcher
313
- * (`server.ts:resolveDbsForTool`) resolves them based on the tool's entry
314
- * in `TOOL_DB_NEEDS`. Memory/Knowledge DBs are opened per-call inside
315
- * their routing branches (existing pattern, unchanged).
316
- *
317
- * **Defensive checks**: branches that require a DB call `assertDataDb`
318
- * or `assertCodegraphDb` first. These should never fire — `TOOL_DB_NEEDS`
319
- * guarantees the dispatcher resolved the right DBs — but the runtime
320
- * check catches manifest/code drift loudly instead of silently passing
321
- * `undefined` into handlers.
322
- */
323
- export async function handleToolCall(
324
- name: string,
325
- args: Record<string, unknown>,
326
- dataDb?: Database.Database,
327
- codegraphDb?: Database.Database
328
- ): Promise<ToolResult> {
329
- // P3-017: Tier gate — check before any routing (ordering invariant step 1)
330
- const userTier = await getCurrentTier();
331
- const requiredTier = getToolTier(name);
332
- if (!isToolAllowed(name, userTier)) {
333
- return text(`This tool requires ${requiredTier} tier. Current tier: ${userTier}. Upgrade at https://massu.ai/pricing`);
334
- }
335
-
336
- const pfx = prefix();
337
-
338
- try {
339
- // Route memory tools to memory handler
340
- if (name.startsWith(pfx + '_memory_')) {
341
- const memDb = getMemoryDb();
342
- try {
343
- return handleMemoryToolCall(name, args, memDb);
344
- } finally {
345
- memDb.close();
346
- }
347
- }
348
-
349
- // Route observability tools to observability handler
350
- if (isObservabilityTool(name)) {
351
- const memDb = getMemoryDb();
352
- try {
353
- return handleObservabilityToolCall(name, args, memDb);
354
- } finally {
355
- memDb.close();
356
- }
357
- }
358
-
359
- // Route docs tools to docs handler
360
- if (name.startsWith(pfx + '_docs_')) {
361
- return handleDocsToolCall(name, args);
362
- }
363
-
364
- // Route sentinel tools to sentinel handler
365
- if (name.startsWith(pfx + '_sentinel_')) {
366
- return handleSentinelToolCall(name, args, assertDataDb(dataDb, name));
367
- }
368
-
369
- // Route analytics layer tools
370
- if (isAnalyticsTool(name)) {
371
- const memDb = getMemoryDb();
372
- try { return handleAnalyticsToolCall(name, args, memDb); }
373
- finally { memDb.close(); }
374
- }
375
- if (isCostTool(name)) {
376
- const memDb = getMemoryDb();
377
- try { return handleCostToolCall(name, args, memDb); }
378
- finally { memDb.close(); }
379
- }
380
- if (isPromptTool(name)) {
381
- const memDb = getMemoryDb();
382
- try { return handlePromptToolCall(name, args, memDb); }
383
- finally { memDb.close(); }
384
- }
385
-
386
- // Route governance layer tools
387
- if (isAuditTool(name)) {
388
- const memDb = getMemoryDb();
389
- try { return handleAuditToolCall(name, args, memDb); }
390
- finally { memDb.close(); }
391
- }
392
- if (isValidationTool(name)) {
393
- const memDb = getMemoryDb();
394
- try { return handleValidationToolCall(name, args, memDb); }
395
- finally { memDb.close(); }
396
- }
397
- if (isAdrTool(name)) {
398
- const memDb = getMemoryDb();
399
- try { return handleAdrToolCall(name, args, memDb); }
400
- finally { memDb.close(); }
401
- }
402
-
403
- // Route security layer tools
404
- if (isSecurityTool(name)) {
405
- const memDb = getMemoryDb();
406
- try { return handleSecurityToolCall(name, args, memDb); }
407
- finally { memDb.close(); }
408
- }
409
- if (isDependencyTool(name)) {
410
- const memDb = getMemoryDb();
411
- try { return handleDependencyToolCall(name, args, memDb); }
412
- finally { memDb.close(); }
413
- }
414
-
415
- // Route enterprise layer tools
416
- // P-A-003: team-tool dispatch gated on cloud availability; if a stale client
417
- // still has the tool name cached, fall through to the unknown-tool branch.
418
- if (isTeamTool(name) && isCloudFeatureAvailable()) {
419
- const memDb = getMemoryDb();
420
- try { return handleTeamToolCall(name, args, memDb); }
421
- finally { memDb.close(); }
422
- }
423
- if (isRegressionTool(name)) {
424
- const memDb = getMemoryDb();
425
- try { return handleRegressionToolCall(name, args, memDb); }
426
- finally { memDb.close(); }
427
- }
428
-
429
- // Route knowledge layer tools
430
- if (isKnowledgeTool(name)) {
431
- const knowledgeDb = getKnowledgeDb();
432
- try { return handleKnowledgeToolCall(name, args, knowledgeDb); }
433
- finally { knowledgeDb.close(); }
434
- }
435
-
436
- // Route Python tools (uses dataDb only; codegraphDb not required).
437
- // ensureIndexes runs WITHOUT codegraphDb — only the Python-index section
438
- // rebuilds (against dataDb). JS-index section is skipped (no codegraph).
439
- if (isPythonTool(name)) {
440
- const pyDataDb = assertDataDb(dataDb, name);
441
- ensureIndexes(pyDataDb);
442
- return handlePythonToolCall(name, args, pyDataDb);
443
- }
444
-
445
- // Route license tools
446
- if (isLicenseTool(name)) {
447
- const memDb = getMemoryDb();
448
- try { return await handleLicenseToolCall(name, args, memDb); }
449
- finally { memDb.close(); }
450
- }
451
-
452
- // Match core tools by base name. Each codegraph-dependent core tool
453
- // asserts both DBs and runs ensureIndexes (full JS + Python rebuild
454
- // when stale).
455
- const baseName = stripPrefix(name);
456
- switch (baseName) {
457
- case 'sync': {
458
- const d = assertDataDb(dataDb, name);
459
- const c = assertCodegraphDb(codegraphDb, name);
460
- return handleSync(d, c);
461
- }
462
- case 'context': {
463
- const d = assertDataDb(dataDb, name);
464
- const c = assertCodegraphDb(codegraphDb, name);
465
- ensureIndexes(d, c);
466
- return handleContext(args.file as string, d, c);
467
- }
468
- case 'trpc_map': {
469
- const d = assertDataDb(dataDb, name);
470
- // trpc_map needs the tRPC index in Data DB. The index is built by
471
- // ensureIndexes' JS section, which requires CodeGraph DB. If
472
- // codegraphDb is provided (manifest declares 'codegraph'), rebuild
473
- // the index; otherwise read whatever stale index exists.
474
- ensureIndexes(d, codegraphDb);
475
- return handleTrpcMap(args, d);
476
- }
477
- case 'coupling_check': {
478
- const d = assertDataDb(dataDb, name);
479
- const c = assertCodegraphDb(codegraphDb, name);
480
- ensureIndexes(d, c);
481
- return handleCouplingCheck(args, d, c);
482
- }
483
- case 'impact': {
484
- const d = assertDataDb(dataDb, name);
485
- const c = assertCodegraphDb(codegraphDb, name);
486
- ensureIndexes(d, c);
487
- return handleImpact(args.file as string, d, c);
488
- }
489
- case 'domains': {
490
- const d = assertDataDb(dataDb, name);
491
- const c = assertCodegraphDb(codegraphDb, name);
492
- ensureIndexes(d, c);
493
- return handleDomains(args, d, c);
494
- }
495
- case 'schema':
496
- // Filesystem-only; no DB access.
497
- return handleSchema(args);
498
- default:
499
- return text(`Unknown tool: ${name}`);
500
- }
501
- } catch (error) {
502
- const msg = error instanceof Error ? error.message : String(error);
503
- // Strip file paths and stack traces from error messages exposed to clients
504
- const safeMsg = msg.split('\n')[0].replace(/\/(Users|home|var|tmp)\/[^\s:]+/g, '<path>');
505
- return text(`Error in ${name}: ${safeMsg}`);
506
- }
507
- }
508
-
509
- function text(content: string): ToolResult {
510
- return { content: [{ type: 'text', text: content }] };
511
- }
512
-
513
- // === Tool Handlers ===
514
-
515
- function handleSync(dataDb: Database.Database, codegraphDb: Database.Database): ToolResult {
516
- const result = ensureIndexes(dataDb, codegraphDb, true);
517
-
518
- // Run feature auto-discovery after index rebuild
519
- try {
520
- const scanResult = runFeatureScan(dataDb);
521
- return text(`${result}\n\nFeature scan: ${scanResult.registered} features registered (${scanResult.fromProcedures} from procedures, ${scanResult.fromPages} from pages, ${scanResult.fromComponents} from components)`);
522
- } catch (error) {
523
- return text(`${result}\n\nFeature scan failed: ${error instanceof Error ? error.message : String(error)}`);
524
- }
525
- }
526
-
527
- function handleContext(file: string, dataDb: Database.Database, codegraphDb: Database.Database): ToolResult {
528
- const lines: string[] = [];
529
-
530
- // 1. CodeGraph context
531
- const nodes = codegraphDb.prepare(
532
- "SELECT name, kind, start_line, end_line FROM nodes WHERE file_path = ? ORDER BY start_line"
533
- ).all(file) as { name: string; kind: string; start_line: number; end_line: number }[];
534
-
535
- if (nodes.length > 0) {
536
- lines.push('## CodeGraph Nodes');
537
- for (const node of nodes.slice(0, 30)) {
538
- lines.push(`- ${node.kind}: ${node.name} (L${node.start_line}-${node.end_line})`);
539
- }
540
- if (nodes.length > 30) {
541
- lines.push(`... and ${nodes.length - 30} more`);
542
- }
543
- lines.push('');
544
- }
545
-
546
- // 2. Applicable rules
547
- const rules = matchRules(file);
548
- if (rules.length > 0) {
549
- lines.push('## Applicable Rules');
550
- for (const rule of rules) {
551
- const severity = rule.severity ? `[${rule.severity}]` : '';
552
- for (const r of rule.rules) {
553
- lines.push(`- ${severity} ${r}`);
554
- }
555
- if (rule.patternFile) {
556
- lines.push(` See: .claude/${rule.patternFile}`);
557
- }
558
- }
559
- lines.push('');
560
- }
561
-
562
- // 3. Middleware tree check
563
- if (isInMiddlewareTree(dataDb, file)) {
564
- lines.push('## WARNING: Middleware Import Tree');
565
- lines.push('This file is imported (directly or transitively) by the middleware entry point.');
566
- lines.push('NO Node.js dependencies allowed (pino, winston, fs, crypto, path, child_process).');
567
- lines.push('');
568
- }
569
-
570
- // 4. Domain classification
571
- const domain = classifyFile(file);
572
- lines.push(`## Domain: ${domain}`);
573
- lines.push('');
574
-
575
- // 5. Import edges
576
- const imports = dataDb.prepare(
577
- `SELECT target_file, imported_names FROM ${t('imports')} WHERE source_file = ? LIMIT 20`
578
- ).all(file) as { target_file: string; imported_names: string }[];
579
-
580
- if (imports.length > 0) {
581
- lines.push('## Imports (from this file)');
582
- for (const imp of imports) {
583
- const names = JSON.parse(imp.imported_names);
584
- lines.push(`- ${imp.target_file}${names.length > 0 ? ': ' + names.join(', ') : ''}`);
585
- }
586
- lines.push('');
587
- }
588
-
589
- // 6. Imported BY
590
- const importedBy = dataDb.prepare(
591
- `SELECT source_file FROM ${t('imports')} WHERE target_file = ? LIMIT 20`
592
- ).all(file) as { source_file: string }[];
593
-
594
- if (importedBy.length > 0) {
595
- lines.push('## Imported By');
596
- for (const imp of importedBy) {
597
- lines.push(`- ${imp.source_file}`);
598
- }
599
- lines.push('');
600
- }
601
-
602
- // 7. Knowledge context (relevant CRs, schema warnings, incidents, corrections)
603
- try {
604
- const knowledgeDb = getKnowledgeDb();
605
- try {
606
- const docCount = (knowledgeDb.prepare('SELECT COUNT(*) as cnt FROM knowledge_documents').get() as { cnt: number })?.cnt ?? 0;
607
- if (docCount > 0) {
608
- // Schema mismatch warnings: check if file references any known-mismatch tables
609
- const mismatches = knowledgeDb.prepare('SELECT table_name, wrong_column, correct_column FROM knowledge_schema_mismatches').all() as {
610
- table_name: string; wrong_column: string; correct_column: string;
611
- }[];
612
-
613
- if (mismatches.length > 0) {
614
- if (file.includes('router') || file.includes('server/api')) {
615
- lines.push('## Schema Mismatch Warnings');
616
- lines.push('Known column name traps (from CLAUDE.md):');
617
- for (const m of mismatches.slice(0, 5)) {
618
- lines.push(`- \`${m.table_name}.${m.wrong_column}\` → use \`${m.correct_column}\` instead`);
619
- }
620
- lines.push('');
621
- }
622
- }
623
-
624
- // Dynamic rule matching — combine file-type defaults with content-based FTS
625
- const fileType = file.includes('router') || file.includes('server/api') ? 'router'
626
- : file.includes('components') || file.includes('app/') ? 'component'
627
- : file.includes('middleware') ? 'middleware'
628
- : file.includes('migration') ? 'migration'
629
- : 'other';
630
-
631
- const domainDefaultCRs: Record<string, string[]> = {
632
- router: ['CR-2', 'CR-6'],
633
- component: ['CR-8', 'CR-12'],
634
- middleware: ['CR-16', 'CR-19'],
635
- migration: ['CR-2', 'CR-36'],
636
- };
637
-
638
- const defaultCRs = domainDefaultCRs[fileType] || [];
639
-
640
- // Extract keywords from file content for FTS matching
641
- let ftsRuleIds: string[] = [];
642
- try {
643
- const resolvedPaths = getResolvedPaths();
644
- const root = getProjectRoot();
645
- const absFilePath = ensureWithinRoot(resolve(resolvedPaths.srcDir, '..', file), root);
646
- if (existsSync(absFilePath)) {
647
- const fileContent = readFileSync(absFilePath, 'utf-8').slice(0, 3000);
648
- const keywords: string[] = [];
649
- if (fileContent.includes('ctx.db')) keywords.push('database', 'schema');
650
- if (fileContent.includes('BigInt') || fileContent.includes('Decimal')) keywords.push('BigInt', 'serialization');
651
- if (fileContent.includes('protectedProcedure') || fileContent.includes('publicProcedure')) keywords.push('procedure', 'mutation');
652
- if (fileContent.includes('Select') || fileContent.includes('value=')) keywords.push('Select', 'value');
653
- if (fileContent.includes('include:')) keywords.push('include', 'relation');
654
- if (fileContent.includes('migration') || fileContent.includes('ALTER TABLE')) keywords.push('migration', 'schema');
655
- if (fileContent.includes('RLS') || fileContent.includes('policy')) keywords.push('RLS', 'policy');
656
- if (fileContent.includes('onPointerDown') || fileContent.includes('onClick')) keywords.push('stylus', 'pointer');
657
-
658
- if (keywords.length > 0) {
659
- const ftsQuery = keywords.slice(0, 5).map(k => `"${k}"`).join(' OR ');
660
- try {
661
- const ftsResults = knowledgeDb.prepare(`
662
- SELECT DISTINCT kc.heading as rule_id
663
- FROM knowledge_fts kf
664
- JOIN knowledge_chunks kc ON kc.id = kf.rowid
665
- WHERE kf.content MATCH ? AND kc.chunk_type = 'rule'
666
- LIMIT 8
667
- `).all(ftsQuery) as { rule_id: string }[];
668
- ftsRuleIds = ftsResults.map(r => r.rule_id).filter(id => id.startsWith('CR-'));
669
- } catch { /* FTS syntax error — fall back to defaults */ }
670
- }
671
- }
672
- } catch { /* File read error — fall back to defaults */ }
673
-
674
- // Combine: defaults + FTS-discovered, deduplicated
675
- const relevantCRs = [...new Set([...defaultCRs, ...ftsRuleIds])].slice(0, 10);
676
-
677
- if (relevantCRs.length > 0) {
678
- const placeholders = relevantCRs.map(() => '?').join(',');
679
- const crRules = knowledgeDb.prepare(
680
- `SELECT rule_id, rule_text, vr_type FROM knowledge_rules WHERE rule_id IN (${placeholders})`
681
- ).all(...relevantCRs) as { rule_id: string; rule_text: string; vr_type: string }[];
682
-
683
- if (crRules.length > 0) {
684
- lines.push('## Relevant Canonical Rules');
685
- for (const cr of crRules) {
686
- lines.push(`- **${cr.rule_id}**: ${cr.rule_text} (${cr.vr_type})`);
687
- }
688
- lines.push('');
689
- }
690
- }
691
-
692
- // Relevant incidents based on file type
693
- const domainKeywords: Record<string, string[]> = {
694
- router: ['Schema', 'Migration', 'Database', 'API'],
695
- component: ['UI', 'Render', 'Component', 'UX'],
696
- middleware: ['Auth', 'Edge', 'Build'],
697
- migration: ['Schema', 'Migration', 'Database'],
698
- };
699
- const incidentKeywords = domainKeywords[fileType] || [];
700
- if (incidentKeywords.length > 0) {
701
- const kwPlaceholders = incidentKeywords.map(() => '?').join(' OR type LIKE ');
702
- const kwParams = incidentKeywords.map(k => `%${k}%`);
703
- const incidents = knowledgeDb.prepare(
704
- `SELECT incident_num, date, type, gap_found, prevention FROM knowledge_incidents WHERE type LIKE ${kwPlaceholders} ORDER BY incident_num DESC LIMIT 5`
705
- ).all(...kwParams) as { incident_num: number; date: string; type: string; gap_found: string; prevention: string }[];
706
-
707
- if (incidents.length > 0) {
708
- lines.push('## Related Incidents');
709
- for (const inc of incidents) {
710
- lines.push(`- **#${inc.incident_num}** (${inc.type}): ${inc.gap_found}`);
711
- }
712
- lines.push('');
713
- }
714
- }
715
-
716
- // Active corrections (behavioral learning)
717
- try {
718
- const corrections = knowledgeDb.prepare(`
719
- SELECT heading, content FROM knowledge_chunks
720
- WHERE metadata LIKE '%"is_correction":true%'
721
- ORDER BY CAST(json_extract(metadata, '$.date') AS TEXT) DESC
722
- LIMIT 3
723
- `).all() as { heading: string; content: string }[];
724
-
725
- if (corrections.length > 0) {
726
- lines.push('## Active Corrections');
727
- for (const c of corrections) {
728
- const ruleLine = c.content.split('\n').find(l => l.startsWith('Rule:')) || c.content.split('\n')[0];
729
- lines.push(`- ${c.heading}: ${ruleLine}`);
730
- }
731
- lines.push('');
732
- }
733
- } catch { /* corrections query failed — graceful degradation */ }
734
- }
735
- } finally {
736
- knowledgeDb.close();
737
- }
738
- } catch {
739
- // Knowledge DB not available — graceful degradation
740
- }
741
-
742
- // 8. Memory context — past bugs/fixes for this file
743
- let memDb: Database.Database | null = null;
744
- try {
745
- memDb = getMemoryDb();
746
- const fileObservations = memDb.prepare(`
747
- SELECT o.type, o.title, o.cr_rule, o.importance, o.created_at
748
- FROM observations o
749
- WHERE o.files_involved LIKE ?
750
- ORDER BY o.importance DESC, o.created_at_epoch DESC
751
- LIMIT 5
752
- `).all(`%${basename(file)}%`) as { type: string; title: string; cr_rule: string | null; importance: number; created_at: string }[];
753
-
754
- if (fileObservations.length > 0) {
755
- lines.push('## Past Observations (This File)');
756
- for (const obs of fileObservations) {
757
- const crTag = obs.cr_rule ? ` [${obs.cr_rule}]` : '';
758
- const impTag = obs.importance >= 4 ? ' **HIGH**' : '';
759
- lines.push(`- [${obs.type}] ${obs.title}${crTag}${impTag} (${obs.created_at})`);
760
- }
761
- lines.push('');
762
- }
763
-
764
- // Also check for failed attempts on this file
765
- const failures = memDb.prepare(`
766
- SELECT title, detail
767
- FROM observations
768
- WHERE type = 'failed_attempt' AND files_involved LIKE ?
769
- ORDER BY recurrence_count DESC
770
- LIMIT 3
771
- `).all(`%${basename(file)}%`) as { title: string; detail: string }[];
772
-
773
- if (failures.length > 0) {
774
- lines.push('## Failed Attempts (DO NOT RETRY)');
775
- for (const f of failures) {
776
- lines.push(`- ${f.title}`);
777
- }
778
- lines.push('');
779
- }
780
- } catch { /* Memory DB not available — graceful degradation */ }
781
- finally { memDb?.close(); }
782
-
783
- return text(lines.join('\n') || 'No context available for this file.');
784
- }
785
-
786
- function handleTrpcMap(args: Record<string, unknown>, dataDb: Database.Database): ToolResult {
787
- const lines: string[] = [];
788
-
789
- if (args.uncoupled) {
790
- const uncoupled = dataDb.prepare(
791
- `SELECT router_name, procedure_name, procedure_type, router_file FROM ${t('trpc_procedures')} WHERE has_ui_caller = 0 ORDER BY router_name, procedure_name`
792
- ).all() as { router_name: string; procedure_name: string; procedure_type: string; router_file: string }[];
793
-
794
- lines.push(`## Uncoupled Procedures (${uncoupled.length} total)`);
795
- lines.push('These procedures have ZERO UI callers.');
796
- lines.push('');
797
-
798
- let currentRouter = '';
799
- for (const proc of uncoupled) {
800
- if (proc.router_name !== currentRouter) {
801
- currentRouter = proc.router_name;
802
- lines.push(`### ${currentRouter} (${proc.router_file})`);
803
- }
804
- lines.push(`- ${proc.procedure_name} (${proc.procedure_type})`);
805
- }
806
- } else if (args.router) {
807
- const procs = dataDb.prepare(
808
- `SELECT id, procedure_name, procedure_type, has_ui_caller FROM ${t('trpc_procedures')} WHERE router_name = ? ORDER BY procedure_name`
809
- ).all(args.router as string) as { id: number; procedure_name: string; procedure_type: string; has_ui_caller: number }[];
810
-
811
- lines.push(`## Router: ${args.router} (${procs.length} procedures)`);
812
- lines.push('');
813
-
814
- for (const proc of procs) {
815
- const status = proc.has_ui_caller ? '' : ' [NO UI CALLERS]';
816
- lines.push(`### ${args.router}.${proc.procedure_name} (${proc.procedure_type})${status}`);
817
-
818
- const callSites = dataDb.prepare(
819
- `SELECT file, line, call_pattern FROM ${t('trpc_call_sites')} WHERE procedure_id = ?`
820
- ).all(proc.id) as { file: string; line: number; call_pattern: string }[];
821
-
822
- if (callSites.length > 0) {
823
- lines.push('UI Call Sites:');
824
- for (const site of callSites) {
825
- lines.push(` - ${site.file}:${site.line} -> ${site.call_pattern}`);
826
- }
827
- } else {
828
- lines.push('UI Call Sites: NONE');
829
- }
830
- lines.push('');
831
- }
832
- } else if (args.procedure) {
833
- const procs = dataDb.prepare(
834
- `SELECT id, router_name, router_file, procedure_type, has_ui_caller FROM ${t('trpc_procedures')} WHERE procedure_name = ? ORDER BY router_name`
835
- ).all(args.procedure as string) as { id: number; router_name: string; router_file: string; procedure_type: string; has_ui_caller: number }[];
836
-
837
- lines.push(`## Procedure "${args.procedure}" found in ${procs.length} routers`);
838
- lines.push('');
839
-
840
- for (const proc of procs) {
841
- lines.push(`### ${proc.router_name}.${args.procedure} (${proc.procedure_type})`);
842
- lines.push(`File: ${proc.router_file}`);
843
-
844
- const callSites = dataDb.prepare(
845
- `SELECT file, line, call_pattern FROM ${t('trpc_call_sites')} WHERE procedure_id = ?`
846
- ).all(proc.id) as { file: string; line: number; call_pattern: string }[];
847
-
848
- if (callSites.length > 0) {
849
- lines.push('UI Call Sites:');
850
- for (const site of callSites) {
851
- lines.push(` - ${site.file}:${site.line} -> ${site.call_pattern}`);
852
- }
853
- } else {
854
- lines.push('UI Call Sites: NONE');
855
- }
856
- lines.push('');
857
- }
858
- } else {
859
- const total = dataDb.prepare(`SELECT COUNT(*) as count FROM ${t('trpc_procedures')}`).get() as { count: number };
860
- const coupled = dataDb.prepare(`SELECT COUNT(*) as count FROM ${t('trpc_procedures')} WHERE has_ui_caller = 1`).get() as { count: number };
861
- const uncoupled = total.count - coupled.count;
862
-
863
- // P-H009 (plan-stage-c-high-batch): when the index is empty, return an
864
- // actionable remedy hint instead of bare "0" — previously fresh installs
865
- // saw "Total procedures: 0" and assumed the flagship tool was broken.
866
- if (total.count === 0) {
867
- lines.push('## tRPC Index Empty');
868
- lines.push('');
869
- lines.push('No tRPC procedures indexed yet. Either this repo has no tRPC');
870
- lines.push('routers, or the CodeGraph index has not been built. To rebuild:');
871
- lines.push('');
872
- lines.push(' npx massu sync');
873
- lines.push('');
874
- lines.push('If `massu sync` completes successfully but `trpc_map` still');
875
- lines.push('returns 0, the repo likely has no tRPC procedures (this is');
876
- lines.push('expected for non-tRPC stacks — try `schema` or `domains` tools).');
877
- } else {
878
- lines.push('## tRPC Procedure Summary');
879
- lines.push(`- Total procedures: ${total.count}`);
880
- lines.push(`- With UI callers: ${coupled.count}`);
881
- lines.push(`- Without UI callers: ${uncoupled}`);
882
- lines.push('');
883
- lines.push('Use { router: "name" } to see details for a specific router.');
884
- lines.push('Use { uncoupled: true } to see all procedures without UI callers.');
885
- }
886
- }
887
-
888
- return text(lines.join('\n'));
889
- }
890
-
891
- function handleCouplingCheck(args: Record<string, unknown>, dataDb: Database.Database, codegraphDb: Database.Database): ToolResult {
892
- const lines: string[] = [];
893
- const stagedFiles = args.staged_files as string[] | undefined;
894
-
895
- let uncoupledProcs;
896
- if (stagedFiles) {
897
- uncoupledProcs = dataDb.prepare(
898
- `SELECT router_name, procedure_name, procedure_type, router_file FROM ${t('trpc_procedures')} WHERE has_ui_caller = 0 AND router_file IN (${stagedFiles.map(() => '?').join(',')})`
899
- ).all(...stagedFiles) as { router_name: string; procedure_name: string; procedure_type: string; router_file: string }[];
900
- } else {
901
- uncoupledProcs = dataDb.prepare(
902
- `SELECT router_name, procedure_name, procedure_type, router_file FROM ${t('trpc_procedures')} WHERE has_ui_caller = 0`
903
- ).all() as { router_name: string; procedure_name: string; procedure_type: string; router_file: string }[];
904
- }
905
-
906
- lines.push('## Coupling Check Results');
907
- lines.push('');
908
-
909
- if (uncoupledProcs.length > 0) {
910
- lines.push(`### Uncoupled Procedures: ${uncoupledProcs.length}`);
911
- for (const proc of uncoupledProcs) {
912
- lines.push(`- ${proc.router_name}.${proc.procedure_name} (${proc.procedure_type}) in ${proc.router_file}`);
913
- }
914
- lines.push('');
915
- } else {
916
- lines.push('### Uncoupled Procedures: 0 (PASS)');
917
- lines.push('');
918
- }
919
-
920
- const allPages = codegraphDb.prepare(
921
- "SELECT path FROM files WHERE path LIKE 'src/app/%/page.tsx' OR path = 'src/app/page.tsx'"
922
- ).all() as { path: string }[];
923
-
924
- const pageImports = new Set<string>();
925
- for (const page of allPages) {
926
- const imports = dataDb.prepare(
927
- `SELECT target_file FROM ${t('imports')} WHERE source_file = ?`
928
- ).all(page.path) as { target_file: string }[];
929
- for (const imp of imports) {
930
- pageImports.add(imp.target_file);
931
- }
932
- }
933
-
934
- let componentFiles: { path: string }[];
935
- if (stagedFiles) {
936
- const placeholders = stagedFiles.map(() => '?').join(',');
937
- componentFiles = codegraphDb.prepare(
938
- `SELECT path FROM files WHERE path LIKE 'src/components/%' AND path IN (${placeholders})`
939
- ).all(...stagedFiles) as { path: string }[];
940
- } else {
941
- componentFiles = [];
942
- }
943
-
944
- const orphanComponents = componentFiles.filter(f => !pageImports.has(f.path));
945
- if (orphanComponents.length > 0) {
946
- lines.push(`### Orphan Components: ${orphanComponents.length}`);
947
- for (const comp of orphanComponents) {
948
- lines.push(`- ${comp.path} (not imported by any page.tsx)`);
949
- }
950
- lines.push('');
951
- }
952
-
953
- const totalIssues = uncoupledProcs.length + orphanComponents.length;
954
- lines.push(`### RESULT: ${totalIssues === 0 ? 'PASS' : `FAIL (${totalIssues} issues)`}`);
955
-
956
- return text(lines.join('\n'));
957
- }
958
-
959
- function handleImpact(file: string, dataDb: Database.Database, codegraphDb: Database.Database): ToolResult {
960
- const lines: string[] = [];
961
-
962
- lines.push(`## Impact Analysis: ${file}`);
963
- lines.push('');
964
-
965
- const affectedPages = findAffectedPages(dataDb, file);
966
-
967
- if (affectedPages.length > 0) {
968
- const portals = [...new Set(affectedPages.map(p => p.portal))];
969
- const allTables = [...new Set(affectedPages.flatMap(p => p.tables))];
970
- const allRouters = [...new Set(affectedPages.flatMap(p => p.routers))];
971
-
972
- lines.push(`### Pages Affected: ${affectedPages.length}`);
973
- for (const page of affectedPages) {
974
- lines.push(`- ${page.route} (${page.portal})`);
975
- }
976
- lines.push('');
977
-
978
- lines.push(`### Scopes Affected: ${portals.join(', ')}`);
979
- lines.push('');
980
-
981
- if (allRouters.length > 0) {
982
- lines.push(`### Routers Called (via hooks/components):`);
983
- for (const router of allRouters) {
984
- lines.push(`- ${router}`);
985
- }
986
- lines.push('');
987
- }
988
-
989
- if (allTables.length > 0) {
990
- lines.push(`### Database Tables:`);
991
- for (const table of allTables) {
992
- lines.push(`- ${table}`);
993
- }
994
- lines.push('');
995
- }
996
- } else {
997
- lines.push('No pages affected (file may not be in any page dependency chain).');
998
- lines.push('');
999
- }
1000
-
1001
- const inMiddleware = isInMiddlewareTree(dataDb, file);
1002
- if (inMiddleware) {
1003
- lines.push('### WARNING: In Middleware Import Tree');
1004
- lines.push('Changes to this file affect Edge Runtime. No Node.js deps allowed.');
1005
- } else {
1006
- lines.push('### Middleware: NOT in middleware import tree (safe)');
1007
- }
1008
- lines.push('');
1009
-
1010
- const fileDomain = classifyFile(file);
1011
- lines.push(`### Domain: ${fileDomain}`);
1012
-
1013
- const imports = dataDb.prepare(
1014
- `SELECT target_file FROM ${t('imports')} WHERE source_file = ?`
1015
- ).all(file) as { target_file: string }[];
1016
-
1017
- const crossings: string[] = [];
1018
- for (const imp of imports) {
1019
- const targetDomain = classifyFile(imp.target_file);
1020
- if (targetDomain !== fileDomain && targetDomain !== 'Unknown') {
1021
- crossings.push(`${imp.target_file} (${targetDomain})`);
1022
- }
1023
- }
1024
-
1025
- if (crossings.length > 0) {
1026
- lines.push(`### Domain Crossings: ${crossings.length}`);
1027
- for (const crossing of crossings) {
1028
- lines.push(`- -> ${crossing}`);
1029
- }
1030
- }
1031
-
1032
- return text(lines.join('\n'));
1033
- }
1034
-
1035
- function handleDomains(args: Record<string, unknown>, dataDb: Database.Database, codegraphDb: Database.Database): ToolResult {
1036
- const lines: string[] = [];
1037
- const domains = getConfig().domains;
1038
-
1039
- if (args.file) {
1040
- const file = args.file as string;
1041
- const domain = classifyFile(file);
1042
- lines.push(`## ${file}`);
1043
- lines.push(`Domain: ${domain}`);
1044
-
1045
- const domainConfig = domains.find(d => d.name === domain);
1046
- if (domainConfig) {
1047
- lines.push(`Allowed imports from: ${domainConfig.allowedImportsFrom.join(', ') || 'any domain (system)'}`);
1048
- }
1049
- } else if (args.crossings) {
1050
- const crossings = findCrossDomainImports(dataDb);
1051
- const violations = crossings.filter(c => !c.allowed);
1052
- const allowed = crossings.filter(c => c.allowed);
1053
-
1054
- lines.push(`## Cross-Domain Import Analysis`);
1055
- lines.push(`Total crossings: ${crossings.length}`);
1056
- lines.push(`Violations: ${violations.length}`);
1057
- lines.push(`Allowed: ${allowed.length}`);
1058
- lines.push('');
1059
-
1060
- if (violations.length > 0) {
1061
- lines.push('### Violations (Disallowed Cross-Domain Imports)');
1062
- for (const v of violations.slice(0, 50)) {
1063
- lines.push(`- ${v.source} (${v.sourceDomain}) -> ${v.target} (${v.targetDomain})`);
1064
- }
1065
- if (violations.length > 50) {
1066
- lines.push(`... and ${violations.length - 50} more`);
1067
- }
1068
- }
1069
- } else if (args.domain) {
1070
- const domainName = args.domain as string;
1071
- const files = getFilesInDomain(dataDb, codegraphDb, domainName);
1072
- const config = domains.find(d => d.name === domainName);
1073
-
1074
- lines.push(`## Domain: ${domainName}`);
1075
- if (config) {
1076
- lines.push(`Allowed imports from: ${config.allowedImportsFrom.join(', ') || 'any domain (system)'}`);
1077
- }
1078
- lines.push('');
1079
-
1080
- lines.push(`### Routers (${files.routers.length})`);
1081
- for (const r of files.routers) lines.push(`- ${r}`);
1082
- lines.push('');
1083
-
1084
- lines.push(`### Pages (${files.pages.length})`);
1085
- for (const p of files.pages) lines.push(`- ${p}`);
1086
- lines.push('');
1087
-
1088
- lines.push(`### Components (${files.components.length})`);
1089
- for (const c of files.components.slice(0, 30)) lines.push(`- ${c}`);
1090
- if (files.components.length > 30) lines.push(`... and ${files.components.length - 30} more`);
1091
- } else {
1092
- lines.push('## Domain Summary');
1093
- for (const domain of domains) {
1094
- lines.push(`- **${domain.name}**: ${domain.routers.length} router patterns, imports from: ${domain.allowedImportsFrom.join(', ') || 'any'}`);
1095
- }
1096
- }
1097
-
1098
- return text(lines.join('\n'));
1099
- }
1100
-
1101
- function handleSchema(args: Record<string, unknown>): ToolResult {
1102
- const lines: string[] = [];
1103
- const models = parsePrismaSchema();
1104
-
1105
- if (args.mismatches) {
1106
- const mismatches = detectMismatches(models);
1107
-
1108
- lines.push(`## Schema Mismatches Detected: ${mismatches.length}`);
1109
- lines.push('');
1110
-
1111
- for (const m of mismatches) {
1112
- lines.push(`### ${m.table}.${m.codeColumn} [${m.severity}]`);
1113
- lines.push(`Code uses "${m.codeColumn}" but this column does NOT exist in the schema.`);
1114
- lines.push(`Files affected:`);
1115
- for (const f of m.files) {
1116
- lines.push(` - ${f}`);
1117
- }
1118
- lines.push('');
1119
- }
1120
-
1121
- if (mismatches.length === 0) {
1122
- lines.push('No known mismatches detected in code.');
1123
- }
1124
- } else if (args.table) {
1125
- const tableName = args.table as string;
1126
- const model = models.find(m => m.tableName === tableName || m.name === tableName);
1127
-
1128
- if (!model) {
1129
- return text(`Model/table "${tableName}" not found in Prisma schema.`);
1130
- }
1131
-
1132
- lines.push(`## ${model.name} (table: ${model.tableName})`);
1133
- lines.push('');
1134
- lines.push('### Fields');
1135
- for (const field of model.fields) {
1136
- const nullable = field.nullable ? '?' : '';
1137
- const relation = field.isRelation ? ' [RELATION]' : '';
1138
- lines.push(`- ${field.name}: ${field.type}${nullable}${relation}`);
1139
- }
1140
- lines.push('');
1141
-
1142
- const usage = findColumnUsageInRouters(model.tableName);
1143
- if (usage.size > 0) {
1144
- lines.push('### Column Usage in Routers');
1145
- for (const [col, usages] of usage) {
1146
- const validField = model.fields.find(f => f.name === col);
1147
- const status = validField ? '' : ' [NOT IN SCHEMA]';
1148
- lines.push(`- ${col}${status}: ${usages.length} references`);
1149
- }
1150
- }
1151
- } else if (args.verify) {
1152
- const file = args.verify as string;
1153
- lines.push(`## Schema Verification: ${file}`);
1154
- lines.push('Checking all column references against Prisma schema...');
1155
- lines.push('');
1156
-
1157
- const projectRoot = getProjectRoot();
1158
- const absPath = ensureWithinRoot(resolve(projectRoot, file), projectRoot);
1159
-
1160
- if (!existsSync(absPath)) {
1161
- return text(`File not found: ${file}`);
1162
- }
1163
-
1164
- const source = readFileSync(absPath, 'utf-8');
1165
-
1166
- // Use configurable db access pattern
1167
- const config = getConfig();
1168
- const dbPattern = config.dbAccessPattern ?? 'ctx.db.{table}';
1169
- const regexStr = dbPattern
1170
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
1171
- .replace('\\{table\\}', '(\\w+)');
1172
- const tableRegex = new RegExp(regexStr + '\\.', 'g');
1173
- const tableRefs = new Set<string>();
1174
- let match;
1175
- while ((match = tableRegex.exec(source)) !== null) {
1176
- tableRefs.add(match[1]);
1177
- }
1178
-
1179
- for (const table of tableRefs) {
1180
- const model = models.find(m => m.tableName === table || m.name.toLowerCase() === table);
1181
- if (!model) {
1182
- lines.push(`### ${table}: MODEL NOT FOUND IN SCHEMA`);
1183
- continue;
1184
- }
1185
-
1186
- lines.push(`### ${table} (model: ${model.name})`);
1187
- const fieldNames = new Set(model.fields.map(f => f.name));
1188
- lines.push(`Schema has ${fieldNames.size} fields.`);
1189
- lines.push('');
1190
- }
1191
- } else {
1192
- lines.push(`## Prisma Schema Summary`);
1193
- lines.push(`Models: ${models.length}`);
1194
- lines.push('');
1195
-
1196
- const mismatches = detectMismatches(models);
1197
- if (mismatches.length > 0) {
1198
- lines.push(`### Active Mismatches: ${mismatches.length}`);
1199
- for (const m of mismatches) {
1200
- lines.push(`- ${m.table}.${m.codeColumn} [${m.severity}]`);
1201
- }
1202
- }
1203
- }
1204
-
1205
- return text(lines.join('\n'));
1206
- }