@massu/core 0.7.0 → 0.8.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/README.md CHANGED
@@ -14,8 +14,8 @@ This sets up the MCP server, configuration, and lifecycle hooks in one command.
14
14
 
15
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
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
17
+ - **73 MCP Tools** — quality analytics, cost tracking, security scoring, dependency analysis, and more
18
+ - **15 Lifecycle Hooks** — pre-commit gates, security scanning, intent suggestion, session management, and auto-learning pipeline
19
19
  - **3-Database Architecture** — code graph (read-only), data (imports/mappings), and memory (sessions/analytics)
20
20
  - **Config-Driven** — all project-specific data lives in `massu.config.yaml`
21
21
 
package/dist/cli.js CHANGED
@@ -268,6 +268,18 @@ var init_config = __esm({
268
268
  "added_missing_import"
269
269
  ])
270
270
  }).default({}),
271
+ failureClassification: z.object({
272
+ enabled: z.boolean().default(true),
273
+ thresholds: z.object({
274
+ known: z.number().default(5),
275
+ similar: z.number().default(3)
276
+ }).default({}),
277
+ scoring: z.object({
278
+ diffPatternWeight: z.number().default(3),
279
+ filePatternWeight: z.number().default(2),
280
+ promptKeywordWeight: z.number().default(2)
281
+ }).default({})
282
+ }).default({}),
271
283
  pipeline: z.object({
272
284
  requireIncidentReport: z.boolean().default(true),
273
285
  requirePreventionRule: z.boolean().default(true),
@@ -375,10 +387,12 @@ var init_config = __esm({
375
387
  var memory_db_exports = {};
376
388
  __export(memory_db_exports, {
377
389
  addConversationTurn: () => addConversationTurn,
390
+ addFailureClass: () => addFailureClass,
378
391
  addObservation: () => addObservation,
379
392
  addSummary: () => addSummary,
380
393
  addToolCallDetail: () => addToolCallDetail,
381
394
  addUserPrompt: () => addUserPrompt,
395
+ appendIncidentToFailureClass: () => appendIncidentToFailureClass,
382
396
  assignImportance: () => assignImportance,
383
397
  autoDetectTaskId: () => autoDetectTaskId,
384
398
  createSession: () => createSession,
@@ -390,6 +404,7 @@ __export(memory_db_exports, {
390
404
  getCrossTaskProgress: () => getCrossTaskProgress,
391
405
  getDecisionsAbout: () => getDecisionsAbout,
392
406
  getFailedAttempts: () => getFailedAttempts,
407
+ getFailureClasses: () => getFailureClasses,
393
408
  getLastProcessedLine: () => getLastProcessedLine,
394
409
  getMemoryDb: () => getMemoryDb,
395
410
  getObservabilityDbSize: () => getObservabilityDbSize,
@@ -406,6 +421,7 @@ __export(memory_db_exports, {
406
421
  pruneOldObservations: () => pruneOldObservations,
407
422
  removePendingSync: () => removePendingSync,
408
423
  sanitizeFts5Query: () => sanitizeFts5Query,
424
+ scoreFailureClasses: () => scoreFailureClasses,
409
425
  searchConversationTurns: () => searchConversationTurns,
410
426
  searchObservations: () => searchObservations,
411
427
  setLastProcessedLine: () => setLastProcessedLine
@@ -905,6 +921,25 @@ function initMemorySchema(db) {
905
921
  features TEXT DEFAULT '[]'
906
922
  );
907
923
  `);
924
+ db.exec(`
925
+ CREATE TABLE IF NOT EXISTS failure_classes (
926
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
927
+ name TEXT NOT NULL UNIQUE,
928
+ description TEXT NOT NULL,
929
+ diff_patterns TEXT NOT NULL DEFAULT '[]',
930
+ file_patterns TEXT NOT NULL DEFAULT '[]',
931
+ prompt_keywords TEXT NOT NULL DEFAULT '[]',
932
+ incidents TEXT NOT NULL DEFAULT '[]',
933
+ rules TEXT NOT NULL DEFAULT '[]',
934
+ scanner_checks TEXT NOT NULL DEFAULT '[]',
935
+ known_message TEXT NOT NULL DEFAULT '',
936
+ needs_review INTEGER NOT NULL DEFAULT 0,
937
+ created_at TEXT DEFAULT (datetime('now')),
938
+ updated_at TEXT DEFAULT (datetime('now'))
939
+ );
940
+ CREATE INDEX IF NOT EXISTS idx_fc_name ON failure_classes(name);
941
+ CREATE INDEX IF NOT EXISTS idx_fc_needs_review ON failure_classes(needs_review);
942
+ `);
908
943
  }
909
944
  function enqueueSyncPayload(db, payload) {
910
945
  db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
@@ -1338,6 +1373,108 @@ function getObservabilityDbSize(db) {
1338
1373
  estimated_size_mb: Math.round(pageCount * pageSize / (1024 * 1024) * 100) / 100
1339
1374
  };
1340
1375
  }
1376
+ function addFailureClass(db, opts) {
1377
+ const result = db.prepare(`
1378
+ INSERT OR IGNORE INTO failure_classes (name, description, diff_patterns, file_patterns, prompt_keywords, incidents, rules, scanner_checks, known_message, needs_review)
1379
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1380
+ `).run(
1381
+ opts.name,
1382
+ opts.description,
1383
+ JSON.stringify(opts.diffPatterns ?? []),
1384
+ JSON.stringify(opts.filePatterns ?? []),
1385
+ JSON.stringify(opts.promptKeywords ?? []),
1386
+ JSON.stringify(opts.incidents ?? []),
1387
+ JSON.stringify(opts.rules ?? []),
1388
+ JSON.stringify(opts.scannerChecks ?? []),
1389
+ opts.knownMessage ?? "",
1390
+ opts.needsReview ? 1 : 0
1391
+ );
1392
+ return Number(result.lastInsertRowid);
1393
+ }
1394
+ function getFailureClasses(db) {
1395
+ const rows = db.prepare("SELECT * FROM failure_classes ORDER BY name").all();
1396
+ return rows.map((row) => ({
1397
+ id: row.id,
1398
+ name: row.name,
1399
+ description: row.description,
1400
+ diff_patterns: JSON.parse(row.diff_patterns || "[]"),
1401
+ file_patterns: JSON.parse(row.file_patterns || "[]"),
1402
+ prompt_keywords: JSON.parse(row.prompt_keywords || "[]"),
1403
+ incidents: JSON.parse(row.incidents || "[]"),
1404
+ rules: JSON.parse(row.rules || "[]"),
1405
+ scanner_checks: JSON.parse(row.scanner_checks || "[]"),
1406
+ known_message: row.known_message,
1407
+ needs_review: !!row.needs_review
1408
+ }));
1409
+ }
1410
+ function appendIncidentToFailureClass(db, className, incidentId) {
1411
+ const row = db.prepare("SELECT incidents FROM failure_classes WHERE name = ?").get(className);
1412
+ if (!row) return;
1413
+ const incidents = JSON.parse(row.incidents || "[]");
1414
+ if (!incidents.includes(incidentId)) {
1415
+ incidents.push(incidentId);
1416
+ db.prepare("UPDATE failure_classes SET incidents = ?, updated_at = datetime('now') WHERE name = ?").run(JSON.stringify(incidents), className);
1417
+ }
1418
+ }
1419
+ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
1420
+ const classes = getFailureClasses(db);
1421
+ if (classes.length === 0) return null;
1422
+ const diffWeight = weights?.diffPatternWeight ?? 3;
1423
+ const fileWeight = weights?.filePatternWeight ?? 2;
1424
+ const promptWeight = weights?.promptKeywordWeight ?? 2;
1425
+ let bestMatch = null;
1426
+ for (const fc of classes) {
1427
+ let score = 0;
1428
+ for (const pattern of fc.diff_patterns) {
1429
+ if (!pattern) continue;
1430
+ try {
1431
+ if (new RegExp(pattern, "i").test(matchText)) {
1432
+ score += diffWeight;
1433
+ }
1434
+ } catch {
1435
+ if (matchText.toLowerCase().includes(pattern.toLowerCase())) {
1436
+ score += diffWeight;
1437
+ }
1438
+ }
1439
+ }
1440
+ for (const pattern of fc.file_patterns) {
1441
+ if (!pattern) continue;
1442
+ try {
1443
+ if (new RegExp(pattern).test(filePath)) {
1444
+ score += fileWeight;
1445
+ }
1446
+ } catch {
1447
+ if (filePath.includes(pattern)) {
1448
+ score += fileWeight;
1449
+ }
1450
+ }
1451
+ }
1452
+ if (promptContext) {
1453
+ for (const keyword of fc.prompt_keywords) {
1454
+ if (!keyword) continue;
1455
+ try {
1456
+ if (new RegExp(keyword, "i").test(promptContext)) {
1457
+ score += promptWeight;
1458
+ }
1459
+ } catch {
1460
+ if (promptContext.toLowerCase().includes(keyword.toLowerCase())) {
1461
+ score += promptWeight;
1462
+ }
1463
+ }
1464
+ }
1465
+ }
1466
+ if (!bestMatch || score > bestMatch.score) {
1467
+ bestMatch = {
1468
+ name: fc.name,
1469
+ score,
1470
+ incidentCount: fc.incidents.length,
1471
+ rules: fc.rules,
1472
+ knownMessage: fc.known_message
1473
+ };
1474
+ }
1475
+ }
1476
+ return bestMatch;
1477
+ }
1341
1478
  var init_memory_db = __esm({
1342
1479
  "src/memory-db.ts"() {
1343
1480
  "use strict";
@@ -1835,14 +1972,24 @@ function buildHooksConfig(hooksDir) {
1835
1972
  {
1836
1973
  matcher: "Edit|Write",
1837
1974
  hooks: [
1838
- { type: "command", command: hookCmd(hooksDir, "post-edit-context.js"), timeout: 5 }
1975
+ { type: "command", command: hookCmd(hooksDir, "post-edit-context.js"), timeout: 5 },
1976
+ { type: "command", command: hookCmd(hooksDir, "fix-detector.js"), timeout: 5 },
1977
+ { type: "command", command: hookCmd(hooksDir, "classify-failure.js"), timeout: 5 }
1978
+ ]
1979
+ },
1980
+ {
1981
+ matcher: "Write",
1982
+ hooks: [
1983
+ { type: "command", command: hookCmd(hooksDir, "incident-pipeline.js"), timeout: 5 },
1984
+ { type: "command", command: hookCmd(hooksDir, "rule-enforcement-pipeline.js"), timeout: 5 }
1839
1985
  ]
1840
1986
  }
1841
1987
  ],
1842
1988
  Stop: [
1843
1989
  {
1844
1990
  hooks: [
1845
- { type: "command", command: hookCmd(hooksDir, "session-end.js"), timeout: 15 }
1991
+ { type: "command", command: hookCmd(hooksDir, "session-end.js"), timeout: 15 },
1992
+ { type: "command", command: hookCmd(hooksDir, "auto-learning-pipeline.js"), timeout: 10 }
1846
1993
  ]
1847
1994
  }
1848
1995
  ],
@@ -2222,7 +2369,7 @@ var init_license = __esm({
2222
2369
  enterprise: 3
2223
2370
  };
2224
2371
  TOOL_TIER_MAP = {
2225
- // --- Free tier (12 tools: core navigation + basic memory + regression + license) ---
2372
+ // --- Free tier (13 tools: core navigation + basic memory + regression + license) ---
2226
2373
  sync: "free",
2227
2374
  context: "free",
2228
2375
  impact: "free",
@@ -2232,6 +2379,7 @@ var init_license = __esm({
2232
2379
  coupling_check: "free",
2233
2380
  memory_search: "free",
2234
2381
  memory_ingest: "free",
2382
+ memory_backfill: "free",
2235
2383
  regression_risk: "free",
2236
2384
  feature_health: "free",
2237
2385
  license_status: "free",
@@ -157,6 +157,18 @@ var AutoLearningConfigSchema = z.object({
157
157
  "added_missing_import"
158
158
  ])
159
159
  }).default({}),
160
+ failureClassification: z.object({
161
+ enabled: z.boolean().default(true),
162
+ thresholds: z.object({
163
+ known: z.number().default(5),
164
+ similar: z.number().default(3)
165
+ }).default({}),
166
+ scoring: z.object({
167
+ diffPatternWeight: z.number().default(3),
168
+ filePatternWeight: z.number().default(2),
169
+ promptKeywordWeight: z.number().default(2)
170
+ }).default({})
171
+ }).default({}),
160
172
  pipeline: z.object({
161
173
  requireIncidentReport: z.boolean().default(true),
162
174
  requirePreventionRule: z.boolean().default(true),
@@ -362,8 +374,8 @@ async function main() {
362
374
  const diff = execSync("git diff --name-only", { cwd: root, timeout: 3e3, encoding: "utf-8" });
363
375
  if (diff.trim()) {
364
376
  const fullDiff = execSync("git diff", { cwd: root, timeout: 5e3, encoding: "utf-8" });
365
- const fixPatterns = (fullDiff.match(/^\+.*(try|except|catch|guard|@MainActor|asyncio\.timeout|X-Service-Token|\.save\(|return False)/gm) || []).length;
366
- const removedBroken = (fullDiff.match(/^-.*(bug|broken|crash|\.store\(|= nil|error)/gm) || []).length;
377
+ const fixPatterns = (fullDiff.match(/^\+.*(try|except|catch|guard|throw|raise|assert|validate|if.*null|if.*nil|if.*None|if.*undefined)/gm) || []).length;
378
+ const removedBroken = (fullDiff.match(/^-.*(bug|broken|crash|wrong|incorrect|typo|fail|error|miss|stale)/gm) || []).length;
367
379
  if (fixPatterns > 3 || removedBroken > 1) {
368
380
  uncommittedFix = true;
369
381
  }