@massu/core 0.1.2 → 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 (84) 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 +12522 -0
  34. package/dist/hooks/cost-tracker.js +80 -5
  35. package/dist/hooks/post-edit-context.js +72 -6
  36. package/dist/hooks/post-tool-use.js +234 -57
  37. package/dist/hooks/pre-compact.js +144 -5
  38. package/dist/hooks/pre-delete-check.js +141 -11
  39. package/dist/hooks/quality-event.js +80 -5
  40. package/dist/hooks/security-gate.js +29 -0
  41. package/dist/hooks/session-end.js +83 -8
  42. package/dist/hooks/session-start.js +153 -7
  43. package/dist/hooks/user-prompt.js +166 -5
  44. package/package.json +6 -5
  45. package/src/backfill-sessions.ts +5 -4
  46. package/src/cli.ts +6 -0
  47. package/src/commands/doctor.ts +193 -6
  48. package/src/commands/init.ts +235 -6
  49. package/src/commands/install-commands.ts +137 -0
  50. package/src/config.ts +68 -2
  51. package/src/db.ts +115 -2
  52. package/src/docs-tools.ts +8 -6
  53. package/src/hooks/post-edit-context.ts +1 -1
  54. package/src/hooks/post-tool-use.ts +130 -0
  55. package/src/hooks/pre-compact.ts +23 -1
  56. package/src/hooks/pre-delete-check.ts +92 -4
  57. package/src/hooks/security-gate.ts +32 -0
  58. package/src/hooks/session-start.ts +97 -4
  59. package/src/hooks/user-prompt.ts +46 -1
  60. package/src/import-resolver.ts +2 -1
  61. package/src/knowledge-db.ts +169 -0
  62. package/src/knowledge-indexer.ts +704 -0
  63. package/src/knowledge-tools.ts +1413 -0
  64. package/src/license.ts +482 -0
  65. package/src/memory-db.ts +14 -1
  66. package/src/observation-extractor.ts +11 -4
  67. package/src/page-deps.ts +3 -2
  68. package/src/python/coupling-detector.ts +124 -0
  69. package/src/python/domain-enforcer.ts +83 -0
  70. package/src/python/impact-analyzer.ts +95 -0
  71. package/src/python/import-parser.ts +244 -0
  72. package/src/python/import-resolver.ts +135 -0
  73. package/src/python/migration-indexer.ts +115 -0
  74. package/src/python/migration-parser.ts +332 -0
  75. package/src/python/model-indexer.ts +70 -0
  76. package/src/python/model-parser.ts +279 -0
  77. package/src/python/route-indexer.ts +58 -0
  78. package/src/python/route-parser.ts +317 -0
  79. package/src/python-tools.ts +629 -0
  80. package/src/sentinel-db.ts +2 -1
  81. package/src/server.ts +29 -6
  82. package/src/session-archiver.ts +4 -5
  83. package/src/tools.ts +283 -31
  84. package/README.md +0 -40
package/src/server.ts CHANGED
@@ -11,10 +11,24 @@
11
11
  * Tool names are configurable via massu.config.yaml toolPrefix.
12
12
  */
13
13
 
14
+ import { readFileSync } from 'fs';
15
+ import { resolve, dirname } from 'path';
16
+ import { fileURLToPath } from 'url';
14
17
  import { getCodeGraphDb, getDataDb } from './db.ts';
15
18
  import { getConfig } from './config.ts';
16
19
  import { getToolDefinitions, handleToolCall } from './tools.ts';
17
20
  import { getMemoryDb, pruneOldConversationTurns, pruneOldObservations } from './memory-db.ts';
21
+ import { getCurrentTier } from './license.ts';
22
+
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ const PKG_VERSION = (() => {
25
+ try {
26
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'));
27
+ return pkg.version ?? '0.0.0';
28
+ } catch {
29
+ return '0.0.0';
30
+ }
31
+ })();
18
32
 
