@massu/core 0.6.3 → 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.
@@ -131,6 +131,44 @@ var RegressionConfigSchema = z.object({
131
131
  warning: z.number().default(50)
132
132
  }).optional()
133
133
  }).optional();
134
+ var AutoLearningConfigSchema = z.object({
135
+ enabled: z.boolean().default(true),
136
+ incidentDir: z.string().default("docs/incidents"),
137
+ memoryDir: z.string().default("memory"),
138
+ memoryIndexFile: z.string().default("MEMORY.md"),
139
+ enforcementHooksDir: z.string().default("scripts/hooks"),
140
+ fixDetection: z.object({
141
+ enabled: z.boolean().default(true),
142
+ lookbackDays: z.number().default(7),
143
+ signals: z.array(z.string()).default([
144
+ "removed_broken_code",
145
+ "added_error_handling",
146
+ "method_name_correction",
147
+ "auth_fix",
148
+ "nil_handling_fix",
149
+ "concurrency_fix",
150
+ "async_pattern_fix",
151
+ "added_missing_import"
152
+ ])
153
+ }).default({}),
154
+ failureClassification: z.object({
155
+ enabled: z.boolean().default(true),
156
+ thresholds: z.object({
157
+ known: z.number().default(5),
158
+ similar: z.number().default(3)
159
+ }).default({}),
160
+ scoring: z.object({
161
+ diffPatternWeight: z.number().default(3),
162
+ filePatternWeight: z.number().default(2),
163
+ promptKeywordWeight: z.number().default(2)
164
+ }).default({})
165
+ }).default({}),
166
+ pipeline: z.object({
167
+ requireIncidentReport: z.boolean().default(true),
168
+ requirePreventionRule: z.boolean().default(true),
169
+ requireEnforcement: z.boolean().default(true)
170
+ }).default({})
171
+ }).optional();
134
172
  var CloudConfigSchema = z.object({
135
173
  enabled: z.boolean().default(false),
136
174
  apiKey: z.string().optional(),
@@ -220,7 +258,8 @@ var RawConfigSchema = z.object({
220
258
  regression: RegressionConfigSchema,
221
259
  cloud: CloudConfigSchema,
222
260
  conventions: ConventionsConfigSchema,
223
- python: PythonConfigSchema
261
+ python: PythonConfigSchema,
262
+ autoLearning: AutoLearningConfigSchema
224
263
  }).passthrough();
225
264
  var _config = null;
226
265
  var _projectRoot = null;
@@ -821,6 +860,25 @@ function initMemorySchema(db) {
821
860
  features TEXT DEFAULT '[]'
822
861
  );
823
862
  `);
863
+ db.exec(`
864
+ CREATE TABLE IF NOT EXISTS failure_classes (
865
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
866
+ name TEXT NOT NULL UNIQUE,
867
+ description TEXT NOT NULL,
868
+ diff_patterns TEXT NOT NULL DEFAULT '[]',
869
+ file_patterns TEXT NOT NULL DEFAULT '[]',
870
+ prompt_keywords TEXT NOT NULL DEFAULT '[]',
871
+ incidents TEXT NOT NULL DEFAULT '[]',
872
+ rules TEXT NOT NULL DEFAULT '[]',
873
+ scanner_checks TEXT NOT NULL DEFAULT '[]',
874
+ known_message TEXT NOT NULL DEFAULT '',
875
+ needs_review INTEGER NOT NULL DEFAULT 0,
876
+ created_at TEXT DEFAULT (datetime('now')),
877
+ updated_at TEXT DEFAULT (datetime('now'))
878
+ );
879
+ CREATE INDEX IF NOT EXISTS idx_fc_name ON failure_classes(name);
880
+ CREATE INDEX IF NOT EXISTS idx_fc_needs_review ON failure_classes(needs_review);
881
+ `);
824
882
  }
825
883
 
826
884
  // src/hooks/cost-tracker.ts
@@ -0,0 +1,474 @@
1
+ #!/usr/bin/env node
2
+ import{createRequire as __cr}from"module";const require=__cr(import.meta.url);
3
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
4
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
5
+ }) : x)(function(x) {
6
+ if (typeof require !== "undefined") return require.apply(this, arguments);
7
+ throw Error('Dynamic require of "' + x + '" is not supported');
8
+ });
9
+
10
+ // src/hooks/fix-detector.ts
11
+ import { execSync } from "child_process";
12
+ import { existsSync as existsSync2, appendFileSync, mkdirSync } from "fs";
13
+ import { tmpdir } from "os";
14
+ import { join } from "path";
15
+
16
+ // src/config.ts
17
+ import { resolve, dirname } from "path";
18
+ import { existsSync, readFileSync } from "fs";
19
+ import { parse as parseYaml } from "yaml";
20
+ import { z } from "zod";
21
+ var DomainConfigSchema = z.object({
22
+ name: z.string().default("Unknown"),
23
+ routers: z.array(z.string()).default([]),
24
+ pages: z.array(z.string()).default([]),
25
+ tables: z.array(z.string()).default([]),
26
+ allowedImportsFrom: z.array(z.string()).default([])
27
+ });
28
+ var PatternRuleConfigSchema = z.object({
29
+ pattern: z.string().default("**"),
30
+ rules: z.array(z.string()).default([])
31
+ });
32
+ var CostModelSchema = z.object({
33
+ input_per_million: z.number(),
34
+ output_per_million: z.number(),
35
+ cache_read_per_million: z.number().optional(),
36
+ cache_write_per_million: z.number().optional()
37
+ });
38
+ var AnalyticsConfigSchema = z.object({
39
+ quality: z.object({
40
+ weights: z.record(z.string(), z.number()).default({
41
+ bug_found: -5,
42
+ vr_failure: -10,
43
+ incident: -20,
44
+ cr_violation: -3,
45
+ vr_pass: 2,
46
+ clean_commit: 5,
47
+ successful_verification: 3
48
+ }),
49
+ categories: z.array(z.string()).default(["security", "architecture", "coupling", "tests", "rule_compliance"])
50
+ }).optional(),
51
+ cost: z.object({
52
+ models: z.record(z.string(), CostModelSchema).default({}),
53
+ currency: z.string().default("USD")
54
+ }).optional(),
55
+ prompts: z.object({
56
+ success_indicators: z.array(z.string()).default(["committed", "approved", "looks good", "perfect", "great", "thanks"]),
57
+ failure_indicators: z.array(z.string()).default(["revert", "wrong", "that's not", "undo", "incorrect"]),
58
+ max_turns_for_success: z.number().default(2)
59
+ }).optional()
60
+ }).optional();
61
+ var CustomPatternSchema = z.object({
62
+ pattern: z.string(),
63
+ severity: z.string(),
64
+ message: z.string()
65
+ });
66
+ var GovernanceConfigSchema = z.object({
67
+ audit: z.object({
68
+ formats: z.array(z.string()).default(["summary", "detailed", "soc2"]),
69
+ retention_days: z.number().default(365),
70
+ auto_log: z.record(z.string(), z.boolean()).default({
71
+ code_changes: true,
72
+ rule_enforcement: true,
73
+ approvals: true,
74
+ commits: true
75
+ })
76
+ }).optional(),
77
+ validation: z.object({
78
+ realtime: z.boolean().default(true),
79
+ checks: z.record(z.string(), z.boolean()).default({
80
+ rule_compliance: true,
81
+ import_existence: true,
82
+ naming_conventions: true
83
+ }),
84
+ custom_patterns: z.array(CustomPatternSchema).default([])
85
+ }).optional(),
86
+ adr: z.object({
87
+ detection_phrases: z.array(z.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
88
+ template: z.string().default("default"),
89
+ storage: z.string().default("database"),
90
+ output_dir: z.string().default("docs/adr")
91
+ }).optional()
92
+ }).optional();
93
+ var SecurityPatternSchema = z.object({
94
+ pattern: z.string(),
95
+ severity: z.string(),
96
+ category: z.string(),
97
+ description: z.string()
98
+ });
99
+ var SecurityConfigSchema = z.object({
100
+ patterns: z.array(SecurityPatternSchema).default([]),
101
+ auto_score_on_edit: z.boolean().default(true),
102
+ score_threshold_alert: z.number().default(50),
103
+ severity_weights: z.record(z.string(), z.number()).optional(),
104
+ restrictive_licenses: z.array(z.string()).optional(),
105
+ dep_alternatives: z.record(z.string(), z.array(z.string())).optional(),
106
+ dependencies: z.object({
107
+ package_manager: z.string().default("npm"),
108
+ blocked_packages: z.array(z.string()).default([]),
109
+ preferred_packages: z.record(z.string(), z.string()).default({}),
110
+ max_bundle_size_kb: z.number().default(500)
111
+ }).optional()
112
+ }).optional();
113
+ var TeamConfigSchema = z.object({
114
+ enabled: z.boolean().default(false),
115
+ sync_backend: z.string().default("local"),
116
+ developer_id: z.string().default("auto"),
117
+ share_by_default: z.boolean().default(false),
118
+ expertise_weights: z.object({
119
+ session: z.number().default(20),
120
+ observation: z.number().default(10)
121
+ }).optional(),
122
+ privacy: z.object({
123
+ share_file_paths: z.boolean().default(true),
124
+ share_code_snippets: z.boolean().default(false),
125
+ share_observations: z.boolean().default(true)
126
+ }).optional()
127
+ }).optional();
128
+ var RegressionConfigSchema = z.object({
129
+ test_patterns: z.array(z.string()).default([
130
+ "{dir}/__tests__/{name}.test.{ext}",
131
+ "{dir}/{name}.spec.{ext}",
132
+ "tests/{path}.test.{ext}"
133
+ ]),
134
+ test_runner: z.string().default("npm test"),
135
+ health_thresholds: z.object({
136
+ healthy: z.number().default(80),
137
+ warning: z.number().default(50)
138
+ }).optional()
139
+ }).optional();
140
+ var AutoLearningConfigSchema = z.object({
141
+ enabled: z.boolean().default(true),
142
+ incidentDir: z.string().default("docs/incidents"),
143
+ memoryDir: z.string().default("memory"),
144
+ memoryIndexFile: z.string().default("MEMORY.md"),
145
+ enforcementHooksDir: z.string().default("scripts/hooks"),
146
+ fixDetection: z.object({
147
+ enabled: z.boolean().default(true),
148
+ lookbackDays: z.number().default(7),
149
+ signals: z.array(z.string()).default([
150
+ "removed_broken_code",
151
+ "added_error_handling",
152
+ "method_name_correction",
153
+ "auth_fix",
154
+ "nil_handling_fix",
155
+ "concurrency_fix",
156
+ "async_pattern_fix",
157
+ "added_missing_import"
158
+ ])
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({}),
172
+ pipeline: z.object({
173
+ requireIncidentReport: z.boolean().default(true),
174
+ requirePreventionRule: z.boolean().default(true),
175
+ requireEnforcement: z.boolean().default(true)
176
+ }).default({})
177
+ }).optional();
178
+ var CloudConfigSchema = z.object({
179
+ enabled: z.boolean().default(false),
180
+ apiKey: z.string().optional(),
181
+ endpoint: z.string().optional(),
182
+ sync: z.object({
183
+ memory: z.boolean().default(true),
184
+ analytics: z.boolean().default(true),
185
+ audit: z.boolean().default(true)
186
+ }).default({ memory: true, analytics: true, audit: true })
187
+ }).optional();
188
+ var ConventionsConfigSchema = z.object({
189
+ claudeDirName: z.string().default(".claude").refine(
190
+ (s) => !s.includes("..") && !s.startsWith("/"),
191
+ { message: 'claudeDirName must not contain ".." or start with "/"' }
192
+ ),
193
+ sessionStatePath: z.string().default(".claude/session-state/CURRENT.md").refine(
194
+ (s) => !s.includes("..") && !s.startsWith("/"),
195
+ { message: 'sessionStatePath must not contain ".." or start with "/"' }
196
+ ),
197
+ sessionArchivePath: z.string().default(".claude/session-state/archive").refine(
198
+ (s) => !s.includes("..") && !s.startsWith("/"),
199
+ { message: 'sessionArchivePath must not contain ".." or start with "/"' }
200
+ ),
201
+ knowledgeCategories: z.array(z.string()).default([
202
+ "patterns",
203
+ "commands",
204
+ "incidents",
205
+ "reference",
206
+ "protocols",
207
+ "checklists",
208
+ "playbooks",
209
+ "critical",
210
+ "scripts",
211
+ "status",
212
+ "templates",
213
+ "loop-state",
214
+ "session-state",
215
+ "agents"
216
+ ]),
217
+ knowledgeSourceFiles: z.array(z.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
218
+ excludePatterns: z.array(z.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
219
+ }).optional();
220
+ var PythonDomainConfigSchema = z.object({
221
+ name: z.string(),
222
+ packages: z.array(z.string()),
223
+ allowed_imports_from: z.array(z.string()).default([])
224
+ });
225
+ var PythonConfigSchema = z.object({
226
+ root: z.string(),
227
+ alembic_dir: z.string().optional(),
228
+ domains: z.array(PythonDomainConfigSchema).default([]),
229
+ exclude_dirs: z.array(z.string()).default(["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"])
230
+ }).optional();
231
+ var PathsConfigSchema = z.object({
232
+ source: z.string().default("src"),
233
+ aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
234
+ routers: z.string().optional(),
235
+ routerRoot: z.string().optional(),
236
+ pages: z.string().optional(),
237
+ middleware: z.string().optional(),
238
+ schema: z.string().optional(),
239
+ components: z.string().optional(),
240
+ hooks: z.string().optional()
241
+ });
242
+ var RawConfigSchema = z.object({
243
+ project: z.object({
244
+ name: z.string().default("my-project"),
245
+ root: z.string().default("auto")
246
+ }).default({ name: "my-project", root: "auto" }),
247
+ framework: z.object({
248
+ type: z.string().default("typescript"),
249
+ router: z.string().default("none"),
250
+ orm: z.string().default("none"),
251
+ ui: z.string().default("none")
252
+ }).default({ type: "typescript", router: "none", orm: "none", ui: "none" }),
253
+ paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
254
+ toolPrefix: z.string().default("massu"),
255
+ dbAccessPattern: z.string().optional(),
256
+ knownMismatches: z.record(z.string(), z.record(z.string(), z.string())).optional(),
257
+ accessScopes: z.array(z.string()).optional(),
258
+ domains: z.array(DomainConfigSchema).default([]),
259
+ rules: z.array(PatternRuleConfigSchema).default([]),
260
+ analytics: AnalyticsConfigSchema,
261
+ governance: GovernanceConfigSchema,
262
+ security: SecurityConfigSchema,
263
+ team: TeamConfigSchema,
264
+ regression: RegressionConfigSchema,
265
+ cloud: CloudConfigSchema,
266
+ conventions: ConventionsConfigSchema,
267
+ python: PythonConfigSchema,
268
+ autoLearning: AutoLearningConfigSchema
269
+ }).passthrough();
270
+ var _config = null;
271
+ var _projectRoot = null;
272
+ function findProjectRoot() {
273
+ const cwd = process.cwd();
274
+ let dir = cwd;
275
+ while (true) {
276
+ if (existsSync(resolve(dir, "massu.config.yaml"))) {
277
+ return dir;
278
+ }
279
+ const parent = dirname(dir);
280
+ if (parent === dir) break;
281
+ dir = parent;
282
+ }
283
+ dir = cwd;
284
+ while (true) {
285
+ if (existsSync(resolve(dir, "package.json"))) {
286
+ return dir;
287
+ }
288
+ if (existsSync(resolve(dir, ".git"))) {
289
+ return dir;
290
+ }
291
+ const parent = dirname(dir);
292
+ if (parent === dir) break;
293
+ dir = parent;
294
+ }
295
+ return cwd;
296
+ }
297
+ function getProjectRoot() {
298
+ if (!_projectRoot) {
299
+ _projectRoot = findProjectRoot();
300
+ }
301
+ return _projectRoot;
302
+ }
303
+ function getConfig() {
304
+ if (_config) return _config;
305
+ const root = getProjectRoot();
306
+ const configPath = resolve(root, "massu.config.yaml");
307
+ let rawYaml = {};
308
+ if (existsSync(configPath)) {
309
+ const content = readFileSync(configPath, "utf-8");
310
+ rawYaml = parseYaml(content) ?? {};
311
+ }
312
+ const parsed = RawConfigSchema.parse(rawYaml);
313
+ const projectRoot = parsed.project.root === "auto" || !parsed.project.root ? root : resolve(root, parsed.project.root);
314
+ _config = {
315
+ project: {
316
+ name: parsed.project.name,
317
+ root: projectRoot
318
+ },
319
+ framework: parsed.framework,
320
+ paths: parsed.paths,
321
+ toolPrefix: parsed.toolPrefix,
322
+ dbAccessPattern: parsed.dbAccessPattern,
323
+ knownMismatches: parsed.knownMismatches,
324
+ accessScopes: parsed.accessScopes,
325
+ domains: parsed.domains,
326
+ rules: parsed.rules,
327
+ analytics: parsed.analytics,
328
+ governance: parsed.governance,
329
+ security: parsed.security,
330
+ team: parsed.team,
331
+ regression: parsed.regression,
332
+ cloud: parsed.cloud,
333
+ conventions: parsed.conventions,
334
+ python: parsed.python
335
+ };
336
+ if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
337
+ _config.cloud = {
338
+ enabled: true,
339
+ sync: { memory: true, analytics: true, audit: true },
340
+ ..._config.cloud,
341
+ apiKey: process.env.MASSU_API_KEY
342
+ };
343
+ }
344
+ return _config;
345
+ }
346
+
347
+ // src/hooks/fix-detector.ts
348
+ var FIX_HEURISTICS = [
349
+ {
350
+ name: "removed_broken_code",
351
+ test: (diff) => /^-.*\b(bug|broken|wrong|incorrect|typo|crash|error|fail|miss|stale)\b/m.test(diff)
352
+ },
353
+ {
354
+ name: "added_error_handling",
355
+ test: (diff) => {
356
+ const added = (diff.match(/^\+.*(try|except|catch|guard|if.*nil|if.*None|validate|assert|raise|throw)/gm) || []).length;
357
+ return added > 2;
358
+ }
359
+ },
360
+ {
361
+ name: "method_name_correction",
362
+ test: (diff) => {
363
+ const removed = diff.match(/^-.*\.([a-z_]+)\(/m);
364
+ const added = diff.match(/^\+.*\.([a-z_]+)\(/m);
365
+ return !!(removed && added && removed[1] !== added[1]);
366
+ }
367
+ },
368
+ {
369
+ name: "auth_fix",
370
+ test: (diff) => /^\+.*(token|auth|header|X-Service|Bearer|credential)/im.test(diff)
371
+ },
372
+ {
373
+ name: "nil_handling_fix",
374
+ test: (diff) => /^\+.*(= nil|= None|\.isNil|is None|!= nil|is not None|guard let|if let|optional)/m.test(diff) && /^-/m.test(diff)
375
+ },
376
+ {
377
+ name: "concurrency_fix",
378
+ test: (diff) => /^\+.*(timeout|semaphore|lock|mutex|throttle|rate.limit|max_conn)/im.test(diff)
379
+ },
380
+ {
381
+ name: "async_pattern_fix",
382
+ test: (diff) => /^\+.*(@MainActor|async with|asyncio\.timeout|\.await)/.test(diff) && /^-/m.test(diff)
383
+ },
384
+ {
385
+ name: "added_missing_import",
386
+ test: (diff) => /^\+.*(import|from.*import|require)/.test(diff) && !/^-.*(import|from.*import|require)/m.test(diff)
387
+ }
388
+ ];
389
+ function getSessionFlagPath(sessionId) {
390
+ const dir = join(tmpdir(), "massu-auto-learning");
391
+ if (!existsSync2(dir)) {
392
+ mkdirSync(dir, { recursive: true });
393
+ }
394
+ return join(dir, `fixes-${sessionId.slice(0, 12)}.jsonl`);
395
+ }
396
+ async function main() {
397
+ try {
398
+ const input = await readStdin();
399
+ const hookInput = JSON.parse(input);
400
+ const filePath = hookInput.tool_input?.file_path;
401
+ if (!filePath || !existsSync2(filePath)) {
402
+ process.exit(0);
403
+ return;
404
+ }
405
+ if (!/\.(py|swift|ts|tsx|js|jsx|rs|go|rb|sh)$/.test(filePath)) {
406
+ process.exit(0);
407
+ return;
408
+ }
409
+ const config = getConfig();
410
+ const incidentDir = config.autoLearning?.incidentDir ?? "docs/incidents";
411
+ const memoryDir = config.autoLearning?.memoryDir ?? "memory";
412
+ if (filePath.includes(incidentDir) || filePath.includes(memoryDir) || filePath.includes("MEMORY.md")) {
413
+ process.exit(0);
414
+ return;
415
+ }
416
+ if (config.autoLearning?.enabled === false || config.autoLearning?.fixDetection?.enabled === false) {
417
+ process.exit(0);
418
+ return;
419
+ }
420
+ const root = getProjectRoot();
421
+ let diff = "";
422
+ try {
423
+ diff = execSync(`git diff -- "${filePath}"`, { cwd: root, timeout: 3e3, encoding: "utf-8" });
424
+ if (!diff) {
425
+ diff = execSync(`git diff HEAD -- "${filePath}"`, { cwd: root, timeout: 3e3, encoding: "utf-8" });
426
+ }
427
+ } catch {
428
+ process.exit(0);
429
+ return;
430
+ }
431
+ if (!diff) {
432
+ process.exit(0);
433
+ return;
434
+ }
435
+ const enabledSignals = new Set(config.autoLearning?.fixDetection?.signals ?? FIX_HEURISTICS.map((h) => h.name));
436
+ const detected = [];
437
+ for (const heuristic of FIX_HEURISTICS) {
438
+ if (enabledSignals.has(heuristic.name) && heuristic.test(diff)) {
439
+ detected.push(heuristic.name);
440
+ }
441
+ }
442
+ if (detected.length === 0) {
443
+ process.exit(0);
444
+ return;
445
+ }
446
+ const signal = {
447
+ file: filePath,
448
+ signals: detected,
449
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
450
+ };
451
+ const flagPath = getSessionFlagPath(hookInput.session_id);
452
+ appendFileSync(flagPath, JSON.stringify(signal) + "\n");
453
+ const lines = __require("fs").readFileSync(flagPath, "utf-8").split("\n").filter(Boolean);
454
+ if (lines.length === 1) {
455
+ console.log(
456
+ `[Massu Auto-Learning] Bug fix detected in ${filePath} (signals: ${detected.join(", ")}). The auto-learning pipeline will prompt you at session end to create an incident report, derive a prevention rule, and add enforcement.`
457
+ );
458
+ }
459
+ } catch {
460
+ }
461
+ process.exit(0);
462
+ }
463
+ function readStdin() {
464
+ return new Promise((resolve2) => {
465
+ let data = "";
466
+ process.stdin.setEncoding("utf-8");
467
+ process.stdin.on("data", (chunk) => {
468
+ data += chunk;
469
+ });
470
+ process.stdin.on("end", () => resolve2(data));
471
+ setTimeout(() => resolve2(data), 3e3);
472
+ });
473
+ }
474
+ main();