@massu/core 0.6.3 → 0.7.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.
@@ -0,0 +1,462 @@
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
+ pipeline: z.object({
161
+ requireIncidentReport: z.boolean().default(true),
162
+ requirePreventionRule: z.boolean().default(true),
163
+ requireEnforcement: z.boolean().default(true)
164
+ }).default({})
165
+ }).optional();
166
+ var CloudConfigSchema = z.object({
167
+ enabled: z.boolean().default(false),
168
+ apiKey: z.string().optional(),
169
+ endpoint: z.string().optional(),
170
+ sync: z.object({
171
+ memory: z.boolean().default(true),
172
+ analytics: z.boolean().default(true),
173
+ audit: z.boolean().default(true)
174
+ }).default({ memory: true, analytics: true, audit: true })
175
+ }).optional();
176
+ var ConventionsConfigSchema = z.object({
177
+ claudeDirName: z.string().default(".claude").refine(
178
+ (s) => !s.includes("..") && !s.startsWith("/"),
179
+ { message: 'claudeDirName must not contain ".." or start with "/"' }
180
+ ),
181
+ sessionStatePath: z.string().default(".claude/session-state/CURRENT.md").refine(
182
+ (s) => !s.includes("..") && !s.startsWith("/"),
183
+ { message: 'sessionStatePath must not contain ".." or start with "/"' }
184
+ ),
185
+ sessionArchivePath: z.string().default(".claude/session-state/archive").refine(
186
+ (s) => !s.includes("..") && !s.startsWith("/"),
187
+ { message: 'sessionArchivePath must not contain ".." or start with "/"' }
188
+ ),
189
+ knowledgeCategories: z.array(z.string()).default([
190
+ "patterns",
191
+ "commands",
192
+ "incidents",
193
+ "reference",
194
+ "protocols",
195
+ "checklists",
196
+ "playbooks",
197
+ "critical",
198
+ "scripts",
199
+ "status",
200
+ "templates",
201
+ "loop-state",
202
+ "session-state",
203
+ "agents"
204
+ ]),
205
+ knowledgeSourceFiles: z.array(z.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
206
+ excludePatterns: z.array(z.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
207
+ }).optional();
208
+ var PythonDomainConfigSchema = z.object({
209
+ name: z.string(),
210
+ packages: z.array(z.string()),
211
+ allowed_imports_from: z.array(z.string()).default([])
212
+ });
213
+ var PythonConfigSchema = z.object({
214
+ root: z.string(),
215
+ alembic_dir: z.string().optional(),
216
+ domains: z.array(PythonDomainConfigSchema).default([]),
217
+ exclude_dirs: z.array(z.string()).default(["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"])
218
+ }).optional();
219
+ var PathsConfigSchema = z.object({
220
+ source: z.string().default("src"),
221
+ aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
222
+ routers: z.string().optional(),
223
+ routerRoot: z.string().optional(),
224
+ pages: z.string().optional(),
225
+ middleware: z.string().optional(),
226
+ schema: z.string().optional(),
227
+ components: z.string().optional(),
228
+ hooks: z.string().optional()
229
+ });
230
+ var RawConfigSchema = z.object({
231
+ project: z.object({
232
+ name: z.string().default("my-project"),
233
+ root: z.string().default("auto")
234
+ }).default({ name: "my-project", root: "auto" }),
235
+ framework: z.object({
236
+ type: z.string().default("typescript"),
237
+ router: z.string().default("none"),
238
+ orm: z.string().default("none"),
239
+ ui: z.string().default("none")
240
+ }).default({ type: "typescript", router: "none", orm: "none", ui: "none" }),
241
+ paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
242
+ toolPrefix: z.string().default("massu"),
243
+ dbAccessPattern: z.string().optional(),
244
+ knownMismatches: z.record(z.string(), z.record(z.string(), z.string())).optional(),
245
+ accessScopes: z.array(z.string()).optional(),
246
+ domains: z.array(DomainConfigSchema).default([]),
247
+ rules: z.array(PatternRuleConfigSchema).default([]),
248
+ analytics: AnalyticsConfigSchema,
249
+ governance: GovernanceConfigSchema,
250
+ security: SecurityConfigSchema,
251
+ team: TeamConfigSchema,
252
+ regression: RegressionConfigSchema,
253
+ cloud: CloudConfigSchema,
254
+ conventions: ConventionsConfigSchema,
255
+ python: PythonConfigSchema,
256
+ autoLearning: AutoLearningConfigSchema
257
+ }).passthrough();
258
+ var _config = null;
259
+ var _projectRoot = null;
260
+ function findProjectRoot() {
261
+ const cwd = process.cwd();
262
+ let dir = cwd;
263
+ while (true) {
264
+ if (existsSync(resolve(dir, "massu.config.yaml"))) {
265
+ return dir;
266
+ }
267
+ const parent = dirname(dir);
268
+ if (parent === dir) break;
269
+ dir = parent;
270
+ }
271
+ dir = cwd;
272
+ while (true) {
273
+ if (existsSync(resolve(dir, "package.json"))) {
274
+ return dir;
275
+ }
276
+ if (existsSync(resolve(dir, ".git"))) {
277
+ return dir;
278
+ }
279
+ const parent = dirname(dir);
280
+ if (parent === dir) break;
281
+ dir = parent;
282
+ }
283
+ return cwd;
284
+ }
285
+ function getProjectRoot() {
286
+ if (!_projectRoot) {
287
+ _projectRoot = findProjectRoot();
288
+ }
289
+ return _projectRoot;
290
+ }
291
+ function getConfig() {
292
+ if (_config) return _config;
293
+ const root = getProjectRoot();
294
+ const configPath = resolve(root, "massu.config.yaml");
295
+ let rawYaml = {};
296
+ if (existsSync(configPath)) {
297
+ const content = readFileSync(configPath, "utf-8");
298
+ rawYaml = parseYaml(content) ?? {};
299
+ }
300
+ const parsed = RawConfigSchema.parse(rawYaml);
301
+ const projectRoot = parsed.project.root === "auto" || !parsed.project.root ? root : resolve(root, parsed.project.root);
302
+ _config = {
303
+ project: {
304
+ name: parsed.project.name,
305
+ root: projectRoot
306
+ },
307
+ framework: parsed.framework,
308
+ paths: parsed.paths,
309
+ toolPrefix: parsed.toolPrefix,
310
+ dbAccessPattern: parsed.dbAccessPattern,
311
+ knownMismatches: parsed.knownMismatches,
312
+ accessScopes: parsed.accessScopes,
313
+ domains: parsed.domains,
314
+ rules: parsed.rules,
315
+ analytics: parsed.analytics,
316
+ governance: parsed.governance,
317
+ security: parsed.security,
318
+ team: parsed.team,
319
+ regression: parsed.regression,
320
+ cloud: parsed.cloud,
321
+ conventions: parsed.conventions,
322
+ python: parsed.python
323
+ };
324
+ if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
325
+ _config.cloud = {
326
+ enabled: true,
327
+ sync: { memory: true, analytics: true, audit: true },
328
+ ..._config.cloud,
329
+ apiKey: process.env.MASSU_API_KEY
330
+ };
331
+ }
332
+ return _config;
333
+ }
334
+
335
+ // src/hooks/fix-detector.ts
336
+ var FIX_HEURISTICS = [
337
+ {
338
+ name: "removed_broken_code",
339
+ test: (diff) => /^-.*\b(bug|broken|wrong|incorrect|typo|crash|error|fail|miss|stale)\b/m.test(diff)
340
+ },
341
+ {
342
+ name: "added_error_handling",
343
+ test: (diff) => {
344
+ const added = (diff.match(/^\+.*(try|except|catch|guard|if.*nil|if.*None|validate|assert|raise|throw)/gm) || []).length;
345
+ return added > 2;
346
+ }
347
+ },
348
+ {
349
+ name: "method_name_correction",
350
+ test: (diff) => {
351
+ const removed = diff.match(/^-.*\.([a-z_]+)\(/m);
352
+ const added = diff.match(/^\+.*\.([a-z_]+)\(/m);
353
+ return !!(removed && added && removed[1] !== added[1]);
354
+ }
355
+ },
356
+ {
357
+ name: "auth_fix",
358
+ test: (diff) => /^\+.*(token|auth|header|X-Service|Bearer|credential)/im.test(diff)
359
+ },
360
+ {
361
+ name: "nil_handling_fix",
362
+ test: (diff) => /^\+.*(= nil|= None|\.isNil|is None|!= nil|is not None|guard let|if let|optional)/m.test(diff) && /^-/m.test(diff)
363
+ },
364
+ {
365
+ name: "concurrency_fix",
366
+ test: (diff) => /^\+.*(timeout|semaphore|lock|mutex|throttle|rate.limit|max_conn)/im.test(diff)
367
+ },
368
+ {
369
+ name: "async_pattern_fix",
370
+ test: (diff) => /^\+.*(@MainActor|async with|asyncio\.timeout|\.await)/.test(diff) && /^-/m.test(diff)
371
+ },
372
+ {
373
+ name: "added_missing_import",
374
+ test: (diff) => /^\+.*(import|from.*import|require)/.test(diff) && !/^-.*(import|from.*import|require)/m.test(diff)
375
+ }
376
+ ];
377
+ function getSessionFlagPath(sessionId) {
378
+ const dir = join(tmpdir(), "massu-auto-learning");
379
+ if (!existsSync2(dir)) {
380
+ mkdirSync(dir, { recursive: true });
381
+ }
382
+ return join(dir, `fixes-${sessionId.slice(0, 12)}.jsonl`);
383
+ }
384
+ async function main() {
385
+ try {
386
+ const input = await readStdin();
387
+ const hookInput = JSON.parse(input);
388
+ const filePath = hookInput.tool_input?.file_path;
389
+ if (!filePath || !existsSync2(filePath)) {
390
+ process.exit(0);
391
+ return;
392
+ }
393
+ if (!/\.(py|swift|ts|tsx|js|jsx|rs|go|rb|sh)$/.test(filePath)) {
394
+ process.exit(0);
395
+ return;
396
+ }
397
+ const config = getConfig();
398
+ const incidentDir = config.autoLearning?.incidentDir ?? "docs/incidents";
399
+ const memoryDir = config.autoLearning?.memoryDir ?? "memory";
400
+ if (filePath.includes(incidentDir) || filePath.includes(memoryDir) || filePath.includes("MEMORY.md")) {
401
+ process.exit(0);
402
+ return;
403
+ }
404
+ if (config.autoLearning?.enabled === false || config.autoLearning?.fixDetection?.enabled === false) {
405
+ process.exit(0);
406
+ return;
407
+ }
408
+ const root = getProjectRoot();
409
+ let diff = "";
410
+ try {
411
+ diff = execSync(`git diff -- "${filePath}"`, { cwd: root, timeout: 3e3, encoding: "utf-8" });
412
+ if (!diff) {
413
+ diff = execSync(`git diff HEAD -- "${filePath}"`, { cwd: root, timeout: 3e3, encoding: "utf-8" });
414
+ }
415
+ } catch {
416
+ process.exit(0);
417
+ return;
418
+ }
419
+ if (!diff) {
420
+ process.exit(0);
421
+ return;
422
+ }
423
+ const enabledSignals = new Set(config.autoLearning?.fixDetection?.signals ?? FIX_HEURISTICS.map((h) => h.name));
424
+ const detected = [];
425
+ for (const heuristic of FIX_HEURISTICS) {
426
+ if (enabledSignals.has(heuristic.name) && heuristic.test(diff)) {
427
+ detected.push(heuristic.name);
428
+ }
429
+ }
430
+ if (detected.length === 0) {
431
+ process.exit(0);
432
+ return;
433
+ }
434
+ const signal = {
435
+ file: filePath,
436
+ signals: detected,
437
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
438
+ };
439
+ const flagPath = getSessionFlagPath(hookInput.session_id);
440
+ appendFileSync(flagPath, JSON.stringify(signal) + "\n");
441
+ const lines = __require("fs").readFileSync(flagPath, "utf-8").split("\n").filter(Boolean);
442
+ if (lines.length === 1) {
443
+ console.log(
444
+ `[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.`
445
+ );
446
+ }
447
+ } catch {
448
+ }
449
+ process.exit(0);
450
+ }
451
+ function readStdin() {
452
+ return new Promise((resolve2) => {
453
+ let data = "";
454
+ process.stdin.setEncoding("utf-8");
455
+ process.stdin.on("data", (chunk) => {
456
+ data += chunk;
457
+ });
458
+ process.stdin.on("end", () => resolve2(data));
459
+ setTimeout(() => resolve2(data), 3e3);
460
+ });
461
+ }
462
+ main();