19
33
  interface JsonRpcRequest {
20
34
  jsonrpc: '2.0';
@@ -40,7 +54,7 @@ function getDb() {
40
54
  return { codegraphDb, dataDb: dataDb };
41
55
  }
42
56
 
43
- function handleRequest(request: JsonRpcRequest): JsonRpcResponse {
57
+ async function handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse> {
44
58
  const { method, params, id } = request;
45
59
 
46
60
  switch (method) {
@@ -54,8 +68,8 @@ function handleRequest(request: JsonRpcRequest): JsonRpcResponse {
54
68
  tools: {},
55
69
  },
56
70
  serverInfo: {
57
- name: 'massu',
58
- version: '1.0.0',
71
+ name: getConfig().toolPrefix || 'massu',
72
+ version: PKG_VERSION,
59
73
  },
60
74
  },
61
75
  };
@@ -80,7 +94,7 @@ function handleRequest(request: JsonRpcRequest): JsonRpcResponse {
80
94
  const toolArgs = (params as { arguments?: Record<string, unknown> })?.arguments ?? {};
81
95
 
82
96
  const { codegraphDb: cgDb, dataDb: lDb } = getDb();
83
- const result = handleToolCall(toolName, toolArgs, lDb, cgDb);
97
+ const result = await handleToolCall(toolName, toolArgs, lDb, cgDb);
84
98
 
85
99
  return {
86
100
  jsonrpc: '2.0',
@@ -133,12 +147,21 @@ function pruneMemoryOnStartup(): void {
133
147
 
134
148
  pruneMemoryOnStartup();
135
149
 
150
+ // === License init: pre-cache tier status ===
151
+ getCurrentTier().then(tier => {
152
+ process.stderr.write(`massu: License tier: ${tier}\n`);
153
+ }).catch(error => {
154
+ process.stderr.write(
155
+ `massu: License check failed (non-fatal): ${error instanceof Error ? error.message : String(error)}\n`
156
+ );
157
+ });
158
+
136
159
  // === stdio JSON-RPC transport ===
137
160
 
138
161
  let buffer = '';
139
162
 
140
163
  process.stdin.setEncoding('utf-8');
141
- process.stdin.on('data', (chunk: string) => {
164
+ process.stdin.on('data', async (chunk: string) => {
142
165
  buffer += chunk;
143
166
 
144
167
  // Process complete messages (newline-delimited JSON-RPC)
@@ -151,7 +174,7 @@ process.stdin.on('data', (chunk: string) => {
151
174
 
152
175
  try {
153
176
  const request = JSON.parse(line) as JsonRpcRequest;
154
- const response = handleRequest(request);
177
+ const response = await handleRequest(request);
155
178
 
156
179
  // Don't send responses for notifications (no id)
157
180
  if (request.id !== undefined) {
@@ -5,14 +5,12 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from '
5
5
  import { resolve, dirname } from 'path';
6
6
  import type Database from 'better-sqlite3';
7
7
  import { generateCurrentMd } from './session-state-generator.ts';
8
- import { getProjectRoot } from './config.ts';
8
+ import { getResolvedPaths } from './config.ts';
9
9
 
10
10
  // ============================================================
11
11
  // P5-002: Session Archiver
12
12
  // ============================================================
13
13
 
14
- const PROJECT_ROOT = getProjectRoot();
15
-
16
14
  /**
17
15
  * Archive the current CURRENT.md and generate a new one from memory DB.
18
16
  */
@@ -21,8 +19,9 @@ export function archiveAndRegenerate(db: Database.Database, sessionId: string):
21
19
  archivePath?: string;
22
20
  newContent: string;
23
21
  } {
24
- const currentMdPath = resolve(PROJECT_ROOT, '.claude/session-state/CURRENT.md');
25
- const archiveDir = resolve(PROJECT_ROOT, '.claude/session-state/archive');
22
+ const resolved = getResolvedPaths();
23
+ const currentMdPath = resolved.sessionStatePath;
24
+ const archiveDir = resolved.sessionArchivePath;
26
25
  let archived = false;
27
26
  let archivePath: string | undefined;
28
27
 
package/src/tools.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  // Licensed under BSL 1.1 - see LICENSE file for details.
3
3
 
4
4
  import { readFileSync, existsSync } from 'fs';
5
- import { resolve } from 'path';
5
+ import { resolve, basename } from 'path';
6
+ import { ensureWithinRoot } from './security-utils.ts';
6
7
  import type Database from 'better-sqlite3';
7
8
  import { matchRules } from './rules.ts';
8
9
  import { buildImportIndex } from './import-resolver.ts';
@@ -11,7 +12,12 @@ import { buildPageDeps, findAffectedPages } from './page-deps.ts';
11
12
  import { buildMiddlewareTree, isInMiddlewareTree, getMiddlewareTree } from './middleware-tree.ts';
12
13
  import { classifyFile, classifyRouter, findCrossDomainImports, getFilesInDomain } from './domains.ts';
13
14
  import { parsePrismaSchema, detectMismatches, findColumnUsageInRouters } from './schema-mapper.ts';
14
- import { isDataStale, updateBuildTimestamp } from './db.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';
15
21
  import { getMemoryToolDefinitions, handleMemoryToolCall } from './memory-tools.ts';
16
22
  import { getMemoryDb } from './memory-db.ts';
17
23
  import { getDocsToolDefinitions, handleDocsToolCall } from './docs-tools.ts';
@@ -28,12 +34,17 @@ import { getSecurityToolDefinitions, isSecurityTool, handleSecurityToolCall } fr
28
34
  import { getDependencyToolDefinitions, isDependencyTool, handleDependencyToolCall } from './dependency-scorer.ts';
29
35
  import { getTeamToolDefinitions, isTeamTool, handleTeamToolCall } from './team-knowledge.ts';
30
36
  import { getRegressionToolDefinitions, isRegressionTool, handleRegressionToolCall } from './regression-detector.ts';
31
- import { getConfig, getProjectRoot } from './config.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';
32
42
 
33
43
  export interface ToolDefinition {
34
44
  name: string;
35
45
  description: string;
36
46
  inputSchema: Record<string, unknown>;
47
+ tier?: 'free' | 'pro' | 'team' | 'enterprise';
37
48
  }
38
49
 
39
50
  export interface ToolResult {
@@ -67,31 +78,58 @@ function stripPrefix(name: string): string {
67
78
  * Lazy initialization: only rebuilds if stale.
68
79
  */
69
80
  function ensureIndexes(dataDb: Database.Database, codegraphDb: Database.Database, force: boolean = false): string {
70
- if (!force && !isDataStale(dataDb, codegraphDb)) {
71
- return 'Indexes are up-to-date.';
72
- }
73
-
74
81
  const results: string[] = [];
82
+ const config = getConfig();
75
83
 
76
- const importCount = buildImportIndex(dataDb, codegraphDb);
77
- results.push(`Import edges: ${importCount}`);
84
+ // JS indexes
85
+ if (force || isDataStale(dataDb, codegraphDb)) {
86
+ const importCount = buildImportIndex(dataDb, codegraphDb);
87
+ results.push(`Import edges: ${importCount}`);
78
88
 
79
- const config = getConfig();
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`);
80
96
 
81
- if (config.framework.router === 'trpc') {
82
- const trpcStats = buildTrpcIndex(dataDb);
83
- results.push(`tRPC procedures: ${trpcStats.totalProcedures} (${trpcStats.withCallers} with UI, ${trpcStats.withoutCallers} without)`);
97
+ if (config.paths.middleware) {
98
+ const middlewareCount = buildMiddlewareTree(dataDb);
99
+ results.push(`Middleware tree: ${middlewareCount} files`);
100
+ }
101
+
102
+ updateBuildTimestamp(dataDb);
84
103
  }
85
104
 
86
- const pageCount = buildPageDeps(dataDb, codegraphDb);
87
- results.push(`Page deps: ${pageCount} pages`);
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}`);
88
113
 
89
- if (config.paths.middleware) {
90
- const middlewareCount = buildMiddlewareTree(dataDb);
91
- results.push(`Middleware tree: ${middlewareCount} files`);
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
+ }
92
130
  }
93
131
 
94
- updateBuildTimestamp(dataDb);
132
+ if (results.length === 0) return 'Indexes are up-to-date.';
95
133
  return `Indexes rebuilt:\n${results.join('\n')}`;
96
134
  }
97
135
 
@@ -101,7 +139,7 @@ function ensureIndexes(dataDb: Database.Database, codegraphDb: Database.Database
101
139
  export function getToolDefinitions(): ToolDefinition[] {
102
140
  const config = getConfig();
103
141
 
104
- return [
142
+ return annotateToolDefinitions([
105
143
  // Memory tools
106
144
  ...getMemoryToolDefinitions(),
107
145
  // Observability tools
@@ -121,9 +159,15 @@ export function getToolDefinitions(): ToolDefinition[] {
121
159
  // Security layer (security scoring, dependency risk)
122
160
  ...getSecurityToolDefinitions(),
123
161
  ...getDependencyToolDefinitions(),
124
- // Enterprise layer (team knowledge — cloud-only; regression detection — always)
125
- ...(config.cloud?.enabled ? getTeamToolDefinitions() : []),
162
+ // Enterprise layer (team knowledge, regression detection)
163
+ ...getTeamToolDefinitions(),
126
164
  ...getRegressionToolDefinitions(),
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(),
127
171
  // Core tools
128
172
  {
129
173
  name: p('sync'),
@@ -212,18 +256,25 @@ export function getToolDefinitions(): ToolDefinition[] {
212
256
  required: [],
213
257
  },
214
258
  }] : []),
215
- ];
259
+ ]);
216
260
  }
217
261
 
218
262
  /**
219
263
  * Handle a tool call and return the result.
220
264
  */
221
- export function handleToolCall(
265
+ export async function handleToolCall(
222
266
  name: string,
223
267
  args: Record<string, unknown>,
224
268
  dataDb: Database.Database,
225
269
  codegraphDb: Database.Database
226
- ): 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
+
227
278
  // Ensure indexes are built before any tool call
228
279
  const syncMessage = ensureIndexes(dataDb, codegraphDb);
229
280
  const pfx = prefix();
@@ -305,11 +356,8 @@ export function handleToolCall(
305
356
  finally { memDb.close(); }
306
357
  }
307
358
 
308
- // Route enterprise layer tools (team tools require cloud sync)
359
+ // Route enterprise layer tools
309
360
  if (isTeamTool(name)) {
310
- if (!getConfig().cloud?.enabled) {
311
- return text('This tool requires Cloud Team or Enterprise. Configure cloud sync to enable.');
312
- }
313
361
  const memDb = getMemoryDb();
314
362
  try { return handleTeamToolCall(name, args, memDb); }
315
363
  finally { memDb.close(); }
@@ -320,6 +368,25 @@ export function handleToolCall(
320
368
  finally { memDb.close(); }
321
369
  }
322
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
+ }
389
+
323
390
  // Match core tools by base name
324
391
  const baseName = stripPrefix(name);
325
392
  switch (baseName) {
@@ -341,7 +408,10 @@ export function handleToolCall(
341
408
  return text(`Unknown tool: ${name}`);
342
409
  }
343
410
  } catch (error) {
344
- 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}`);
345
415
  }
346
416
  }
347
417
 
@@ -438,6 +508,187 @@ function handleContext(file: string, dataDb: Database.Database, codegraphDb: Dat
438
508
  lines.push('');
439
509
  }
440
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
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
+
441
692
  return text(lines.join('\n') || 'No context available for this file.');
442
693
  }
443
694
 
@@ -796,7 +1047,8 @@ function handleSchema(args: Record<string, unknown>): ToolResult {
796
1047
  lines.push('Checking all column references against Prisma schema...');
797
1048
  lines.push('');
798
1049
 
799
- const absPath = resolve(getProjectRoot(), file);
1050
+ const projectRoot = getProjectRoot();
1051
+ const absPath = ensureWithinRoot(resolve(projectRoot, file), projectRoot);
800
1052
 
801
1053
  if (!existsSync(absPath)) {
802
1054
  return text(`File not found: ${file}`);
package/README.md DELETED
@@ -1,40 +0,0 @@
1
- # @massu/core
2
-
3
- AI Engineering Governance MCP Server — session memory, feature registry, code intelligence, and rule enforcement for AI coding assistants.
4
-
5
- ## Quick Start
6
-
7
- ```bash
8
- npx massu init
9
- ```
10
-
11
- This sets up the MCP server, configuration, and lifecycle hooks in one command.
12
-
13
- ## What is Massu?
14
-
15
- Massu is a source-available [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that adds governance capabilities to AI coding assistants like Claude Code. It provides:
16
-
17
- - **51 MCP Tools** — quality analytics, cost tracking, security scoring, dependency analysis, and more
18
- - **11 Lifecycle Hooks** — pre-commit gates, security scanning, intent suggestion, and session management
19
- - **3-Database Architecture** — code graph (read-only), data (imports/mappings), and memory (sessions/analytics)
20
- - **Config-Driven** — all project-specific data lives in `massu.config.yaml`
21
-
22
- ## Usage
23
-
24
- After `npx massu init`, your AI assistant gains access to all governance tools automatically via the MCP protocol.
25
-
26
- ```bash
27
- # Health check
28
- npx massu doctor
29
-
30
- # Validate configuration
31
- npx massu validate-config
32
- ```
33
-
34
- ## Documentation
35
-
36
- Full documentation at [massu.ai](https://massu.ai).
37
-
38
- ## License
39
-
40
- [BSL 1.1](https://github.com/massu-ai/massu/blob/main/LICENSE) — source-available. Free to use, modify, and distribute. See LICENSE for full terms.