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