@massu/core 0.1.2 → 0.4.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.
Files changed (84) hide show
  1. package/commands/_shared-preamble.md +76 -0
  2. package/commands/massu-audit-deps.md +211 -0
  3. package/commands/massu-changelog.md +174 -0
  4. package/commands/massu-cleanup.md +315 -0
  5. package/commands/massu-commit.md +481 -0
  6. package/commands/massu-create-plan.md +752 -0
  7. package/commands/massu-dead-code.md +131 -0
  8. package/commands/massu-debug.md +484 -0
  9. package/commands/massu-deploy.md +91 -0
  10. package/commands/massu-deps.md +374 -0
  11. package/commands/massu-doc-gen.md +279 -0
  12. package/commands/massu-docs.md +364 -0
  13. package/commands/massu-estimate.md +313 -0
  14. package/commands/massu-golden-path.md +973 -0
  15. package/commands/massu-guide.md +167 -0
  16. package/commands/massu-hotfix.md +480 -0
  17. package/commands/massu-loop-playwright.md +837 -0
  18. package/commands/massu-loop.md +775 -0
  19. package/commands/massu-new-feature.md +511 -0
  20. package/commands/massu-parity.md +214 -0
  21. package/commands/massu-plan.md +456 -0
  22. package/commands/massu-push-light.md +207 -0
  23. package/commands/massu-push.md +434 -0
  24. package/commands/massu-refactor.md +410 -0
  25. package/commands/massu-release.md +363 -0
  26. package/commands/massu-review.md +238 -0
  27. package/commands/massu-simplify.md +281 -0
  28. package/commands/massu-status.md +278 -0
  29. package/commands/massu-tdd.md +201 -0
  30. package/commands/massu-test.md +516 -0
  31. package/commands/massu-verify-playwright.md +281 -0
  32. package/commands/massu-verify.md +667 -0
  33. package/dist/cli.js +12522 -0
  34. package/dist/hooks/cost-tracker.js +80 -5
  35. package/dist/hooks/post-edit-context.js +72 -6
  36. package/dist/hooks/post-tool-use.js +234 -57
  37. package/dist/hooks/pre-compact.js +144 -5
  38. package/dist/hooks/pre-delete-check.js +141 -11
  39. package/dist/hooks/quality-event.js +80 -5
  40. package/dist/hooks/security-gate.js +29 -0
  41. package/dist/hooks/session-end.js +83 -8
  42. package/dist/hooks/session-start.js +153 -7
  43. package/dist/hooks/user-prompt.js +166 -5
  44. package/package.json +6 -5
  45. package/src/backfill-sessions.ts +5 -4
  46. package/src/cli.ts +6 -0
  47. package/src/commands/doctor.ts +193 -6
  48. package/src/commands/init.ts +235 -6
  49. package/src/commands/install-commands.ts +137 -0
  50. package/src/config.ts +68 -2
  51. package/src/db.ts +115 -2
  52. package/src/docs-tools.ts +8 -6
  53. package/src/hooks/post-edit-context.ts +1 -1
  54. package/src/hooks/post-tool-use.ts +130 -0
  55. package/src/hooks/pre-compact.ts +23 -1
  56. package/src/hooks/pre-delete-check.ts +92 -4
  57. package/src/hooks/security-gate.ts +32 -0
  58. package/src/hooks/session-start.ts +97 -4
  59. package/src/hooks/user-prompt.ts +46 -1
  60. package/src/import-resolver.ts +2 -1
  61. package/src/knowledge-db.ts +169 -0
  62. package/src/knowledge-indexer.ts +704 -0
  63. package/src/knowledge-tools.ts +1413 -0
  64. package/src/license.ts +482 -0
  65. package/src/memory-db.ts +14 -1
  66. package/src/observation-extractor.ts +11 -4
  67. package/src/page-deps.ts +3 -2
  68. package/src/python/coupling-detector.ts +124 -0
  69. package/src/python/domain-enforcer.ts +83 -0
  70. package/src/python/impact-analyzer.ts +95 -0
  71. package/src/python/import-parser.ts +244 -0
  72. package/src/python/import-resolver.ts +135 -0
  73. package/src/python/migration-indexer.ts +115 -0
  74. package/src/python/migration-parser.ts +332 -0
  75. package/src/python/model-indexer.ts +70 -0
  76. package/src/python/model-parser.ts +279 -0
  77. package/src/python/route-indexer.ts +58 -0
  78. package/src/python/route-parser.ts +317 -0
  79. package/src/python-tools.ts +629 -0
  80. package/src/sentinel-db.ts +2 -1
  81. package/src/server.ts +29 -6
  82. package/src/session-archiver.ts +4 -5
  83. package/src/tools.ts +283 -31
  84. package/README.md +0 -40
