@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.
- package/commands/_shared-preamble.md +76 -0
- package/commands/massu-audit-deps.md +211 -0
- package/commands/massu-changelog.md +174 -0
- package/commands/massu-cleanup.md +315 -0
- package/commands/massu-commit.md +481 -0
- package/commands/massu-create-plan.md +752 -0
- package/commands/massu-dead-code.md +131 -0
- package/commands/massu-debug.md +484 -0
- package/commands/massu-deploy.md +91 -0
- package/commands/massu-deps.md +374 -0
- package/commands/massu-doc-gen.md +279 -0
- package/commands/massu-docs.md +364 -0
- package/commands/massu-estimate.md +313 -0
- package/commands/massu-golden-path.md +973 -0
- package/commands/massu-guide.md +167 -0
- package/commands/massu-hotfix.md +480 -0
- package/commands/massu-loop-playwright.md +837 -0
- package/commands/massu-loop.md +775 -0
- package/commands/massu-new-feature.md +511 -0
- package/commands/massu-parity.md +214 -0
- package/commands/massu-plan.md +456 -0
- package/commands/massu-push-light.md +207 -0
- package/commands/massu-push.md +434 -0
- package/commands/massu-refactor.md +410 -0
- package/commands/massu-release.md +363 -0
- package/commands/massu-review.md +238 -0
- package/commands/massu-simplify.md +281 -0
- package/commands/massu-status.md +278 -0
- package/commands/massu-tdd.md +201 -0
- package/commands/massu-test.md +516 -0
- package/commands/massu-verify-playwright.md +281 -0
- package/commands/massu-verify.md +667 -0
- package/dist/cli.js +12522 -0
- package/dist/hooks/cost-tracker.js +80 -5
- package/dist/hooks/post-edit-context.js +72 -6
- package/dist/hooks/post-tool-use.js +234 -57
- package/dist/hooks/pre-compact.js +144 -5
- package/dist/hooks/pre-delete-check.js +141 -11
- package/dist/hooks/quality-event.js +80 -5
- package/dist/hooks/security-gate.js +29 -0
- package/dist/hooks/session-end.js +83 -8
- package/dist/hooks/session-start.js +153 -7
- package/dist/hooks/user-prompt.js +166 -5
- package/package.json +6 -5
- package/src/backfill-sessions.ts +5 -4
- package/src/cli.ts +6 -0
- package/src/commands/doctor.ts +193 -6
- package/src/commands/init.ts +235 -6
- package/src/commands/install-commands.ts +137 -0
- package/src/config.ts +68 -2
- package/src/db.ts +115 -2
- package/src/docs-tools.ts +8 -6
- package/src/hooks/post-edit-context.ts +1 -1
- package/src/hooks/post-tool-use.ts +130 -0
- package/src/hooks/pre-compact.ts +23 -1
- package/src/hooks/pre-delete-check.ts +92 -4
- package/src/hooks/security-gate.ts +32 -0
- package/src/hooks/session-start.ts +97 -4
- package/src/hooks/user-prompt.ts +46 -1
- package/src/import-resolver.ts +2 -1
- package/src/knowledge-db.ts +169 -0
- package/src/knowledge-indexer.ts +704 -0
- package/src/knowledge-tools.ts +1413 -0
- package/src/license.ts +482 -0
- package/src/memory-db.ts +14 -1
- package/src/observation-extractor.ts +11 -4
- package/src/page-deps.ts +3 -2
- package/src/python/coupling-detector.ts +124 -0
- package/src/python/domain-enforcer.ts +83 -0
- package/src/python/impact-analyzer.ts +95 -0
- package/src/python/import-parser.ts +244 -0
- package/src/python/import-resolver.ts +135 -0
- package/src/python/migration-indexer.ts +115 -0
- package/src/python/migration-parser.ts +332 -0
- package/src/python/model-indexer.ts +70 -0
- package/src/python/model-parser.ts +279 -0
- package/src/python/route-indexer.ts +58 -0
- package/src/python/route-parser.ts +317 -0
- package/src/python-tools.ts +629 -0
- package/src/sentinel-db.ts +2 -1
- package/src/server.ts +29 -6
- package/src/session-archiver.ts +4 -5
- package/src/tools.ts +283 -31
- 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:
|
|
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) {
|
package/src/session-archiver.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
25
|
-
const
|
|
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 {
|
|
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
|
-
|
|
77
|
-
|
|
84
|
+
// JS indexes
|
|
85
|
+
if (force || isDataStale(dataDb, codegraphDb)) {
|
|
86
|
+
const importCount = buildImportIndex(dataDb, codegraphDb);
|
|
87
|
+
results.push(`Import edges: ${importCount}`);
|
|
78
88
|
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
|
125
|
-
...
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|