@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.
- package/commands/_shared-preamble.md +76 -0
- package/commands/massu-audit-deps.md +211 -0
- package/commands/massu-changelog.md +174 -0
- package/commands/massu-cleanup.md +315 -0
- package/commands/massu-commit.md +481 -0
- package/commands/massu-create-plan.md +752 -0
- package/commands/massu-dead-code.md +131 -0
- package/commands/massu-debug.md +484 -0
- package/commands/massu-deploy.md +91 -0
- package/commands/massu-deps.md +374 -0
- package/commands/massu-doc-gen.md +279 -0
- package/commands/massu-docs.md +364 -0
- package/commands/massu-estimate.md +313 -0
- package/commands/massu-golden-path.md +973 -0
- package/commands/massu-guide.md +167 -0
- package/commands/massu-hotfix.md +480 -0
- package/commands/massu-loop-playwright.md +837 -0
- package/commands/massu-loop.md +775 -0
- package/commands/massu-new-feature.md +511 -0
- package/commands/massu-parity.md +214 -0
- package/commands/massu-plan.md +456 -0
- package/commands/massu-push-light.md +207 -0
- package/commands/massu-push.md +434 -0
- package/commands/massu-refactor.md +410 -0
- package/commands/massu-release.md +363 -0
- package/commands/massu-review.md +238 -0
- package/commands/massu-simplify.md +281 -0
- package/commands/massu-status.md +278 -0
- package/commands/massu-tdd.md +201 -0
- package/commands/massu-test.md +516 -0
- package/commands/massu-verify-playwright.md +281 -0
- package/commands/massu-verify.md +667 -0
- package/dist/cli.js +12522 -0
- package/dist/hooks/cost-tracker.js +80 -5
- package/dist/hooks/post-edit-context.js +72 -6
- package/dist/hooks/post-tool-use.js +234 -57
- package/dist/hooks/pre-compact.js +144 -5
- package/dist/hooks/pre-delete-check.js +141 -11
- package/dist/hooks/quality-event.js +80 -5
- package/dist/hooks/security-gate.js +29 -0
- package/dist/hooks/session-end.js +83 -8
- package/dist/hooks/session-start.js +153 -7
- package/dist/hooks/user-prompt.js +166 -5
- package/package.json +6 -5
- package/src/backfill-sessions.ts +5 -4
- package/src/cli.ts +6 -0
- package/src/commands/doctor.ts +193 -6
- package/src/commands/init.ts +235 -6
- package/src/commands/install-commands.ts +137 -0
- package/src/config.ts +68 -2
- package/src/db.ts +115 -2
- package/src/docs-tools.ts +8 -6
- package/src/hooks/post-edit-context.ts +1 -1
- package/src/hooks/post-tool-use.ts +130 -0
- package/src/hooks/pre-compact.ts +23 -1
- package/src/hooks/pre-delete-check.ts +92 -4
- package/src/hooks/security-gate.ts +32 -0
- package/src/hooks/session-start.ts +97 -4
- package/src/hooks/user-prompt.ts +46 -1
- package/src/import-resolver.ts +2 -1
- package/src/knowledge-db.ts +169 -0
- package/src/knowledge-indexer.ts +704 -0
- package/src/knowledge-tools.ts +1413 -0
- package/src/license.ts +482 -0
- package/src/memory-db.ts +14 -1
- package/src/observation-extractor.ts +11 -4
- package/src/page-deps.ts +3 -2
- package/src/python/coupling-detector.ts +124 -0
- package/src/python/domain-enforcer.ts +83 -0
- package/src/python/impact-analyzer.ts +95 -0
- package/src/python/import-parser.ts +244 -0
- package/src/python/import-resolver.ts +135 -0
- package/src/python/migration-indexer.ts +115 -0
- package/src/python/migration-parser.ts +332 -0
- package/src/python/model-indexer.ts +70 -0
- package/src/python/model-parser.ts +279 -0
- package/src/python/route-indexer.ts +58 -0
- package/src/python/route-parser.ts +317 -0
- package/src/python-tools.ts +629 -0
- package/src/sentinel-db.ts +2 -1
- package/src/server.ts +29 -6
- package/src/session-archiver.ts +4 -5
- package/src/tools.ts +283 -31
- 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
|
+
}
|