@@ -0,0 +1,1413 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ import Database from 'better-sqlite3';
5
+ import type { ToolDefinition, ToolResult } from './tools.ts';
6
+ import { indexIfStale } from './knowledge-indexer.ts';
7
+ import { readFileSync, writeFileSync, appendFileSync, existsSync, readdirSync } from 'fs';
8
+ import { resolve, basename } from 'path';
9
+ import { getConfig, getResolvedPaths } from './config.ts';
10
+ import { getDataDb } from './db.ts';
11
+ import { getMemoryDb, sanitizeFts5Query } from './memory-db.ts';
12
+
13
+ // ============================================================
14
+ // Massu Knowledge: MCP Tool Definitions & Handlers
15
+ // ============================================================
16
+
17
+ /** Prefix a base tool name with the configured tool prefix. */
18
+ function p(baseName: string): string {
19
+ return `${getConfig().toolPrefix}_${baseName}`;
20
+ }
21
+
22
+ /** Strip the configured prefix from a tool name to get the base name. */
23
+ function stripPrefix(name: string): string {
24
+ const pfx = getConfig().toolPrefix + '_';
25
+ if (name.startsWith(pfx)) {
26
+ return name.slice(pfx.length);
27
+ }
28
+ return name;
29
+ }
30
+
31
+ function text(content: string): ToolResult {
32
+ return { content: [{ type: 'text', text: content }] };
33
+ }
34
+
35
+ /**
36
+ * Ensure knowledge DB is indexed before queries (P4-002: lazy re-index).
37
+ * Non-fatal: returns false if indexing fails, tools should degrade gracefully.
38
+ */
39
+ function ensureKnowledgeIndexed(db: Database.Database): boolean {
40
+ try {
41
+ indexIfStale(db);
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ function hasData(db: Database.Database): boolean {
49
+ const row = db.prepare('SELECT COUNT(*) as cnt FROM knowledge_documents').get() as { cnt: number } | undefined;
50
+ return (row?.cnt ?? 0) > 0;
51
+ }
52
+
53
+ // ============================================================
54
+ // Tool Definitions (12 tools)
55
+ // ============================================================
56
+
57
+ export function getKnowledgeToolDefinitions(): ToolDefinition[] {
58
+ const claudeDir = getConfig().conventions?.claudeDirName ?? '.claude';
59
+ return [
60
+ // P2-001: knowledge_search
61
+ {
62
+ name: p('knowledge_search'),
63
+ description: `Full-text + structured search across all ${claudeDir}/ knowledge (rules, patterns, incidents, commands, protocols). Returns matched chunks with file path, heading, and content preview.`,
64
+ inputSchema: {
65
+ type: 'object',
66
+ properties: {
67
+ query: { type: 'string', description: 'FTS5 search term (e.g., "BigInt serialization", "schema mismatch")' },
68
+ category: { type: 'string', description: 'Filter by category: patterns, commands, incidents, reference, protocols, memory, checklists, playbooks, critical, root' },
69
+ chunk_type: { type: 'string', description: 'Filter by chunk type: section, rule, incident, pattern, command, mismatch, code_block, table_row' },
70
+ limit: { type: 'number', description: 'Max results (default 10)' },
71
+ },
72
+ required: ['query'],
73
+ },
74
+ },
75
+ // P2-002: knowledge_rule
76
+ {
77
+ name: p('knowledge_rule'),
78
+ description: 'Look up a specific CR rule, its VR verification, linked incidents, and prevention. Returns rule text, reference file, and connected entities.',
79
+ inputSchema: {
80
+ type: 'object',
81
+ properties: {
82
+ rule_id: { type: 'string', description: 'Exact CR rule ID (e.g., "CR-6")' },
83
+ keyword: { type: 'string', description: 'Search rule text (e.g., "database schema")' },
84
+ with_incidents: { type: 'boolean', description: 'Include linked incidents (default true)' },
85
+ },
86
+ required: [],
87
+ },
88
+ },
89
+ // P2-003: knowledge_incident
90
+ {
91
+ name: p('knowledge_incident'),
92
+ description: 'Look up a specific incident or search incidents by type/keyword. Returns full incident detail with date, type, gap, prevention, CR added, root cause.',
93
+ inputSchema: {
94
+ type: 'object',
95
+ properties: {
96
+ incident_num: { type: 'number', description: 'Exact incident number' },
97
+ keyword: { type: 'string', description: 'Search gap_found + prevention + root_cause text' },
98
+ type: { type: 'string', description: 'Filter by type (e.g., "Schema Migration", "Plan Verification")' },
99
+ },
100
+ required: [],
101
+ },
102
+ },
103
+ // P2-004: knowledge_schema_check
104
+ {
105
+ name: p('knowledge_schema_check'),
106
+ description: `Check if a table/column name has a known mismatch documented in ${claudeDir}/. Instant warning for common pitfalls.`,
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ table: { type: 'string', description: 'Table name to check' },
111
+ column: { type: 'string', description: 'Column name to check if known-wrong anywhere' },
112
+ },
113
+ required: [],
114
+ },
115
+ },
116
+ // P2-005: knowledge_pattern
117
+ {
118
+ name: p('knowledge_pattern'),
119
+ description: 'Get relevant pattern guidance for a domain/topic without loading entire pattern files. Returns relevant sections with code examples and anti-patterns.',
120
+ inputSchema: {
121
+ type: 'object',
122
+ properties: {
123
+ domain: { type: 'string', description: 'Domain: database, auth, ui, build, security, realtime, form, etc.' },
124
+ topic: { type: 'string', description: 'Narrow to specific topic (e.g., "BigInt", "RLS", "Select.Item", "ctx.db")' },
125
+ },
126
+ required: ['domain'],
127
+ },
128
+ },
129
+ // P2-006: knowledge_verification
130
+ {
131
+ name: p('knowledge_verification'),
132
+ description: 'Look up which VR-* check to run for a given situation. Returns VR type, command, expected output, when to use.',
133
+ inputSchema: {
134
+ type: 'object',
135
+ properties: {
136
+ vr_type: { type: 'string', description: 'Exact VR type (e.g., "VR-BUILD", "VR-SCHEMA-PRE")' },
137
+ situation: { type: 'string', description: 'Describe the situation (e.g., "claiming production ready", "schema change")' },
138
+ },
139
+ required: [],
140
+ },
141
+ },
142
+ // P2-007: knowledge_graph
143
+ {
144
+ name: p('knowledge_graph'),
145
+ description: 'Traverse the knowledge cross-reference graph. Find everything connected to a given entity (CR, VR, incident, pattern, command) at configurable depth.',
146
+ inputSchema: {
147
+ type: 'object',
148
+ properties: {
149
+ entity_type: { type: 'string', description: 'Type: cr, vr, incident, pattern, command, chunk' },
150
+ entity_id: { type: 'string', description: 'ID: "CR-6", "VR-SCHEMA", "3", "database-patterns"' },
151
+ depth: { type: 'number', description: 'Traversal depth (default 1, max 3)' },
152
+ },
153
+ required: ['entity_type', 'entity_id'],
154
+ },
155
+ },
156
+ // P2-008: knowledge_command
157
+ {
158
+ name: p('knowledge_command'),
159
+ description: 'Get summary of a slash command\'s purpose, workflow position, and key rules. Search commands by name or keyword.',
160
+ inputSchema: {
161
+ type: 'object',
162
+ properties: {
163
+ command: { type: 'string', description: 'Command name (e.g., "massu-loop", "massu-plan")' },
164
+ keyword: { type: 'string', description: 'Search command descriptions (e.g., "audit", "implement")' },
165
+ },
166
+ required: [],
167
+ },
168
+ },
169
+ // P2-009: knowledge_correct
170
+ {
171
+ name: p('knowledge_correct'),
172
+ description: 'Record a user correction for persistent behavioral learning. Appends structured entry to corrections.md and indexes into knowledge DB.',
173
+ inputSchema: {
174
+ type: 'object',
175
+ properties: {
176
+ wrong: { type: 'string', description: 'What the model did incorrectly' },
177
+ correction: { type: 'string', description: 'The correct behavior (user feedback)' },
178
+ rule: { type: 'string', description: 'Prevention rule in imperative form' },
179
+ cr_rule: { type: 'string', description: 'Linked canonical rule ID (e.g., CR-28). Optional.' },
180
+ },
181
+ required: ['wrong', 'correction', 'rule'],
182
+ },
183
+ },
184
+ // P2-010: knowledge_plan
185
+ {
186
+ name: p('knowledge_plan'),
187
+ description: 'Find plans related to a file, feature, or keyword. Answers "which plan created/modified this file?" and "which files does plan X touch?"',
188
+ inputSchema: {
189
+ type: 'object',
190
+ properties: {
191
+ file: { type: 'string', description: 'File path to find related plans for (e.g., "src/server/api/routers/orders.ts")' },
192
+ keyword: { type: 'string', description: 'Search plan titles and content by keyword' },
193
+ plan_name: { type: 'string', description: 'Plan filename or slug to get details for' },
194
+ status: { type: 'string', description: 'Filter by implementation status: COMPLETE, IN_PROGRESS, NOT_STARTED' },
195
+ },
196
+ required: [],
197
+ },
198
+ },
199
+ // P2-011: knowledge_gaps
200
+ {
201
+ name: p('knowledge_gaps'),
202
+ description: `Detect documentation blind spots: features, routers, or domains with no corresponding ${claudeDir}/ documentation coverage. Cross-references sentinel features against knowledge documents.`,
203
+ inputSchema: {
204
+ type: 'object',
205
+ properties: {
206
+ domain: { type: 'string', description: 'Check gaps for specific domain' },
207
+ check_type: { type: 'string', enum: ['features', 'routers', 'patterns', 'incidents'], description: 'Type of gap check. Default: features' },
208
+ },
209
+ required: [],
210
+ },
211
+ },
212
+ // P2-012: knowledge_effectiveness
213
+ {
214
+ name: p('knowledge_effectiveness'),
215
+ description: 'Track which CR rules and VR checks are most frequently violated or triggered. Cross-references rules against memory observations to rank by effectiveness.',
216
+ inputSchema: {
217
+ type: 'object',
218
+ properties: {
219
+ rule_id: { type: 'string', description: 'Check effectiveness of a specific rule (e.g., CR-28)' },
220
+ top_n: { type: 'number', description: 'Show top N most/least violated rules. Default: 10' },
221
+ mode: { type: 'string', enum: ['most_violated', 'least_violated', 'most_effective', 'detail'], description: 'Ranking mode. Default: most_violated' },
222
+ },
223
+ required: [],
224
+ },
225
+ },
226
+ ];
227
+ }
228
+
229
+ // ============================================================
230
+ // Tool Name Matching (3-function pattern)
231
+ // ============================================================
232
+
233
+ const KNOWLEDGE_BASE_NAMES = new Set([
234
+ 'knowledge_search',
235
+ 'knowledge_rule',
236
+ 'knowledge_incident',
237
+ 'knowledge_schema_check',
238
+ 'knowledge_pattern',
239
+ 'knowledge_verification',
240
+ 'knowledge_graph',
241
+ 'knowledge_command',
242
+ 'knowledge_correct',
243
+ 'knowledge_plan',
244
+ 'knowledge_gaps',
245
+ 'knowledge_effectiveness',
246
+ ]);
247
+
248
+ export function isKnowledgeTool(name: string): boolean {
249
+ const pfx = getConfig().toolPrefix + '_';
250
+ const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
251
+ return KNOWLEDGE_BASE_NAMES.has(baseName);
252
+ }
253
+
254
+ // ============================================================
255
+ // Tool Handler Router
256
+ // ============================================================
257
+
258
+ export function handleKnowledgeToolCall(
259
+ name: string,
260
+ args: Record<string, unknown>,
261
+ db: Database.Database
262
+ ): ToolResult {
263
+ // P4-002: Lazy re-index before any query
264
+ ensureKnowledgeIndexed(db);
265
+
266
+ if (!hasData(db)) {
267
+ return text('No knowledge indexed yet. The knowledge DB will be populated automatically on next MCP server restart.');
268
+ }
269
+
270
+ const baseName = stripPrefix(name);
271
+
272
+ switch (baseName) {
273
+ case 'knowledge_search':
274
+ return handleSearch(db, args);
275
+ case 'knowledge_rule':
276
+ return handleRule(db, args);
277
+ case 'knowledge_incident':
278
+ return handleIncident(db, args);
279
+ case 'knowledge_schema_check':
280
+ return handleSchemaCheck(db, args);
281
+ case 'knowledge_pattern':
282
+ return handlePattern(db, args);
283
+ case 'knowledge_verification':
284
+ return handleVerification(db, args);
285
+ case 'knowledge_graph':
286
+ return handleGraph(db, args);
287
+ case 'knowledge_command':
288
+ return handleCommand(db, args);
289
+ case 'knowledge_correct':
290
+ return handleCorrect(db, args);
291
+ case 'knowledge_plan':
292
+ return handlePlan(db, args);
293
+ case 'knowledge_gaps':
294
+ return handleGaps(db, args);
295
+ case 'knowledge_effectiveness':
296
+ return handleEffectiveness(db, args);
297
+ default:
298
+ return text(`Unknown knowledge tool: ${name}`);
299
+ }
300
+ }
301
+
302
+ // ============================================================
303
+ // P2-001: knowledge_search
304
+ // ============================================================
305
+
306
+ function handleSearch(db: Database.Database, args: Record<string, unknown>): ToolResult {
307
+ const claudeDir = getConfig().conventions?.claudeDirName ?? '.claude';
308
+ const query = args.query as string;
309
+ const category = args.category as string | undefined;
310
+ const chunkType = args.chunk_type as string | undefined;
311
+ const limit = Math.min((args.limit as number) || 10, 50);
312
+
313
+ if (!query) return text('Error: query is required');
314
+
315
+ const lines: string[] = [];
316
+ lines.push(`## Knowledge Search: "${query}"`);
317
+ lines.push('');
318
+
319
+ try {
320
+ // Build FTS5 query with optional filters
321
+ let sql = `
322
+ SELECT kc.id, kc.heading, kc.content, kc.chunk_type, kc.metadata,
323
+ kd.file_path, kd.category, kd.title,
324
+ rank
325
+ FROM knowledge_fts
326
+ JOIN knowledge_chunks kc ON kc.id = knowledge_fts.rowid
327
+ JOIN knowledge_documents kd ON kd.id = kc.document_id
328
+ WHERE knowledge_fts MATCH ?
329
+ `;
330
+ const params: (string | number)[] = [sanitizeFts5Query(query)];
331
+
332
+ if (category) {
333
+ sql += ' AND kd.category = ?';
334
+ params.push(category);
335
+ }
336
+ if (chunkType) {
337
+ sql += ' AND kc.chunk_type = ?';
338
+ params.push(chunkType);
339
+ }
340
+
341
+ sql += ' ORDER BY rank LIMIT ?';
342
+ params.push(limit);
343
+
344
+ const results = db.prepare(sql).all(...params) as {
345
+ id: number; heading: string; content: string; chunk_type: string;
346
+ metadata: string; file_path: string; category: string; title: string; rank: number;
347
+ }[];
348
+
349
+ if (results.length === 0) {
350
+ lines.push('No matches found.');
351
+ lines.push('');
352
+ lines.push('Tips: Try broader terms, check spelling, or remove filters.');
353
+ } else {
354
+ lines.push(`Found ${results.length} result(s):`);
355
+ lines.push('');
356
+
357
+ for (const r of results) {
358
+ const preview = r.content.length > 500 ? r.content.substring(0, 500) + '...' : r.content;
359
+ lines.push(`### ${r.heading || r.title} [${r.chunk_type}]`);
360
+ lines.push(`**File**: ${claudeDir}/${r.file_path} | **Category**: ${r.category}`);
361
+ lines.push('');
362
+ lines.push(preview);
363
+ lines.push('');
364
+ lines.push('---');
365
+ lines.push('');
366
+ }
367
+ }
368
+ } catch (error) {
369
+ lines.push(`Search error: ${error instanceof Error ? error.message : String(error)}`);
370
+ lines.push('');
371
+ lines.push('Tip: FTS5 uses match syntax. For exact phrase, wrap in double quotes: "BigInt serialization"');
372
+ }
373
+
374
+ return text(lines.join('\n'));
375
+ }
376
+
377
+ // ============================================================
378
+ // P2-002: knowledge_rule
379
+ // ============================================================
380
+
381
+ function handleRule(db: Database.Database, args: Record<string, unknown>): ToolResult {
382
+ const ruleId = args.rule_id as string | undefined;
383
+ const keyword = args.keyword as string | undefined;
384
+ const withIncidents = args.with_incidents !== false; // default true
385
+
386
+ const lines: string[] = [];
387
+
388
+ if (ruleId) {
389
+ // Exact lookup
390
+ const rule = db.prepare('SELECT * FROM knowledge_rules WHERE rule_id = ?').get(ruleId) as {
391
+ rule_id: string; rule_text: string; vr_type: string; reference_path: string;
392
+ severity: string; prevention_summary: string;
393
+ } | undefined;
394
+
395
+ if (!rule) {
396
+ return text(`Rule ${ruleId} not found in knowledge base. Try ${p('knowledge_search')} with query: "${ruleId}" or ${p('knowledge_rule')} with keyword to search rule text.`);
397
+ }
398
+
399
+ lines.push(`## ${rule.rule_id}: ${rule.rule_text}`);
400
+ lines.push('');
401
+ lines.push(`| Field | Value |`);
402
+ lines.push(`|-------|-------|`);
403
+ lines.push(`| **Verification** | ${rule.vr_type || 'N/A'} |`);
404
+ lines.push(`| **Reference** | ${rule.reference_path || 'N/A'} |`);
405
+ lines.push(`| **Severity** | ${rule.severity || 'HIGH'} |`);
406
+ if (rule.prevention_summary) {
407
+ lines.push(`| **Prevention** | ${rule.prevention_summary} |`);
408
+ }
409
+ lines.push('');
410
+
411
+ // Get linked VR details
412
+ if (rule.vr_type) {
413
+ const vrTypes = rule.vr_type.split(/[,\s]+/).filter(v => v.startsWith('VR-'));
414
+ for (const vrType of vrTypes) {
415
+ const vr = db.prepare('SELECT * FROM knowledge_verifications WHERE vr_type = ?').get(vrType) as {
416
+ vr_type: string; command: string; expected: string; use_when: string;
417
+ } | undefined;
418
+ if (vr) {
419
+ lines.push(`### Verification: ${vr.vr_type}`);
420
+ lines.push(`- **Command**: \`${vr.command}\``);
421
+ lines.push(`- **Expected**: ${vr.expected}`);
422
+ lines.push(`- **Use When**: ${vr.use_when}`);
423
+ lines.push('');
424
+ }
425
+ }
426
+ }
427
+
428
+ // Get linked incidents
429
+ if (withIncidents) {
430
+ const edges = db.prepare(
431
+ "SELECT source_id FROM knowledge_edges WHERE target_type = 'cr' AND target_id = ? AND source_type = 'incident'"
432
+ ).all(ruleId) as { source_id: string }[];
433
+
434
+ if (edges.length > 0) {
435
+ lines.push('### Related Incidents');
436
+ for (const edge of edges) {
437
+ const incident = db.prepare('SELECT * FROM knowledge_incidents WHERE incident_num = ?').get(parseInt(edge.source_id, 10)) as {
438
+ incident_num: number; date: string; type: string; gap_found: string; prevention: string;
439
+ } | undefined;
440
+ if (incident) {
441
+ lines.push(`- **Incident #${incident.incident_num}** (${incident.date}): ${incident.type}`);
442
+ lines.push(` Gap: ${incident.gap_found}`);
443
+ lines.push(` Prevention: ${incident.prevention}`);
444
+ }
445
+ }
446
+ lines.push('');
447
+ }
448
+ }
449
+ } else if (keyword) {
450
+ // Search by keyword
451
+ const rules = db.prepare(
452
+ 'SELECT * FROM knowledge_rules WHERE rule_text LIKE ? OR rule_id LIKE ? ORDER BY rule_id'
453
+ ).all(`%${keyword}%`, `%${keyword}%`) as {
454
+ rule_id: string; rule_text: string; vr_type: string; reference_path: string;
455
+ }[];
456
+
457
+ lines.push(`## Rules matching "${keyword}" (${rules.length} found)`);
458
+ lines.push('');
459
+
460
+ for (const rule of rules) {
461
+ lines.push(`- **${rule.rule_id}**: ${rule.rule_text} | VR: ${rule.vr_type || 'N/A'} | Ref: ${rule.reference_path || 'N/A'}`);
462
+ }
463
+ } else {
464
+ // List all rules
465
+ const rules = db.prepare('SELECT * FROM knowledge_rules ORDER BY rule_id').all() as {
466
+ rule_id: string; rule_text: string; vr_type: string;
467
+ }[];
468
+
469
+ lines.push(`## All Canonical Rules (${rules.length} total)`);
470
+ lines.push('');
471
+
472
+ for (const rule of rules) {
473
+ lines.push(`- **${rule.rule_id}**: ${rule.rule_text} (${rule.vr_type || 'N/A'})`);
474
+ }
475
+ }
476
+
477
+ return text(lines.join('\n'));
478
+ }
479
+
480
+ // ============================================================
481
+ // P2-003: knowledge_incident
482
+ // ============================================================
483
+
484
+ function handleIncident(db: Database.Database, args: Record<string, unknown>): ToolResult {
485
+ const incidentNum = args.incident_num as number | undefined;
486
+ const keyword = args.keyword as string | undefined;
487
+ const type = args.type as string | undefined;
488
+
489
+ const lines: string[] = [];
490
+
491
+ if (incidentNum) {
492
+ // Exact lookup
493
+ const incident = db.prepare('SELECT * FROM knowledge_incidents WHERE incident_num = ?').get(incidentNum) as {
494
+ incident_num: number; date: string; type: string; gap_found: string;
495
+ prevention: string; cr_added: string; root_cause: string; user_quote: string;
496
+ } | undefined;
497
+
498
+ if (!incident) {
499
+ return text(`Incident #${incidentNum} not found in knowledge base. Try ${p('knowledge_incident')} with keyword to search incident descriptions.`);
500
+ }
501
+
502
+ lines.push(`## Incident #${incident.incident_num}`);
503
+ lines.push('');
504
+ lines.push(`| Field | Value |`);
505
+ lines.push(`|-------|-------|`);
506
+ lines.push(`| **Date** | ${incident.date} |`);
507
+ lines.push(`| **Type** | ${incident.type} |`);
508
+ lines.push(`| **Gap Found** | ${incident.gap_found} |`);
509
+ lines.push(`| **Prevention** | ${incident.prevention} |`);
510
+ if (incident.cr_added) lines.push(`| **CR Added** | ${incident.cr_added} |`);
511
+ if (incident.root_cause) lines.push(`| **Root Cause** | ${incident.root_cause} |`);
512
+ if (incident.user_quote) lines.push(`| **User Quote** | ${incident.user_quote.replace(/\|/g, '\\|')} |`);
513
+ lines.push('');
514
+
515
+ // Show linked CRs
516
+ const edges = db.prepare(
517
+ "SELECT target_id FROM knowledge_edges WHERE source_type = 'incident' AND source_id = ? AND target_type = 'cr'"
518
+ ).all(String(incidentNum)) as { target_id: string }[];
519
+
520
+ if (edges.length > 0) {
521
+ lines.push('### Linked Rules');
522
+ for (const edge of edges) {
523
+ const rule = db.prepare('SELECT rule_id, rule_text FROM knowledge_rules WHERE rule_id = ?').get(edge.target_id) as { rule_id: string; rule_text: string } | undefined;
524
+ if (rule) {
525
+ lines.push(`- **${rule.rule_id}**: ${rule.rule_text}`);
526
+ }
527
+ }
528
+ lines.push('');
529
+ }
530
+ } else {
531
+ // Search or filter
532
+ let sql = 'SELECT * FROM knowledge_incidents WHERE 1=1';
533
+ const params: string[] = [];
534
+
535
+ if (keyword) {
536
+ sql += ' AND (gap_found LIKE ? OR prevention LIKE ? OR root_cause LIKE ? OR type LIKE ?)';
537
+ const kw = `%${keyword}%`;
538
+ params.push(kw, kw, kw, kw);
539
+ }
540
+ if (type) {
541
+ sql += ' AND type LIKE ?';
542
+ params.push(`%${type}%`);
543
+ }
544
+
545
+ sql += ' ORDER BY incident_num';
546
+
547
+ const incidents = db.prepare(sql).all(...params) as {
548
+ incident_num: number; date: string; type: string; gap_found: string;
549
+ prevention: string; cr_added: string;
550
+ }[];
551
+
552
+ const desc = keyword ? `matching "${keyword}"` : type ? `type "${type}"` : 'all';
553
+ lines.push(`## Incidents ${desc} (${incidents.length} found)`);
554
+ lines.push('');
555
+
556
+ for (const inc of incidents) {
557
+ lines.push(`### #${inc.incident_num} (${inc.date}) — ${inc.type}`);
558
+ lines.push(`- **Gap**: ${inc.gap_found}`);
559
+ lines.push(`- **Prevention**: ${inc.prevention}`);
560
+ if (inc.cr_added) lines.push(`- **CR Added**: ${inc.cr_added}`);
561
+ lines.push('');
562
+ }
563
+ }
564
+
565
+ return text(lines.join('\n'));
566
+ }
567
+
568
+ // ============================================================
569
+ // P2-004: knowledge_schema_check
570
+ // ============================================================
571
+
572
+ function handleSchemaCheck(db: Database.Database, args: Record<string, unknown>): ToolResult {
573
+ const table = args.table as string | undefined;
574
+ const column = args.column as string | undefined;
575
+
576
+ const lines: string[] = [];
577
+
578
+ if (table) {
579
+ const mismatches = db.prepare(
580
+ 'SELECT * FROM knowledge_schema_mismatches WHERE table_name = ?'
581
+ ).all(table) as { table_name: string; wrong_column: string; correct_column: string; source: string }[];
582
+
583
+ if (mismatches.length > 0) {
584
+ lines.push(`## Schema Mismatches for \`${table}\``);
585
+ lines.push('');
586
+ lines.push('| WRONG Column | CORRECT Column | Source |');
587
+ lines.push('|-------------|----------------|--------|');
588
+ for (const m of mismatches) {
589
+ lines.push(`| \`${m.wrong_column}\` | \`${m.correct_column}\` | ${m.source} |`);
590
+ }
591
+ lines.push('');
592
+ lines.push('**WARNING**: Using the WRONG column will cause runtime errors. Always use the CORRECT column.');
593
+ } else {
594
+ lines.push(`No known schema mismatches for table \`${table}\`.`);
595
+ }
596
+ }
597
+
598
+ if (column) {
599
+ const mismatches = db.prepare(
600
+ 'SELECT * FROM knowledge_schema_mismatches WHERE wrong_column = ?'
601
+ ).all(column) as { table_name: string; wrong_column: string; correct_column: string; source: string }[];
602
+
603
+ if (mismatches.length > 0) {
604
+ if (lines.length > 0) lines.push('');
605
+ lines.push(`## Column \`${column}\` is KNOWN WRONG in:`);
606
+ lines.push('');
607
+ for (const m of mismatches) {
608
+ lines.push(`- **${m.table_name}**.${m.wrong_column} → should be **${m.correct_column}**`);
609
+ }
610
+ } else if (!table) {
611
+ lines.push(`Column \`${column}\` has no known mismatches.`);
612
+ }
613
+ }
614
+
615
+ if (!table && !column) {
616
+ // Show all mismatches
617
+ const all = db.prepare('SELECT * FROM knowledge_schema_mismatches ORDER BY table_name').all() as {
618
+ table_name: string; wrong_column: string; correct_column: string;
619
+ }[];
620
+
621
+ lines.push(`## All Known Schema Mismatches (${all.length} total)`);
622
+ lines.push('');
623
+ lines.push('| Table | WRONG | CORRECT |');
624
+ lines.push('|-------|-------|---------|');
625
+ for (const m of all) {
626
+ lines.push(`| ${m.table_name} | \`${m.wrong_column}\` | \`${m.correct_column}\` |`);
627
+ }
628
+ }
629
+
630
+ return text(lines.join('\n'));
631
+ }
632
+
633
+ // ============================================================
634
+ // P2-005: knowledge_pattern
635
+ // ============================================================
636
+
637
+ function handlePattern(db: Database.Database, args: Record<string, unknown>): ToolResult {
638
+ const claudeDir = getConfig().conventions?.claudeDirName ?? '.claude';
639
+ const domain = args.domain as string;
640
+ const topic = args.topic as string | undefined;
641
+
642
+ if (!domain) return text('Error: domain is required');
643
+
644
+ const lines: string[] = [];
645
+ lines.push(`## Pattern Guidance: ${domain}${topic ? ` / ${topic}` : ''}`);
646
+ lines.push('');
647
+
648
+ // Find pattern documents matching the domain
649
+ const domainPatterns = [
650
+ `${domain}-patterns`,
651
+ `${domain}`,
652
+ `patterns-quickref`,
653
+ ];
654
+
655
+ // First: get chunks from domain-specific pattern files
656
+ const docs = db.prepare(
657
+ "SELECT id, file_path, title FROM knowledge_documents WHERE category = 'patterns' OR category = 'reference' OR category = 'root'"
658
+ ).all() as { id: number; file_path: string; title: string }[];
659
+
660
+ const relevantDocs = docs.filter(d =>
661
+ domainPatterns.some(pat => d.file_path.toLowerCase().includes(pat.toLowerCase()))
662
+ );
663
+
664
+ if (relevantDocs.length === 0) {
665
+ // Fall back to FTS search
666
+ lines.push(`No dedicated pattern file found for domain "${domain}". Searching all knowledge...`);
667
+ lines.push('');
668
+ }
669
+
670
+ if (topic && relevantDocs.length > 0) {
671
+ // Search within domain-specific docs for the topic
672
+ const docIds = relevantDocs.map(d => d.id);
673
+ const placeholders = docIds.map(() => '?').join(',');
674
+
675
+ try {
676
+ const topicResults = db.prepare(`
677
+ SELECT kc.heading, kc.content, kc.chunk_type, kd.file_path
678
+ FROM knowledge_fts
679
+ JOIN knowledge_chunks kc ON kc.id = knowledge_fts.rowid
680
+ JOIN knowledge_documents kd ON kd.id = kc.document_id
681
+ WHERE knowledge_fts MATCH ?
682
+ AND kc.document_id IN (${placeholders})
683
+ ORDER BY rank
684
+ LIMIT 10
685
+ `).all(sanitizeFts5Query(topic), ...docIds) as {
686
+ heading: string; content: string; chunk_type: string; file_path: string;
687
+ }[];
688
+
689
+ if (topicResults.length > 0) {
690
+ for (const r of topicResults) {
691
+ lines.push(`### ${r.heading || '(section)'} [${r.chunk_type}]`);
692
+ lines.push(`*Source: ${claudeDir}/${r.file_path}*`);
693
+ lines.push('');
694
+ lines.push(r.content.length > 800 ? r.content.substring(0, 800) + '...' : r.content);
695
+ lines.push('');
696
+ lines.push('---');
697
+ lines.push('');
698
+ }
699
+ } else {
700
+ lines.push(`No specific sections found for topic "${topic}" in ${domain} patterns.`);
701
+ lines.push('');
702
+ }
703
+ } catch {
704
+ // FTS5 syntax error — fall through to direct search
705
+ lines.push(`FTS5 search failed for topic "${topic}". Showing all ${domain} sections instead.`);
706
+ lines.push('');
707
+ }
708
+ }
709
+
710
+ if (!topic && relevantDocs.length > 0) {
711
+ // Show all sections from the domain pattern file
712
+ for (const doc of relevantDocs.slice(0, 2)) {
713
+ const chunks = db.prepare(
714
+ "SELECT heading, content, chunk_type FROM knowledge_chunks WHERE document_id = ? AND chunk_type = 'section' ORDER BY line_start"
715
+ ).all(doc.id) as { heading: string; content: string; chunk_type: string }[];
716
+
717
+ lines.push(`### From: ${claudeDir}/${doc.file_path}`);
718
+ lines.push('');
719
+ lines.push(`**Sections** (${chunks.length} total):`);
720
+ for (const c of chunks) {
721
+ const preview = c.content.length > 100 ? c.content.substring(0, 100) + '...' : c.content;
722
+ lines.push(`- **${c.heading || '(untitled)'}**: ${preview}`);
723
+ }
724
+ lines.push('');
725
+ }
726
+ }
727
+
728
+ // If no results from domain docs, try broad FTS
729
+ if (relevantDocs.length === 0 || lines.length < 5) {
730
+ const searchTerm = topic ? `${domain} ${topic}` : domain;
731
+ try {
732
+ const ftsResults = db.prepare(`
733
+ SELECT kc.heading, kc.content, kc.chunk_type, kd.file_path
734
+ FROM knowledge_fts
735
+ JOIN knowledge_chunks kc ON kc.id = knowledge_fts.rowid
736
+ JOIN knowledge_documents kd ON kd.id = kc.document_id
737
+ WHERE knowledge_fts MATCH ?
738
+ ORDER BY rank
739
+ LIMIT 5
740
+ `).all(sanitizeFts5Query(searchTerm)) as {
741
+ heading: string; content: string; chunk_type: string; file_path: string;
742
+ }[];
743
+
744
+ if (ftsResults.length > 0) {
745
+ lines.push('### Broad Search Results');
746
+ lines.push('');
747
+ for (const r of ftsResults) {
748
+ lines.push(`- **${r.heading || '(section)'}** (${claudeDir}/${r.file_path}): ${r.content.substring(0, 200)}...`);
749
+ }
750
+ }
751
+ } catch {
752
+ // FTS syntax error — skip
753
+ }
754
+ }
755
+
756
+ return text(lines.join('\n'));
757
+ }
758
+
759
+ // ============================================================
760
+ // P2-006: knowledge_verification
761
+ // ============================================================
762
+
763
+ function handleVerification(db: Database.Database, args: Record<string, unknown>): ToolResult {
764
+ const vrType = args.vr_type as string | undefined;
765
+ const situation = args.situation as string | undefined;
766
+
767
+ const lines: string[] = [];
768
+
769
+ if (vrType) {
770
+ const vr = db.prepare('SELECT * FROM knowledge_verifications WHERE vr_type = ?').get(vrType) as {
771
+ vr_type: string; command: string; expected: string; use_when: string; catches: string; category: string;
772
+ } | undefined;
773
+
774
+ if (!vr) {
775
+ return text(`Verification type ${vrType} not found. Use ${p('knowledge_search')} to find related checks.`);
776
+ }
777
+
778
+ lines.push(`## ${vr.vr_type}`);
779
+ lines.push('');
780
+ lines.push(`| Field | Value |`);
781
+ lines.push(`|-------|-------|`);
782
+ lines.push(`| **Command** | \`${vr.command}\` |`);
783
+ lines.push(`| **Expected** | ${vr.expected} |`);
784
+ lines.push(`| **Use When** | ${vr.use_when} |`);
785
+ if (vr.catches) lines.push(`| **Catches** | ${vr.catches} |`);
786
+ if (vr.category) lines.push(`| **Category** | ${vr.category} |`);
787
+ } else if (situation) {
788
+ // Search use_when field
789
+ const results = db.prepare(
790
+ 'SELECT * FROM knowledge_verifications WHERE use_when LIKE ? OR vr_type LIKE ? OR catches LIKE ? ORDER BY vr_type'
791
+ ).all(`%${situation}%`, `%${situation}%`, `%${situation}%`) as {
792
+ vr_type: string; command: string; expected: string; use_when: string; catches: string;
793
+ }[];
794
+
795
+ lines.push(`## Verifications for "${situation}" (${results.length} found)`);
796
+ lines.push('');
797
+
798
+ for (const vr of results) {
799
+ lines.push(`### ${vr.vr_type}`);
800
+ lines.push(`- **Command**: \`${vr.command}\``);
801
+ lines.push(`- **Expected**: ${vr.expected}`);
802
+ lines.push(`- **Use When**: ${vr.use_when}`);
803
+ lines.push('');
804
+ }
805
+ } else {
806
+ // List all
807
+ const all = db.prepare('SELECT vr_type, command, use_when FROM knowledge_verifications ORDER BY vr_type').all() as {
808
+ vr_type: string; command: string; use_when: string;
809
+ }[];
810
+
811
+ lines.push(`## All Verification Types (${all.length} total)`);
812
+ lines.push('');
813
+
814
+ for (const vr of all) {
815
+ lines.push(`- **${vr.vr_type}**: \`${vr.command}\` — ${vr.use_when}`);
816
+ }
817
+ }
818
+
819
+ return text(lines.join('\n'));
820
+ }
821
+
822
+ // ============================================================
823
+ // P2-007: knowledge_graph
824
+ // ============================================================
825
+
826
+ function handleGraph(db: Database.Database, args: Record<string, unknown>): ToolResult {
827
+ const entityType = args.entity_type as string;
828
+ const entityId = args.entity_id as string;
829
+ const maxDepth = Math.min((args.depth as number) || 1, 3);
830
+
831
+ if (!entityType || !entityId) return text('Error: entity_type and entity_id are required');
832
+
833
+ const lines: string[] = [];
834
+ lines.push(`## Knowledge Graph: ${entityType}/${entityId} (depth ${maxDepth})`);
835
+ lines.push('');
836
+
837
+ // BFS traversal
838
+ type Entity = { type: string; id: string };
839
+ const visited = new Set<string>();
840
+ let currentLevel: Entity[] = [{ type: entityType, id: entityId }];
841
+ visited.add(`${entityType}:${entityId}`);
842
+
843
+ for (let depth = 0; depth < maxDepth && currentLevel.length > 0; depth++) {
844
+ const nextLevel: Entity[] = [];
845
+
846
+ lines.push(`### Depth ${depth + 1}`);
847
+ lines.push('');
848
+
849
+ for (const entity of currentLevel) {
850
+ // Find outgoing edges
851
+ const outgoing = db.prepare(
852
+ 'SELECT target_type, target_id, edge_type FROM knowledge_edges WHERE source_type = ? AND source_id = ?'
853
+ ).all(entity.type, entity.id) as { target_type: string; target_id: string; edge_type: string }[];
854
+
855
+ // Find incoming edges
856
+ const incoming = db.prepare(
857
+ 'SELECT source_type, source_id, edge_type FROM knowledge_edges WHERE target_type = ? AND target_id = ?'
858
+ ).all(entity.type, entity.id) as { source_type: string; source_id: string; edge_type: string }[];
859
+
860
+ for (const edge of outgoing) {
861
+ const key = `${edge.target_type}:${edge.target_id}`;
862
+ if (!visited.has(key)) {
863
+ visited.add(key);
864
+ lines.push(`- ${entity.type}/${entity.id} —[${edge.edge_type}]→ **${edge.target_type}/${edge.target_id}**`);
865
+ nextLevel.push({ type: edge.target_type, id: edge.target_id });
866
+ }
867
+ }
868
+
869
+ for (const edge of incoming) {
870
+ const key = `${edge.source_type}:${edge.source_id}`;
871
+ if (!visited.has(key)) {
872
+ visited.add(key);
873
+ lines.push(`- **${edge.source_type}/${edge.source_id}** —[${edge.edge_type}]→ ${entity.type}/${entity.id}`);
874
+ nextLevel.push({ type: edge.source_type, id: edge.source_id });
875
+ }
876
+ }
877
+ }
878
+
879
+ if (nextLevel.length === 0) {
880
+ lines.push('(no further connections)');
881
+ }
882
+ lines.push('');
883
+
884
+ currentLevel = nextLevel;
885
+ }
886
+
887
+ // Summary
888
+ lines.push(`**Total connected entities**: ${visited.size - 1}`);
889
+
890
+ return text(lines.join('\n'));
891
+ }
892
+
893
+ // ============================================================
894
+ // P2-008: knowledge_command
895
+ // ============================================================
896
+
897
+ function handleCommand(db: Database.Database, args: Record<string, unknown>): ToolResult {
898
+ const claudeDir = getConfig().conventions?.claudeDirName ?? '.claude';
899
+ const command = args.command as string | undefined;
900
+ const keyword = args.keyword as string | undefined;
901
+
902
+ const lines: string[] = [];
903
+
904
+ if (command) {
905
+ // Exact command lookup — find the command chunk
906
+ // Support both prefixed (massu-xxx) and unprefixed (xxx) lookups
907
+ const chunks = db.prepare(`
908
+ SELECT kc.heading, kc.content, kd.file_path, kd.title, kd.description
909
+ FROM knowledge_chunks kc
910
+ JOIN knowledge_documents kd ON kd.id = kc.document_id
911
+ WHERE kc.chunk_type = 'command' AND (kc.heading = ? OR kc.heading = ?)
912
+ LIMIT 1
913
+ `).all(command, command.replace('massu-', '')) as {
914
+ heading: string; content: string; file_path: string; title: string; description: string;
915
+ }[];
916
+
917
+ if (chunks.length === 0) {
918
+ // Try searching by file path
919
+ const doc = db.prepare(
920
+ "SELECT id, file_path, title, description FROM knowledge_documents WHERE category = 'commands' AND file_path LIKE ?"
921
+ ).get(`%${command}%`) as { id: number; file_path: string; title: string; description: string } | undefined;
922
+
923
+ if (!doc) {
924
+ return text(`Command "${command}" not found. Use keyword search to find related commands.`);
925
+ }
926
+
927
+ // Get the first section of this command's document
928
+ const sections = db.prepare(
929
+ "SELECT heading, content FROM knowledge_chunks WHERE document_id = ? AND chunk_type = 'section' ORDER BY line_start LIMIT 3"
930
+ ).all(doc.id) as { heading: string; content: string }[];
931
+
932
+ lines.push(`## Command: ${command}`);
933
+ lines.push(`**File**: ${claudeDir}/${doc.file_path}`);
934
+ if (doc.description) lines.push(`**Description**: ${doc.description}`);
935
+ lines.push('');
936
+
937
+ for (const s of sections) {
938
+ lines.push(`### ${s.heading}`);
939
+ lines.push(s.content.length > 500 ? s.content.substring(0, 500) + '...' : s.content);
940
+ lines.push('');
941
+ }
942
+ } else {
943
+ const chunk = chunks[0];
944
+ lines.push(`## Command: ${chunk.heading}`);
945
+ lines.push(`**File**: ${claudeDir}/${chunk.file_path}`);
946
+ if (chunk.description) lines.push(`**Description**: ${chunk.description}`);
947
+ lines.push('');
948
+ lines.push(chunk.content);
949
+ }
950
+ } else if (keyword) {
951
+ // Search commands
952
+ const results = db.prepare(`
953
+ SELECT kc.heading, kc.content, kd.file_path, kd.title
954
+ FROM knowledge_chunks kc
955
+ JOIN knowledge_documents kd ON kd.id = kc.document_id
956
+ WHERE kc.chunk_type = 'command' AND (kc.content LIKE ? OR kc.heading LIKE ?)
957
+ ORDER BY kc.heading
958
+ `).all(`%${keyword}%`, `%${keyword}%`) as {
959
+ heading: string; content: string; file_path: string; title: string;
960
+ }[];
961
+
962
+ lines.push(`## Commands matching "${keyword}" (${results.length} found)`);
963
+ lines.push('');
964
+
965
+ for (const r of results) {
966
+ const preview = r.content.substring(0, 200).replace(/\n/g, ' ');
967
+ lines.push(`- **${r.heading}** (${claudeDir}/${r.file_path}): ${preview}...`);
968
+ }
969
+ } else {
970
+ // List all commands
971
+ const commands = db.prepare(`
972
+ SELECT kc.heading, kd.file_path, kd.description
973
+ FROM knowledge_chunks kc
974
+ JOIN knowledge_documents kd ON kd.id = kc.document_id
975
+ WHERE kc.chunk_type = 'command'
976
+ ORDER BY kc.heading
977
+ `).all() as { heading: string; file_path: string; description: string }[];
978
+
979
+ lines.push(`## All Commands (${commands.length} total)`);
980
+ lines.push('');
981
+
982
+ for (const cmd of commands) {
983
+ lines.push(`- **${cmd.heading}** (${claudeDir}/${cmd.file_path})${cmd.description ? ': ' + cmd.description : ''}`);
984
+ }
985
+ }
986
+
987
+ return text(lines.join('\n'));
988
+ }
989
+
990
+ // ============================================================
991
+ // P2-009: knowledge_correct
992
+ // ============================================================
993
+
994
+ function handleCorrect(db: Database.Database, args: Record<string, unknown>): ToolResult {
995
+ const wrong = args.wrong as string;
996
+ const correction = args.correction as string;
997
+ const rule = args.rule as string;
998
+ const crRule = args.cr_rule as string | undefined;
999
+
1000
+ if (!wrong || !correction || !rule) {
1001
+ return text('Error: wrong, correction, and rule are all required.');
1002
+ }
1003
+
1004
+ // 1. Append to corrections.md
1005
+ const correctionsPath = resolve(getResolvedPaths().memoryDir, 'corrections.md');
1006
+ const today = new Date().toISOString().split('T')[0];
1007
+ const title = rule.slice(0, 60);
1008
+
1009
+ const entry = `\n### ${today} - ${title}\n- **Wrong**: ${wrong}\n- **Correction**: ${correction}\n- **Rule**: ${rule}\n${crRule ? `- **CR**: ${crRule}\n` : ''}\n`;
1010
+
1011
+ // Read existing content, find insertion point (before ## Archived if present, else append)
1012
+ let existing = '';
1013
+ try { existing = readFileSync(correctionsPath, 'utf-8'); } catch { /* new file */ }
1014
+
1015
+ const archiveIdx = existing.indexOf('## Archived');
1016
+ if (archiveIdx > 0) {
1017
+ const before = existing.slice(0, archiveIdx);
1018
+ const after = existing.slice(archiveIdx);
1019
+ writeFileSync(correctionsPath, before + entry + after);
1020
+ } else {
1021
+ appendFileSync(correctionsPath, entry);
1022
+ }
1023
+
1024
+ // 2. Index into knowledge DB immediately (insert chunk + edge)
1025
+ const doc = db.prepare('SELECT id FROM knowledge_documents WHERE file_path LIKE ?').get('%corrections.md') as { id: number } | undefined;
1026
+ if (doc) {
1027
+ db.prepare('INSERT INTO knowledge_chunks (document_id, chunk_type, heading, content, metadata) VALUES (?, ?, ?, ?, ?)')
1028
+ .run(doc.id, 'section', `Correction: ${title}`,
1029
+ `Wrong: ${wrong}\nCorrection: ${correction}\nRule: ${rule}`,
1030
+ JSON.stringify({ is_correction: true, date: today, cr_rule: crRule }));
1031
+
1032
+ if (crRule) {
1033
+ db.prepare('INSERT OR IGNORE INTO knowledge_edges (source_type, source_id, target_type, target_id, edge_type) VALUES (?, ?, ?, ?, ?)')
1034
+ .run('correction', title, 'cr', crRule, 'enforces');
1035
+ }
1036
+ }
1037
+
1038
+ return text(`Correction recorded:\n- **Date**: ${today}\n- **Wrong**: ${wrong}\n- **Rule**: ${rule}\n${crRule ? `- **CR**: ${crRule}\n` : ''}File: corrections.md updated`);
1039
+ }
1040
+
1041
+ // ============================================================
1042
+ // P2-010: knowledge_plan
1043
+ // ============================================================
1044
+
1045
+ function handlePlan(db: Database.Database, args: Record<string, unknown>): ToolResult {
1046
+ const file = args.file as string | undefined;
1047
+ const keyword = args.keyword as string | undefined;
1048
+ const planName = args.plan_name as string | undefined;
1049
+ const status = args.status as string | undefined;
1050
+
1051
+ const lines: string[] = [];
1052
+
1053
+ if (file) {
1054
+ // Find plans that reference this file
1055
+ const results = db.prepare(`
1056
+ SELECT kd.file_path, kd.title, kc.content
1057
+ FROM knowledge_chunks kc
1058
+ JOIN knowledge_documents kd ON kd.id = kc.document_id
1059
+ WHERE kd.category = 'plan'
1060
+ AND kc.heading = 'Referenced Files'
1061
+ AND kc.content LIKE ?
1062
+ ORDER BY kd.file_path DESC
1063
+ LIMIT 20
1064
+ `).all(`%${basename(file)}%`) as { file_path: string; title: string; content: string }[];
1065
+
1066
+ lines.push(`## Plans referencing \`${file}\` (${results.length} found)`);
1067
+ lines.push('');
1068
+
1069
+ for (const r of results) {
1070
+ lines.push(`- **${r.title}** (${r.file_path})`);
1071
+ }
1072
+ } else if (keyword) {
1073
+ // FTS search scoped to plan documents
1074
+ try {
1075
+ const results = db.prepare(`
1076
+ SELECT DISTINCT kd.file_path, kd.title
1077
+ FROM knowledge_fts kf
1078
+ JOIN knowledge_chunks kc ON kc.id = kf.rowid
1079
+ JOIN knowledge_documents kd ON kd.id = kc.document_id
1080
+ WHERE kf.content MATCH ? AND kd.category = 'plan'
1081
+ ORDER BY rank
1082
+ LIMIT 20
1083
+ `).all(sanitizeFts5Query(keyword)) as { file_path: string; title: string }[];
1084
+
1085
+ lines.push(`## Plans matching "${keyword}" (${results.length} found)`);
1086
+ lines.push('');
1087
+
1088
+ for (const r of results) {
1089
+ lines.push(`- **${r.title}** (${r.file_path})`);
1090
+ }
1091
+ } catch {
1092
+ lines.push(`FTS search error for "${keyword}". Try simpler terms.`);
1093
+ }
1094
+ } else if (planName) {
1095
+ // Find specific plan by filename
1096
+ const doc = db.prepare(
1097
+ "SELECT id, file_path, title, description FROM knowledge_documents WHERE category = 'plan' AND file_path LIKE ? LIMIT 1"
1098
+ ).get(`%${planName}%`) as { id: number; file_path: string; title: string; description: string } | undefined;
1099
+
1100
+ if (!doc) {
1101
+ return text(`Plan "${planName}" not found. Try keyword search.`);
1102
+ }
1103
+
1104
+ lines.push(`## Plan: ${doc.title}`);
1105
+ lines.push(`**File**: ${doc.file_path}`);
1106
+ if (doc.description) lines.push(`**Description**: ${doc.description}`);
1107
+ lines.push('');
1108
+
1109
+ // Get plan items
1110
+ const items = db.prepare(
1111
+ "SELECT heading, content FROM knowledge_chunks WHERE document_id = ? AND metadata LIKE '%plan_item_id%' ORDER BY heading"
1112
+ ).all(doc.id) as { heading: string; content: string }[];
1113
+
1114
+ if (items.length > 0) {
1115
+ lines.push(`### Plan Items (${items.length})`);
1116
+ for (const item of items) {
1117
+ lines.push(`- ${item.content}`);
1118
+ }
1119
+ lines.push('');
1120
+ }
1121
+
1122
+ // Get implementation status
1123
+ const statusChunk = db.prepare(
1124
+ "SELECT content FROM knowledge_chunks WHERE document_id = ? AND heading = 'IMPLEMENTATION STATUS' LIMIT 1"
1125
+ ).get(doc.id) as { content: string } | undefined;
1126
+
1127
+ if (statusChunk) {
1128
+ lines.push('### Implementation Status');
1129
+ lines.push(statusChunk.content.substring(0, 1000));
1130
+ lines.push('');
1131
+ }
1132
+
1133
+ // Get referenced files
1134
+ const fileRefs = db.prepare(
1135
+ "SELECT content FROM knowledge_chunks WHERE document_id = ? AND heading = 'Referenced Files' LIMIT 1"
1136
+ ).get(doc.id) as { content: string } | undefined;
1137
+
1138
+ if (fileRefs) {
1139
+ lines.push('### Referenced Files');
1140
+ lines.push(fileRefs.content.substring(0, 500));
1141
+ }
1142
+ } else if (status) {
1143
+ // Filter plans by status
1144
+ const docs = db.prepare(
1145
+ "SELECT kd.file_path, kd.title, kc.content FROM knowledge_chunks kc JOIN knowledge_documents kd ON kd.id = kc.document_id WHERE kd.category = 'plan' AND kc.heading = 'IMPLEMENTATION STATUS' AND kc.content LIKE ? ORDER BY kd.file_path DESC"
1146
+ ).all(`%${status}%`) as { file_path: string; title: string; content: string }[];
1147
+
1148
+ lines.push(`## Plans with status "${status}" (${docs.length} found)`);
1149
+ lines.push('');
1150
+
1151
+ for (const d of docs) {
1152
+ lines.push(`- **${d.title}** (${d.file_path})`);
1153
+ }
1154
+ } else {
1155
+ // List all plans
1156
+ const plans = db.prepare(
1157
+ "SELECT file_path, title FROM knowledge_documents WHERE category = 'plan' ORDER BY file_path DESC LIMIT 50"
1158
+ ).all() as { file_path: string; title: string }[];
1159
+
1160
+ lines.push(`## All Plans (${plans.length} indexed)`);
1161
+ lines.push('');
1162
+
1163
+ for (const plan of plans) {
1164
+ lines.push(`- **${plan.title}** (${plan.file_path})`);
1165
+ }
1166
+ }
1167
+
1168
+ return text(lines.join('\n'));
1169
+ }
1170
+
1171
+ // ============================================================
1172
+ // P2-011: knowledge_gaps
1173
+ // ============================================================
1174
+
1175
+ function handleGaps(db: Database.Database, args: Record<string, unknown>): ToolResult {
1176
+ const domain = args.domain as string | undefined;
1177
+ const checkType = (args.check_type as string) || 'features';
1178
+
1179
+ const lines: string[] = [];
1180
+ lines.push(`## Knowledge Gap Analysis (${checkType})`);
1181
+ lines.push('');
1182
+
1183
+ if (checkType === 'features') {
1184
+ // Query sentinel features from data DB (massu_sentinel table)
1185
+ let dataDb: Database.Database | null = null;
1186
+ try {
1187
+ dataDb = getDataDb();
1188
+
1189
+ let sql = "SELECT feature_key, title, domain, status FROM massu_sentinel WHERE status = 'active'";
1190
+ const params: string[] = [];
1191
+ if (domain) {
1192
+ sql += ' AND domain LIKE ?';
1193
+ params.push(`%${domain}%`);
1194
+ }
1195
+ sql += ' ORDER BY domain, feature_key';
1196
+
1197
+ const features = dataDb.prepare(sql).all(...params) as {
1198
+ feature_key: string; title: string; domain: string; status: string;
1199
+ }[];
1200
+
1201
+ lines.push(`| Feature | Domain | Knowledge Hits | Status |`);
1202
+ lines.push(`|---------|--------|----------------|--------|`);
1203
+
1204
+ let gapCount = 0;
1205
+ let coveredCount = 0;
1206
+
1207
+ for (const feat of features) {
1208
+ // Search knowledge DB for this feature
1209
+ let hits = 0;
1210
+ try {
1211
+ const searchTerm = feat.title.replace(/['"]/g, '');
1212
+ const results = db.prepare(
1213
+ `SELECT COUNT(*) as cnt FROM knowledge_fts WHERE content MATCH ?`
1214
+ ).get(sanitizeFts5Query(searchTerm)) as { cnt: number };
1215
+ hits = results.cnt;
1216
+ } catch {
1217
+ // FTS error — try LIKE fallback
1218
+ const results = db.prepare(
1219
+ `SELECT COUNT(*) as cnt FROM knowledge_chunks WHERE content LIKE ?`
1220
+ ).get(`%${feat.feature_key}%`) as { cnt: number };
1221
+ hits = results.cnt;
1222
+ }
1223
+
1224
+ const gapStatus = hits === 0 ? 'GAP' : hits < 3 ? 'THIN' : 'COVERED';
1225
+ if (gapStatus === 'GAP') gapCount++;
1226
+ else coveredCount++;
1227
+
1228
+ lines.push(`| ${feat.feature_key} | ${feat.domain} | ${hits} | ${gapStatus} |`);
1229
+ }
1230
+
1231
+ lines.push('');
1232
+ lines.push(`**Summary**: ${coveredCount} covered, ${gapCount} gaps out of ${features.length} features`);
1233
+ } catch (error) {
1234
+ lines.push(`Error querying sentinel: ${error instanceof Error ? error.message : String(error)}`);
1235
+ } finally {
1236
+ dataDb?.close();
1237
+ }
1238
+ } else if (checkType === 'routers') {
1239
+ // List router files and check knowledge coverage
1240
+ try {
1241
+ const routersDir = getResolvedPaths().routersDir;
1242
+ const routerFiles = readdirSync(routersDir)
1243
+ .filter(f => f.endsWith('.ts') && !f.startsWith('_'));
1244
+
1245
+ lines.push(`| Router | Knowledge Hits | Status |`);
1246
+ lines.push(`|--------|----------------|--------|`);
1247
+
1248
+ for (const file of routerFiles) {
1249
+ const routerName = file.replace('.ts', '');
1250
+ const results = db.prepare(
1251
+ `SELECT COUNT(*) as cnt FROM knowledge_chunks WHERE content LIKE ?`
1252
+ ).get(`%${routerName}%`) as { cnt: number };
1253
+
1254
+ const routerStatus = results.cnt === 0 ? 'GAP' : results.cnt < 3 ? 'THIN' : 'COVERED';
1255
+ lines.push(`| ${routerName} | ${results.cnt} | ${routerStatus} |`);
1256
+ }
1257
+ } catch (error) {
1258
+ lines.push(`Error scanning routers: ${error instanceof Error ? error.message : String(error)}`);
1259
+ }
1260
+ } else if (checkType === 'patterns') {
1261
+ // Check if domains have pattern documentation
1262
+ const patternDocs = db.prepare(
1263
+ "SELECT file_path, title FROM knowledge_documents WHERE category = 'patterns'"
1264
+ ).all() as { file_path: string; title: string }[];
1265
+
1266
+ const documentedDomains = new Set(patternDocs.map(d => d.title.replace('-patterns', '').replace(' Patterns', '').toLowerCase()));
1267
+
1268
+ lines.push('### Pattern Coverage');
1269
+ lines.push(`Documented domains: ${[...documentedDomains].join(', ')}`);
1270
+ lines.push('');
1271
+
1272
+ // Check sentinel domains against pattern docs
1273
+ let dataDb: Database.Database | null = null;
1274
+ try {
1275
+ dataDb = getDataDb();
1276
+
1277
+ const domains = dataDb.prepare(
1278
+ "SELECT DISTINCT domain FROM massu_sentinel WHERE status = 'active' ORDER BY domain"
1279
+ ).all() as { domain: string }[];
1280
+
1281
+ for (const d of domains) {
1282
+ const hasPattern = documentedDomains.has(d.domain.toLowerCase());
1283
+ lines.push(`- **${d.domain}**: ${hasPattern ? 'COVERED' : 'GAP'}`);
1284
+ }
1285
+ } catch { /* sentinel not available */ }
1286
+ finally { dataDb?.close(); }
1287
+ } else if (checkType === 'incidents') {
1288
+ // Find domains with incidents but no prevention patterns
1289
+ const incidents = db.prepare(
1290
+ "SELECT type, COUNT(*) as cnt FROM knowledge_incidents GROUP BY type ORDER BY cnt DESC"
1291
+ ).all() as { type: string; cnt: number }[];
1292
+
1293
+ lines.push('### Incident-to-Pattern Coverage');
1294
+ lines.push(`| Incident Type | Count | Pattern Coverage |`);
1295
+ lines.push(`|---------------|-------|-----------------|`);
1296
+
1297
+ for (const inc of incidents) {
1298
+ const patternHits = db.prepare(
1299
+ "SELECT COUNT(*) as cnt FROM knowledge_chunks WHERE content LIKE ? AND document_id IN (SELECT id FROM knowledge_documents WHERE category = 'patterns')"
1300
+ ).get(`%${inc.type}%`) as { cnt: number };
1301
+
1302
+ const incStatus = patternHits.cnt === 0 ? 'GAP' : 'COVERED';
1303
+ lines.push(`| ${inc.type} | ${inc.cnt} | ${incStatus} (${patternHits.cnt} hits) |`);
1304
+ }
1305
+ }
1306
+
1307
+ return text(lines.join('\n'));
1308
+ }
1309
+
1310
+ // ============================================================
1311
+ // P2-012: knowledge_effectiveness
1312
+ // ============================================================
1313
+
1314
+ function handleEffectiveness(db: Database.Database, args: Record<string, unknown>): ToolResult {
1315
+ const ruleId = args.rule_id as string | undefined;
1316
+ const topN = Math.min((args.top_n as number) || 10, 50);
1317
+ const mode = (args.mode as string) || 'most_violated';
1318
+
1319
+ const lines: string[] = [];
1320
+
1321
+ // Get all CR rules from knowledge DB
1322
+ const allRules = db.prepare('SELECT rule_id, rule_text FROM knowledge_rules ORDER BY rule_id').all() as {
1323
+ rule_id: string; rule_text: string;
1324
+ }[];
1325
+
1326
+ // Try to get violation data from memory DB
1327
+ let memoryDb: InstanceType<typeof Database> | null = null;
1328
+ const violationCounts = new Map<string, number>();
1329
+ const lastViolated = new Map<string, string>();
1330
+
1331
+ try {
1332
+ memoryDb = getMemoryDb();
1333
+
1334
+ // Count observations that reference CR rules
1335
+ const observations = memoryDb.prepare(`
1336
+ SELECT cr_rule, COUNT(*) as cnt, MAX(created_at) as last_at
1337
+ FROM observations
1338
+ WHERE cr_rule IS NOT NULL AND cr_rule != ''
1339
+ GROUP BY cr_rule
1340
+ `).all() as { cr_rule: string; cnt: number; last_at: string }[];
1341
+
1342
+ for (const obs of observations) {
1343
+ violationCounts.set(obs.cr_rule, obs.cnt);
1344
+ lastViolated.set(obs.cr_rule, obs.last_at);
1345
+ }
1346
+ } catch { /* Memory DB not available */ }
1347
+ finally { memoryDb?.close(); }
1348
+
1349
+ if (ruleId || mode === 'detail') {
1350
+ const targetRule = ruleId || '';
1351
+ const rule = allRules.find(r => r.rule_id === targetRule);
1352
+
1353
+ if (!rule) {
1354
+ return text(`Rule ${targetRule} not found.`);
1355
+ }
1356
+
1357
+ lines.push(`## Rule Effectiveness: ${rule.rule_id}`);
1358
+ lines.push(`**Text**: ${rule.rule_text}`);
1359
+ lines.push('');
1360
+
1361
+ const violations = violationCounts.get(rule.rule_id) || 0;
1362
+ const lastAt = lastViolated.get(rule.rule_id) || 'Never';
1363
+
1364
+ // Count linked incidents
1365
+ const incidentEdges = db.prepare(
1366
+ "SELECT COUNT(*) as cnt FROM knowledge_edges WHERE target_type = 'cr' AND target_id = ? AND source_type = 'incident'"
1367
+ ).get(rule.rule_id) as { cnt: number };
1368
+
1369
+ // Count correction links
1370
+ const correctionEdges = db.prepare(
1371
+ "SELECT COUNT(*) as cnt FROM knowledge_edges WHERE target_type = 'cr' AND target_id = ? AND source_type = 'correction'"
1372
+ ).get(rule.rule_id) as { cnt: number };
1373
+
1374
+ lines.push(`| Metric | Value |`);
1375
+ lines.push(`|--------|-------|`);
1376
+ lines.push(`| Violations Observed | ${violations} |`);
1377
+ lines.push(`| Last Violated | ${lastAt} |`);
1378
+ lines.push(`| Linked Incidents | ${incidentEdges.cnt} |`);
1379
+ lines.push(`| Linked Corrections | ${correctionEdges.cnt} |`);
1380
+ lines.push(`| Effectiveness Score | ${violations === 0 ? 'HIGH (never violated)' : violations < 3 ? 'MEDIUM' : 'LOW (frequently violated)'} |`);
1381
+ } else {
1382
+ // Ranking mode
1383
+ const rulesWithCounts = allRules.map(r => ({
1384
+ ...r,
1385
+ violations: violationCounts.get(r.rule_id) || 0,
1386
+ last_at: lastViolated.get(r.rule_id) || 'Never',
1387
+ }));
1388
+
1389
+ if (mode === 'most_violated') {
1390
+ rulesWithCounts.sort((a, b) => b.violations - a.violations);
1391
+ lines.push(`## Most Violated Rules (Top ${topN})`);
1392
+ } else if (mode === 'least_violated') {
1393
+ rulesWithCounts.sort((a, b) => a.violations - b.violations);
1394
+ lines.push(`## Least Violated Rules (Top ${topN})`);
1395
+ } else if (mode === 'most_effective') {
1396
+ // Most effective = most violations observed (frequently caught and prevented)
1397
+ rulesWithCounts.sort((a, b) => b.violations - a.violations);
1398
+ lines.push(`## Most Effective Rules (Top ${topN})`);
1399
+ lines.push('*Rules that catch the most issues*');
1400
+ }
1401
+
1402
+ lines.push('');
1403
+ lines.push(`| Rule | Text | Violations | Last Violated |`);
1404
+ lines.push(`|------|------|------------|---------------|`);
1405
+
1406
+ for (const r of rulesWithCounts.slice(0, topN)) {
1407
+ const ruleText = r.rule_text.length > 40 ? r.rule_text.substring(0, 40) + '...' : r.rule_text;
1408
+ lines.push(`| ${r.rule_id} | ${ruleText} | ${r.violations} | ${r.last_at} |`);
1409
+ }
1410
+ }
1411
+
1412
+ return text(lines.join('\n'));
1413
+ }