@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.
- package/README.md +2 -2
- package/dist/cli.js +180 -5
- package/dist/hooks/auto-learning-pipeline.js +481 -0
- package/dist/hooks/classify-failure.js +1146 -0
- package/dist/hooks/cost-tracker.js +59 -1
- package/dist/hooks/fix-detector.js +474 -0
- package/dist/hooks/incident-pipeline.js +1114 -0
- package/dist/hooks/post-edit-context.js +40 -1
- package/dist/hooks/post-tool-use.js +59 -1
- package/dist/hooks/pre-compact.js +59 -1
- package/dist/hooks/pre-delete-check.js +40 -1
- package/dist/hooks/quality-event.js +59 -1
- package/dist/hooks/rule-enforcement-pipeline.js +453 -0
- package/dist/hooks/session-end.js +59 -1
- package/dist/hooks/session-start.js +60 -2
- package/dist/hooks/user-prompt.js +91 -2
- package/package.json +2 -2
- package/reference/hook-execution-order.md +25 -17
- package/src/commands/doctor.ts +1 -1
- package/src/commands/init.ts +11 -1
- package/src/config.ts +43 -0
- package/src/hooks/auto-learning-pipeline.ts +195 -0
- package/src/hooks/classify-failure.ts +259 -0
- package/src/hooks/fix-detector.ts +186 -0
- package/src/hooks/incident-pipeline.ts +190 -0
- package/src/hooks/rule-enforcement-pipeline.ts +159 -0
- package/src/hooks/session-start.ts +1 -1
- package/src/hooks/user-prompt.ts +21 -1
- package/src/license.ts +2 -1
- package/src/mcp-bridge-tools.ts +1 -1
- package/src/memory-db.ts +201 -0
|
@@ -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();
|