@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.
@@ -0,0 +1,1146 @@
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/classify-failure.ts
11
+ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync, unlinkSync } from "fs";
12
+ import { tmpdir } from "os";
13
+ import { join, basename as basename2 } from "path";
14
+
15
+ // src/config.ts
16
+ import { resolve, dirname } from "path";
17
+ import { existsSync, readFileSync } from "fs";
18
+ import { homedir } from "os";
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
+ function getResolvedPaths() {
347
+ const config = getConfig();
348
+ const root = getProjectRoot();
349
+ const claudeDirName = config.conventions?.claudeDirName ?? ".claude";
350
+ return {
351
+ codegraphDbPath: resolve(root, ".codegraph/codegraph.db"),
352
+ dataDbPath: resolve(root, ".massu/data.db"),
353
+ prismaSchemaPath: resolve(root, config.paths.schema ?? "prisma/schema.prisma"),
354
+ rootRouterPath: resolve(root, config.paths.routerRoot ?? "src/server/api/root.ts"),
355
+ routersDir: resolve(root, config.paths.routers ?? "src/server/api/routers"),
356
+ srcDir: resolve(root, config.paths.source),
357
+ pathAlias: Object.fromEntries(
358
+ Object.entries(config.paths.aliases).map(([alias, target]) => [
359
+ alias,
360
+ resolve(root, target)
361
+ ])
362
+ ),
363
+ extensions: [".ts", ".tsx", ".js", ".jsx"],
364
+ indexFiles: ["index.ts", "index.tsx", "index.js", "index.jsx"],
365
+ patternsDir: resolve(root, claudeDirName, "patterns"),
366
+ claudeMdPath: resolve(root, claudeDirName, "CLAUDE.md"),
367
+ docsMapPath: resolve(root, ".massu/docs-map.json"),
368
+ helpSitePath: resolve(root, "../" + config.project.name + "-help"),
369
+ memoryDbPath: resolve(root, ".massu/memory.db"),
370
+ knowledgeDbPath: resolve(root, ".massu/knowledge.db"),
371
+ plansDir: resolve(root, "docs/plans"),
372
+ docsDir: resolve(root, "docs"),
373
+ claudeDir: resolve(root, claudeDirName),
374
+ memoryDir: resolve(homedir(), claudeDirName, "projects", root.replace(/\//g, "-"), "memory"),
375
+ sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
376
+ sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
377
+ mcpJsonPath: resolve(root, ".mcp.json"),
378
+ settingsLocalPath: resolve(root, claudeDirName, "settings.local.json")
379
+ };
380
+ }
381
+
382
+ // src/memory-db.ts
383
+ import Database from "better-sqlite3";
384
+ import { dirname as dirname2, basename } from "path";
385
+ import { existsSync as existsSync2, mkdirSync } from "fs";
386
+ function getMemoryDb() {
387
+ const dbPath = getResolvedPaths().memoryDbPath;
388
+ const dir = dirname2(dbPath);
389
+ if (!existsSync2(dir)) {
390
+ mkdirSync(dir, { recursive: true });
391
+ }
392
+ const db = new Database(dbPath);
393
+ db.pragma("journal_mode = WAL");
394
+ db.pragma("foreign_keys = ON");
395
+ initMemorySchema(db);
396
+ return db;
397
+ }
398
+ function initMemorySchema(db) {
399
+ db.exec(`
400
+ -- Sessions table (linked to Claude Code session IDs)
401
+ CREATE TABLE IF NOT EXISTS sessions (
402
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
403
+ session_id TEXT UNIQUE NOT NULL,
404
+ project TEXT NOT NULL DEFAULT 'my-project',
405
+ git_branch TEXT,
406
+ started_at TEXT NOT NULL,
407
+ started_at_epoch INTEGER NOT NULL,
408
+ ended_at TEXT,
409
+ ended_at_epoch INTEGER,
410
+ status TEXT CHECK(status IN ('active', 'completed', 'abandoned')) NOT NULL DEFAULT 'active',
411
+ plan_file TEXT,
412
+ plan_phase TEXT,
413
+ task_id TEXT
414
+ );
415
+
416
+ CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id);
417
+ CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at_epoch DESC);
418
+ CREATE INDEX IF NOT EXISTS idx_sessions_task_id ON sessions(task_id);
419
+
420
+ -- Observations table (structured knowledge from tool usage)
421
+ CREATE TABLE IF NOT EXISTS observations (
422
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
423
+ session_id TEXT NOT NULL,
424
+ type TEXT NOT NULL CHECK(type IN (
425
+ 'decision', 'bugfix', 'feature', 'refactor', 'discovery',
426
+ 'cr_violation', 'vr_check', 'pattern_compliance', 'failed_attempt',
427
+ 'file_change', 'incident_near_miss'
428
+ )),
429
+ title TEXT NOT NULL,
430
+ detail TEXT,
431
+ files_involved TEXT DEFAULT '[]',
432
+ plan_item TEXT,
433
+ cr_rule TEXT,
434
+ vr_type TEXT,
435
+ evidence TEXT,
436
+ importance INTEGER NOT NULL DEFAULT 3 CHECK(importance BETWEEN 1 AND 5),
437
+ recurrence_count INTEGER NOT NULL DEFAULT 1,
438
+ original_tokens INTEGER DEFAULT 0,
439
+ created_at TEXT NOT NULL,
440
+ created_at_epoch INTEGER NOT NULL,
441
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
442
+ );
443
+
444
+ CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
445
+ CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
446
+ CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
447
+ CREATE INDEX IF NOT EXISTS idx_observations_plan_item ON observations(plan_item);
448
+ CREATE INDEX IF NOT EXISTS idx_observations_cr_rule ON observations(cr_rule);
449
+ CREATE INDEX IF NOT EXISTS idx_observations_importance ON observations(importance DESC);
450
+ `);
451
+ try {
452
+ db.exec(`
453
+ CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
454
+ title, detail, evidence,
455
+ content='observations',
456
+ content_rowid='id'
457
+ );
458
+ `);
459
+ } catch (_e) {
460
+ }
461
+ db.exec(`
462
+ CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
463
+ INSERT INTO observations_fts(rowid, title, detail, evidence)
464
+ VALUES (new.id, new.title, new.detail, new.evidence);
465
+ END;
466
+
467
+ CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
468
+ INSERT INTO observations_fts(observations_fts, rowid, title, detail, evidence)
469
+ VALUES ('delete', old.id, old.title, old.detail, old.evidence);
470
+ END;
471
+
472
+ CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
473
+ INSERT INTO observations_fts(observations_fts, rowid, title, detail, evidence)
474
+ VALUES ('delete', old.id, old.title, old.detail, old.evidence);
475
+ INSERT INTO observations_fts(rowid, title, detail, evidence)
476
+ VALUES (new.id, new.title, new.detail, new.evidence);
477
+ END;
478
+ `);
479
+ db.exec(`
480
+ CREATE TABLE IF NOT EXISTS session_summaries (
481
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
482
+ session_id TEXT NOT NULL,
483
+ request TEXT,
484
+ investigated TEXT,
485
+ decisions TEXT,
486
+ completed TEXT,
487
+ failed_attempts TEXT,
488
+ next_steps TEXT,
489
+ files_created TEXT DEFAULT '[]',
490
+ files_modified TEXT DEFAULT '[]',
491
+ verification_results TEXT DEFAULT '{}',
492
+ plan_progress TEXT DEFAULT '{}',
493
+ created_at TEXT NOT NULL,
494
+ created_at_epoch INTEGER NOT NULL,
495
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
496
+ );
497
+
498
+ CREATE INDEX IF NOT EXISTS idx_summaries_session ON session_summaries(session_id);
499
+ `);
500
+ db.exec(`
501
+ CREATE TABLE IF NOT EXISTS user_prompts (
502
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
503
+ session_id TEXT NOT NULL,
504
+ prompt_text TEXT NOT NULL,
505
+ prompt_number INTEGER NOT NULL DEFAULT 1,
506
+ created_at TEXT NOT NULL,
507
+ created_at_epoch INTEGER NOT NULL,
508
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
509
+ );
510
+ `);
511
+ try {
512
+ db.exec(`
513
+ CREATE VIRTUAL TABLE IF NOT EXISTS user_prompts_fts USING fts5(
514
+ prompt_text,
515
+ content='user_prompts',
516
+ content_rowid='id'
517
+ );
518
+ `);
519
+ } catch (_e) {
520
+ }
521
+ db.exec(`
522
+ CREATE TRIGGER IF NOT EXISTS prompts_ai AFTER INSERT ON user_prompts BEGIN
523
+ INSERT INTO user_prompts_fts(rowid, prompt_text) VALUES (new.id, new.prompt_text);
524
+ END;
525
+
526
+ CREATE TRIGGER IF NOT EXISTS prompts_ad AFTER DELETE ON user_prompts BEGIN
527
+ INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
528
+ VALUES ('delete', old.id, old.prompt_text);
529
+ END;
530
+ `);
531
+ db.exec(`
532
+ CREATE TABLE IF NOT EXISTS memory_meta (
533
+ key TEXT PRIMARY KEY,
534
+ value TEXT NOT NULL
535
+ );
536
+ `);
537
+ db.exec(`
538
+ CREATE TABLE IF NOT EXISTS conversation_turns (
539
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
540
+ session_id TEXT NOT NULL,
541
+ turn_number INTEGER NOT NULL,
542
+ user_prompt TEXT NOT NULL,
543
+ assistant_response TEXT,
544
+ tool_calls_json TEXT,
545
+ tool_call_count INTEGER DEFAULT 0,
546
+ model_used TEXT,
547
+ duration_ms INTEGER,
548
+ prompt_tokens INTEGER,
549
+ response_tokens INTEGER,
550
+ created_at TEXT DEFAULT (datetime('now')),
551
+ created_at_epoch INTEGER DEFAULT (unixepoch()),
552
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
553
+ );
554
+
555
+ CREATE INDEX IF NOT EXISTS idx_ct_session ON conversation_turns(session_id);
556
+ CREATE INDEX IF NOT EXISTS idx_ct_created ON conversation_turns(created_at DESC);
557
+ CREATE INDEX IF NOT EXISTS idx_ct_turn ON conversation_turns(session_id, turn_number);
558
+ `);
559
+ db.exec(`
560
+ CREATE TABLE IF NOT EXISTS tool_call_details (
561
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
562
+ session_id TEXT NOT NULL,
563
+ turn_number INTEGER NOT NULL,
564
+ tool_name TEXT NOT NULL,
565
+ tool_input_summary TEXT,
566
+ tool_input_size INTEGER,
567
+ tool_output_size INTEGER,
568
+ tool_success INTEGER DEFAULT 1,
569
+ duration_ms INTEGER,
570
+ files_involved TEXT,
571
+ created_at TEXT DEFAULT (datetime('now')),
572
+ created_at_epoch INTEGER DEFAULT (unixepoch()),
573
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
574
+ );
575
+
576
+ CREATE INDEX IF NOT EXISTS idx_tcd_session ON tool_call_details(session_id);
577
+ CREATE INDEX IF NOT EXISTS idx_tcd_tool ON tool_call_details(tool_name);
578
+ CREATE INDEX IF NOT EXISTS idx_tcd_created ON tool_call_details(created_at DESC);
579
+ `);
580
+ try {
581
+ db.exec(`
582
+ CREATE VIRTUAL TABLE IF NOT EXISTS conversation_turns_fts USING fts5(
583
+ user_prompt,
584
+ assistant_response,
585
+ content=conversation_turns,
586
+ content_rowid=id
587
+ );
588
+ `);
589
+ } catch (_e) {
590
+ }
591
+ db.exec(`
592
+ CREATE TRIGGER IF NOT EXISTS ct_fts_insert AFTER INSERT ON conversation_turns BEGIN
593
+ INSERT INTO conversation_turns_fts(rowid, user_prompt, assistant_response)
594
+ VALUES (new.id, new.user_prompt, new.assistant_response);
595
+ END;
596
+
597
+ CREATE TRIGGER IF NOT EXISTS ct_fts_delete AFTER DELETE ON conversation_turns BEGIN
598
+ INSERT INTO conversation_turns_fts(conversation_turns_fts, rowid, user_prompt, assistant_response)
599
+ VALUES ('delete', old.id, old.user_prompt, old.assistant_response);
600
+ END;
601
+
602
+ CREATE TRIGGER IF NOT EXISTS ct_fts_update AFTER UPDATE ON conversation_turns BEGIN
603
+ INSERT INTO conversation_turns_fts(conversation_turns_fts, rowid, user_prompt, assistant_response)
604
+ VALUES ('delete', old.id, old.user_prompt, old.assistant_response);
605
+ INSERT INTO conversation_turns_fts(rowid, user_prompt, assistant_response)
606
+ VALUES (new.id, new.user_prompt, new.assistant_response);
607
+ END;
608
+ `);
609
+ db.exec(`
610
+ CREATE TABLE IF NOT EXISTS session_quality_scores (
611
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
612
+ session_id TEXT NOT NULL UNIQUE,
613
+ project TEXT NOT NULL DEFAULT 'my-project',
614
+ score INTEGER NOT NULL DEFAULT 100,
615
+ security_score INTEGER NOT NULL DEFAULT 100,
616
+ architecture_score INTEGER NOT NULL DEFAULT 100,
617
+ coupling_score INTEGER NOT NULL DEFAULT 100,
618
+ test_score INTEGER NOT NULL DEFAULT 100,
619
+ rule_compliance_score INTEGER NOT NULL DEFAULT 100,
620
+ observations_total INTEGER NOT NULL DEFAULT 0,
621
+ bugs_found INTEGER NOT NULL DEFAULT 0,
622
+ bugs_fixed INTEGER NOT NULL DEFAULT 0,
623
+ vr_checks_passed INTEGER NOT NULL DEFAULT 0,
624
+ vr_checks_failed INTEGER NOT NULL DEFAULT 0,
625
+ incidents_triggered INTEGER NOT NULL DEFAULT 0,
626
+ created_at TEXT DEFAULT (datetime('now')),
627
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
628
+ );
629
+ CREATE INDEX IF NOT EXISTS idx_sqs_session ON session_quality_scores(session_id);
630
+ CREATE INDEX IF NOT EXISTS idx_sqs_project ON session_quality_scores(project);
631
+ `);
632
+ db.exec(`
633
+ CREATE TABLE IF NOT EXISTS session_costs (
634
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
635
+ session_id TEXT NOT NULL UNIQUE,
636
+ project TEXT NOT NULL DEFAULT 'my-project',
637
+ input_tokens INTEGER NOT NULL DEFAULT 0,
638
+ output_tokens INTEGER NOT NULL DEFAULT 0,
639
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
640
+ cache_write_tokens INTEGER NOT NULL DEFAULT 0,
641
+ total_tokens INTEGER NOT NULL DEFAULT 0,
642
+ estimated_cost_usd REAL NOT NULL DEFAULT 0.0,
643
+ model TEXT,
644
+ duration_minutes REAL NOT NULL DEFAULT 0.0,
645
+ tool_calls INTEGER NOT NULL DEFAULT 0,
646
+ created_at TEXT DEFAULT (datetime('now')),
647
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
648
+ );
649
+ CREATE INDEX IF NOT EXISTS idx_sc_session ON session_costs(session_id);
650
+ `);
651
+ db.exec(`
652
+ CREATE TABLE IF NOT EXISTS feature_costs (
653
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
654
+ feature_key TEXT NOT NULL,
655
+ session_id TEXT NOT NULL,
656
+ tokens_used INTEGER NOT NULL DEFAULT 0,
657
+ estimated_cost_usd REAL NOT NULL DEFAULT 0.0,
658
+ commit_hash TEXT,
659
+ created_at TEXT DEFAULT (datetime('now')),
660
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
661
+ );
662
+ CREATE INDEX IF NOT EXISTS idx_fc_feature ON feature_costs(feature_key);
663
+ CREATE INDEX IF NOT EXISTS idx_fc_session ON feature_costs(session_id);
664
+ `);
665
+ db.exec(`
666
+ CREATE TABLE IF NOT EXISTS prompt_outcomes (
667
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
668
+ session_id TEXT NOT NULL,
669
+ prompt_hash TEXT NOT NULL,
670
+ prompt_text TEXT NOT NULL,
671
+ prompt_category TEXT NOT NULL DEFAULT 'feature',
672
+ word_count INTEGER NOT NULL DEFAULT 0,
673
+ outcome TEXT NOT NULL DEFAULT 'success' CHECK(outcome IN ('success', 'partial', 'failure', 'abandoned')),
674
+ corrections_needed INTEGER NOT NULL DEFAULT 0,
675
+ follow_up_prompts INTEGER NOT NULL DEFAULT 0,
676
+ created_at TEXT DEFAULT (datetime('now')),
677
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
678
+ );
679
+ CREATE INDEX IF NOT EXISTS idx_po_session ON prompt_outcomes(session_id);
680
+ CREATE INDEX IF NOT EXISTS idx_po_category ON prompt_outcomes(prompt_category);
681
+ `);
682
+ db.exec(`
683
+ CREATE TABLE IF NOT EXISTS audit_log (
684
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
685
+ session_id TEXT NOT NULL,
686
+ timestamp TEXT DEFAULT (datetime('now')),
687
+ event_type TEXT NOT NULL CHECK(event_type IN ('code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction')),
688
+ actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
689
+ model_id TEXT,
690
+ file_path TEXT,
691
+ change_type TEXT CHECK(change_type IN ('create', 'edit', 'delete')),
692
+ rules_in_effect TEXT,
693
+ approval_status TEXT CHECK(approval_status IN ('auto_approved', 'human_approved', 'pending', 'denied')),
694
+ evidence TEXT,
695
+ metadata TEXT,
696
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
697
+ );
698
+ CREATE INDEX IF NOT EXISTS idx_al_session ON audit_log(session_id);
699
+ CREATE INDEX IF NOT EXISTS idx_al_file ON audit_log(file_path);
700
+ CREATE INDEX IF NOT EXISTS idx_al_event ON audit_log(event_type);
701
+ CREATE INDEX IF NOT EXISTS idx_al_timestamp ON audit_log(timestamp DESC);
702
+ `);
703
+ db.exec(`
704
+ CREATE TABLE IF NOT EXISTS validation_results (
705
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
706
+ session_id TEXT NOT NULL,
707
+ file_path TEXT NOT NULL,
708
+ validation_type TEXT NOT NULL,
709
+ passed INTEGER NOT NULL DEFAULT 1,
710
+ details TEXT,
711
+ rules_violated TEXT,
712
+ created_at TEXT DEFAULT (datetime('now')),
713
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
714
+ );
715
+ CREATE INDEX IF NOT EXISTS idx_vr_session ON validation_results(session_id);
716
+ CREATE INDEX IF NOT EXISTS idx_vr_file ON validation_results(file_path);
717
+ `);
718
+ db.exec(`
719
+ CREATE TABLE IF NOT EXISTS architecture_decisions (
720
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
721
+ session_id TEXT NOT NULL,
722
+ title TEXT NOT NULL,
723
+ context TEXT,
724
+ decision TEXT NOT NULL,
725
+ status TEXT NOT NULL DEFAULT 'accepted' CHECK(status IN ('accepted', 'superseded', 'deprecated')),
726
+ alternatives TEXT,
727
+ consequences TEXT,
728
+ affected_files TEXT,
729
+ commit_hash TEXT,
730
+ created_at TEXT DEFAULT (datetime('now')),
731
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
732
+ );
733
+ CREATE INDEX IF NOT EXISTS idx_ad_session ON architecture_decisions(session_id);
734
+ CREATE INDEX IF NOT EXISTS idx_ad_status ON architecture_decisions(status);
735
+ `);
736
+ db.exec(`
737
+ CREATE TABLE IF NOT EXISTS security_scores (
738
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
739
+ session_id TEXT NOT NULL,
740
+ file_path TEXT NOT NULL,
741
+ risk_score INTEGER NOT NULL DEFAULT 0,
742
+ findings TEXT,
743
+ created_at TEXT DEFAULT (datetime('now')),
744
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
745
+ );
746
+ CREATE INDEX IF NOT EXISTS idx_ss_session ON security_scores(session_id);
747
+ CREATE INDEX IF NOT EXISTS idx_ss_file ON security_scores(file_path);
748
+ `);
749
+ db.exec(`
750
+ CREATE TABLE IF NOT EXISTS dependency_assessments (
751
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
752
+ package_name TEXT NOT NULL,
753
+ version TEXT,
754
+ risk_score INTEGER NOT NULL DEFAULT 0,
755
+ vulnerabilities INTEGER NOT NULL DEFAULT 0,
756
+ last_publish_days INTEGER,
757
+ weekly_downloads INTEGER,
758
+ license TEXT,
759
+ bundle_size_kb INTEGER,
760
+ previous_removals INTEGER NOT NULL DEFAULT 0,
761
+ assessed_at TEXT DEFAULT (datetime('now'))
762
+ );
763
+ CREATE INDEX IF NOT EXISTS idx_da_package ON dependency_assessments(package_name);
764
+ `);
765
+ db.exec(`
766
+ CREATE TABLE IF NOT EXISTS developer_expertise (
767
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
768
+ developer_id TEXT NOT NULL,
769
+ module TEXT NOT NULL,
770
+ session_count INTEGER NOT NULL DEFAULT 0,
771
+ observation_count INTEGER NOT NULL DEFAULT 0,
772
+ expertise_score INTEGER NOT NULL DEFAULT 0,
773
+ last_active TEXT DEFAULT (datetime('now')),
774
+ UNIQUE(developer_id, module)
775
+ );
776
+ CREATE INDEX IF NOT EXISTS idx_de_developer ON developer_expertise(developer_id);
777
+ CREATE INDEX IF NOT EXISTS idx_de_module ON developer_expertise(module);
778
+ `);
779
+ db.exec(`
780
+ CREATE TABLE IF NOT EXISTS shared_observations (
781
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
782
+ original_id INTEGER,
783
+ developer_id TEXT NOT NULL,
784
+ project TEXT NOT NULL,
785
+ observation_type TEXT NOT NULL,
786
+ summary TEXT NOT NULL,
787
+ file_path TEXT,
788
+ module TEXT,
789
+ severity INTEGER NOT NULL DEFAULT 3,
790
+ is_shared INTEGER NOT NULL DEFAULT 0,
791
+ shared_at TEXT,
792
+ created_at TEXT DEFAULT (datetime('now'))
793
+ );
794
+ CREATE INDEX IF NOT EXISTS idx_so_developer ON shared_observations(developer_id);
795
+ CREATE INDEX IF NOT EXISTS idx_so_file ON shared_observations(file_path);
796
+ CREATE INDEX IF NOT EXISTS idx_so_module ON shared_observations(module);
797
+ `);
798
+ db.exec(`
799
+ CREATE TABLE IF NOT EXISTS knowledge_conflicts (
800
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
801
+ file_path TEXT NOT NULL,
802
+ developer_a TEXT NOT NULL,
803
+ developer_b TEXT NOT NULL,
804
+ conflict_type TEXT NOT NULL DEFAULT 'concurrent_edit',
805
+ resolved INTEGER NOT NULL DEFAULT 0,
806
+ detected_at TEXT DEFAULT (datetime('now'))
807
+ );
808
+ CREATE INDEX IF NOT EXISTS idx_kc_file ON knowledge_conflicts(file_path);
809
+ `);
810
+ db.exec(`
811
+ CREATE TABLE IF NOT EXISTS feature_health (
812
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
813
+ feature_key TEXT NOT NULL UNIQUE,
814
+ health_score INTEGER NOT NULL DEFAULT 100,
815
+ tests_passing INTEGER NOT NULL DEFAULT 0,
816
+ tests_failing INTEGER NOT NULL DEFAULT 0,
817
+ test_coverage_pct REAL,
818
+ modifications_since_test INTEGER NOT NULL DEFAULT 0,
819
+ last_modified TEXT,
820
+ last_tested TEXT,
821
+ created_at TEXT DEFAULT (datetime('now'))
822
+ );
823
+ CREATE INDEX IF NOT EXISTS idx_fh_feature ON feature_health(feature_key);
824
+ CREATE INDEX IF NOT EXISTS idx_fh_health ON feature_health(health_score);
825
+ `);
826
+ db.exec(`
827
+ CREATE TABLE IF NOT EXISTS tool_cost_events (
828
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
829
+ session_id TEXT NOT NULL,
830
+ tool_name TEXT NOT NULL,
831
+ estimated_input_tokens INTEGER DEFAULT 0,
832
+ estimated_output_tokens INTEGER DEFAULT 0,
833
+ model TEXT DEFAULT '',
834
+ created_at TEXT DEFAULT (datetime('now'))
835
+ );
836
+ CREATE INDEX IF NOT EXISTS idx_tce_session ON tool_cost_events(session_id);
837
+ CREATE INDEX IF NOT EXISTS idx_tce_tool ON tool_cost_events(tool_name);
838
+ CREATE INDEX IF NOT EXISTS idx_tce_created ON tool_cost_events(created_at DESC);
839
+ `);
840
+ db.exec(`
841
+ CREATE TABLE IF NOT EXISTS quality_events (
842
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
843
+ session_id TEXT NOT NULL,
844
+ event_type TEXT NOT NULL,
845
+ tool_name TEXT NOT NULL,
846
+ details TEXT DEFAULT '',
847
+ created_at TEXT DEFAULT (datetime('now'))
848
+ );
849
+ CREATE INDEX IF NOT EXISTS idx_qe_session ON quality_events(session_id);
850
+ CREATE INDEX IF NOT EXISTS idx_qe_event_type ON quality_events(event_type);
851
+ CREATE INDEX IF NOT EXISTS idx_qe_created ON quality_events(created_at DESC);
852
+ `);
853
+ db.exec(`
854
+ CREATE TABLE IF NOT EXISTS pending_sync (
855
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
856
+ payload TEXT NOT NULL,
857
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
858
+ retry_count INTEGER NOT NULL DEFAULT 0,
859
+ last_error TEXT
860
+ );
861
+ CREATE INDEX IF NOT EXISTS idx_pending_sync_created ON pending_sync(created_at ASC);
862
+ `);
863
+ db.exec(`
864
+ CREATE TABLE IF NOT EXISTS license_cache (
865
+ api_key_hash TEXT PRIMARY KEY,
866
+ tier TEXT NOT NULL,
867
+ valid_until TEXT NOT NULL,
868
+ last_validated TEXT NOT NULL,
869
+ features TEXT DEFAULT '[]'
870
+ );
871
+ `);
872
+ db.exec(`
873
+ CREATE TABLE IF NOT EXISTS failure_classes (
874
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
875
+ name TEXT NOT NULL UNIQUE,
876
+ description TEXT NOT NULL,
877
+ diff_patterns TEXT NOT NULL DEFAULT '[]',
878
+ file_patterns TEXT NOT NULL DEFAULT '[]',
879
+ prompt_keywords TEXT NOT NULL DEFAULT '[]',
880
+ incidents TEXT NOT NULL DEFAULT '[]',
881
+ rules TEXT NOT NULL DEFAULT '[]',
882
+ scanner_checks TEXT NOT NULL DEFAULT '[]',
883
+ known_message TEXT NOT NULL DEFAULT '',
884
+ needs_review INTEGER NOT NULL DEFAULT 0,
885
+ created_at TEXT DEFAULT (datetime('now')),
886
+ updated_at TEXT DEFAULT (datetime('now'))
887
+ );
888
+ CREATE INDEX IF NOT EXISTS idx_fc_name ON failure_classes(name);
889
+ CREATE INDEX IF NOT EXISTS idx_fc_needs_review ON failure_classes(needs_review);
890
+ `);
891
+ }
892
+ function getFailureClasses(db) {
893
+ const rows = db.prepare("SELECT * FROM failure_classes ORDER BY name").all();
894
+ return rows.map((row) => ({
895
+ id: row.id,
896
+ name: row.name,
897
+ description: row.description,
898
+ diff_patterns: JSON.parse(row.diff_patterns || "[]"),
899
+ file_patterns: JSON.parse(row.file_patterns || "[]"),
900
+ prompt_keywords: JSON.parse(row.prompt_keywords || "[]"),
901
+ incidents: JSON.parse(row.incidents || "[]"),
902
+ rules: JSON.parse(row.rules || "[]"),
903
+ scanner_checks: JSON.parse(row.scanner_checks || "[]"),
904
+ known_message: row.known_message,
905
+ needs_review: !!row.needs_review
906
+ }));
907
+ }
908
+ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
909
+ const classes = getFailureClasses(db);
910
+ if (classes.length === 0) return null;
911
+ const diffWeight = weights?.diffPatternWeight ?? 3;
912
+ const fileWeight = weights?.filePatternWeight ?? 2;
913
+ const promptWeight = weights?.promptKeywordWeight ?? 2;
914
+ let bestMatch = null;
915
+ for (const fc of classes) {
916
+ let score = 0;
917
+ for (const pattern of fc.diff_patterns) {
918
+ if (!pattern) continue;
919
+ try {
920
+ if (new RegExp(pattern, "i").test(matchText)) {
921
+ score += diffWeight;
922
+ }
923
+ } catch {
924
+ if (matchText.toLowerCase().includes(pattern.toLowerCase())) {
925
+ score += diffWeight;
926
+ }
927
+ }
928
+ }
929
+ for (const pattern of fc.file_patterns) {
930
+ if (!pattern) continue;
931
+ try {
932
+ if (new RegExp(pattern).test(filePath)) {
933
+ score += fileWeight;
934
+ }
935
+ } catch {
936
+ if (filePath.includes(pattern)) {
937
+ score += fileWeight;
938
+ }
939
+ }
940
+ }
941
+ if (promptContext) {
942
+ for (const keyword of fc.prompt_keywords) {
943
+ if (!keyword) continue;
944
+ try {
945
+ if (new RegExp(keyword, "i").test(promptContext)) {
946
+ score += promptWeight;
947
+ }
948
+ } catch {
949
+ if (promptContext.toLowerCase().includes(keyword.toLowerCase())) {
950
+ score += promptWeight;
951
+ }
952
+ }
953
+ }
954
+ }
955
+ if (!bestMatch || score > bestMatch.score) {
956
+ bestMatch = {
957
+ name: fc.name,
958
+ score,
959
+ incidentCount: fc.incidents.length,
960
+ rules: fc.rules,
961
+ knownMessage: fc.known_message
962
+ };
963
+ }
964
+ }
965
+ return bestMatch;
966
+ }
967
+
968
+ // src/hooks/classify-failure.ts
969
+ var BUG_FIX_INDICATORS = [
970
+ /\b(catch|error|throw|fail|fix|bug|broken|missing|crash|wrong|typo|incorrect)\b/i
971
+ ];
972
+ var CODE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|swift|rs|go|rb|sh)$/;
973
+ function getDedupeMarkerPath(sessionId, filePath) {
974
+ const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
975
+ const hash = simpleHash(filePath);
976
+ return join(tmpdir(), `massu-classify-${day}-${sessionId.slice(0, 8)}-${hash}`);
977
+ }
978
+ function simpleHash(str) {
979
+ let h = 0;
980
+ for (let i = 0; i < str.length; i++) {
981
+ h = (h << 5) - h + str.charCodeAt(i) | 0;
982
+ }
983
+ return Math.abs(h).toString(36);
984
+ }
985
+ function readFailureContextFiles() {
986
+ const dir = tmpdir();
987
+ let context = "";
988
+ try {
989
+ const files = readdirSync(dir).filter((f) => f.startsWith("massu-failure-context-"));
990
+ for (const file of files) {
991
+ try {
992
+ context += " " + readFileSync2(join(dir, file), "utf-8");
993
+ } catch {
994
+ }
995
+ }
996
+ } catch {
997
+ }
998
+ return context.trim();
999
+ }
1000
+ function cleanupFailureContextFiles() {
1001
+ const dir = tmpdir();
1002
+ try {
1003
+ const files = readdirSync(dir).filter((f) => f.startsWith("massu-failure-context-"));
1004
+ for (const file of files) {
1005
+ try {
1006
+ unlinkSync(join(dir, file));
1007
+ } catch {
1008
+ }
1009
+ }
1010
+ } catch {
1011
+ }
1012
+ }
1013
+ async function main() {
1014
+ try {
1015
+ const input = await readStdin();
1016
+ const hookInput = JSON.parse(input);
1017
+ const filePath = hookInput.tool_input?.file_path;
1018
+ if (!filePath) {
1019
+ process.exit(0);
1020
+ return;
1021
+ }
1022
+ const config = getConfig();
1023
+ if (config.autoLearning?.enabled === false || config.autoLearning?.failureClassification?.enabled === false) {
1024
+ process.exit(0);
1025
+ return;
1026
+ }
1027
+ if (!CODE_EXTENSIONS.test(filePath)) {
1028
+ process.exit(0);
1029
+ return;
1030
+ }
1031
+ const root = getProjectRoot();
1032
+ const incidentDir = config.autoLearning?.incidentDir ?? "docs/incidents";
1033
+ const memoryDir = config.autoLearning?.memoryDir ?? "memory";
1034
+ const relPath = filePath.startsWith(root + "/") ? filePath.slice(root.length + 1) : filePath;
1035
+ if (relPath.startsWith(incidentDir) || relPath.includes(memoryDir) || relPath.includes("MEMORY.md")) {
1036
+ process.exit(0);
1037
+ return;
1038
+ }
1039
+ const oldString = hookInput.tool_input?.old_string ?? "";
1040
+ const newString = hookInput.tool_input?.new_string ?? "";
1041
+ const content = hookInput.tool_input?.content ?? "";
1042
+ const matchText = `${oldString} ${newString} ${content}`;
1043
+ let isBugFix = false;
1044
+ const promptContext = readFailureContextFiles();
1045
+ if (promptContext) {
1046
+ isBugFix = true;
1047
+ }
1048
+ if (!isBugFix) {
1049
+ for (const pattern of BUG_FIX_INDICATORS) {
1050
+ if (pattern.test(matchText)) {
1051
+ isBugFix = true;
1052
+ break;
1053
+ }
1054
+ }
1055
+ }
1056
+ if (!isBugFix && oldString && newString) {
1057
+ const oldKeys = oldString.match(/[a-z_]+:/g)?.sort().join(",");
1058
+ const newKeys = newString.match(/[a-z_]+:/g)?.sort().join(",");
1059
+ if (oldKeys && newKeys && oldKeys !== newKeys) {
1060
+ isBugFix = true;
1061
+ }
1062
+ }
1063
+ if (!isBugFix) {
1064
+ process.exit(0);
1065
+ return;
1066
+ }
1067
+ const dedupeMarker = getDedupeMarkerPath(hookInput.session_id, filePath);
1068
+ if (existsSync3(dedupeMarker)) {
1069
+ process.exit(0);
1070
+ return;
1071
+ }
1072
+ try {
1073
+ __require("fs").writeFileSync(dedupeMarker, "1");
1074
+ } catch {
1075
+ }
1076
+ const db = getMemoryDb();
1077
+ try {
1078
+ const scoringConfig = config.autoLearning?.failureClassification?.scoring;
1079
+ const thresholds = config.autoLearning?.failureClassification?.thresholds ?? { known: 5, similar: 3 };
1080
+ const bestMatch = scoreFailureClasses(db, matchText, filePath, promptContext, scoringConfig);
1081
+ if (!bestMatch || bestMatch.score === 0) {
1082
+ outputNewPattern(basename2(filePath), bestMatch);
1083
+ } else if (bestMatch.score >= thresholds.known) {
1084
+ outputKnownPattern(bestMatch);
1085
+ } else if (bestMatch.score >= thresholds.similar) {
1086
+ outputSimilarPattern(bestMatch);
1087
+ } else {
1088
+ outputNewPattern(basename2(filePath), bestMatch);
1089
+ }
1090
+ } finally {
1091
+ db.close();
1092
+ }
1093
+ cleanupFailureContextFiles();
1094
+ } catch {
1095
+ }
1096
+ process.exit(0);
1097
+ }
1098
+ function outputKnownPattern(match) {
1099
+ const lines = [];
1100
+ lines.push("");
1101
+ lines.push(`[KNOWN PATTERN] ${match.name} (score: ${match.score}, ${match.incidentCount} prior incident(s))`);
1102
+ if (match.knownMessage) {
1103
+ lines.push(` ${match.knownMessage}`);
1104
+ }
1105
+ if (match.rules.length > 0) {
1106
+ lines.push(` Covered by: ${match.rules.join(", ")}`);
1107
+ }
1108
+ lines.push(" No new rules needed. Reference existing incident if logging.");
1109
+ console.log(lines.join("\n"));
1110
+ }
1111
+ function outputSimilarPattern(match) {
1112
+ const lines = [];
1113
+ lines.push("");
1114
+ lines.push(`[POSSIBLE MATCH] Resembles ${match.name} (score: ${match.score})`);
1115
+ if (match.rules.length > 0) {
1116
+ lines.push(` Check if existing rules cover this case: ${match.rules.join(", ")}`);
1117
+ }
1118
+ lines.push(" If genuinely new: create incident + prevention rule + enforcement.");
1119
+ console.log(lines.join("\n"));
1120
+ }
1121
+ function outputNewPattern(fileName, match) {
1122
+ const lines = [];
1123
+ lines.push("");
1124
+ if (match && match.score > 0) {
1125
+ lines.push(`[NEW PATTERN] No known failure class matches this fix in ${fileName} (best: ${match.name}, score: ${match.score}).`);
1126
+ } else {
1127
+ lines.push(`[NEW PATTERN] Bug fix detected in ${fileName} \u2014 no failure classes in taxonomy yet.`);
1128
+ }
1129
+ lines.push(" Full incident loop required:");
1130
+ lines.push(" 1. INCIDENT REPORT");
1131
+ lines.push(" 2. PREVENTION RULE (if new failure pattern)");
1132
+ lines.push(" 3. ENFORCEMENT (hook or static check)");
1133
+ console.log(lines.join("\n"));
1134
+ }
1135
+ function readStdin() {
1136
+ return new Promise((resolve3) => {
1137
+ let data = "";
1138
+ process.stdin.setEncoding("utf-8");
1139
+ process.stdin.on("data", (chunk) => {
1140
+ data += chunk;
1141
+ });
1142
+ process.stdin.on("end", () => resolve3(data));
1143
+ setTimeout(() => resolve3(data), 3e3);
1144
+ });
1145
+ }
1146
+ main();