@massu/core 0.1.0 → 0.1.2
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/LICENSE +71 -0
- package/README.md +2 -2
- package/dist/hooks/cost-tracker.js +149 -11527
- package/dist/hooks/post-edit-context.js +127 -11493
- package/dist/hooks/post-tool-use.js +169 -11550
- package/dist/hooks/pre-compact.js +149 -11530
- package/dist/hooks/pre-delete-check.js +144 -11523
- package/dist/hooks/quality-event.js +149 -11527
- package/dist/hooks/session-end.js +188 -11570
- package/dist/hooks/session-start.js +159 -11534
- package/dist/hooks/user-prompt.js +149 -11530
- package/package.json +14 -19
- package/src/adr-generator.ts +292 -0
- package/src/analytics.ts +373 -0
- package/src/audit-trail.ts +450 -0
- package/src/backfill-sessions.ts +180 -0
- package/src/cli.ts +105 -0
- package/src/cloud-sync.ts +190 -0
- package/src/commands/doctor.ts +300 -0
- package/src/commands/init.ts +395 -0
- package/src/commands/install-hooks.ts +26 -0
- package/src/config.ts +357 -0
- package/src/cost-tracker.ts +355 -0
- package/src/db.ts +233 -0
- package/src/dependency-scorer.ts +337 -0
- package/src/docs-map.json +100 -0
- package/src/docs-tools.ts +517 -0
- package/src/domains.ts +181 -0
- package/src/hooks/cost-tracker.ts +66 -0
- package/src/hooks/intent-suggester.ts +131 -0
- package/src/hooks/post-edit-context.ts +91 -0
- package/src/hooks/post-tool-use.ts +175 -0
- package/src/hooks/pre-compact.ts +146 -0
- package/src/hooks/pre-delete-check.ts +153 -0
- package/src/hooks/quality-event.ts +127 -0
- package/src/hooks/security-gate.ts +121 -0
- package/src/hooks/session-end.ts +467 -0
- package/src/hooks/session-start.ts +210 -0
- package/src/hooks/user-prompt.ts +91 -0
- package/src/import-resolver.ts +224 -0
- package/src/memory-db.ts +1376 -0
- package/src/memory-tools.ts +391 -0
- package/src/middleware-tree.ts +70 -0
- package/src/observability-tools.ts +343 -0
- package/src/observation-extractor.ts +411 -0
- package/src/page-deps.ts +283 -0
- package/src/prompt-analyzer.ts +332 -0
- package/src/regression-detector.ts +319 -0
- package/src/rules.ts +57 -0
- package/src/schema-mapper.ts +232 -0
- package/src/security-scorer.ts +405 -0
- package/src/security-utils.ts +133 -0
- package/src/sentinel-db.ts +578 -0
- package/src/sentinel-scanner.ts +405 -0
- package/src/sentinel-tools.ts +512 -0
- package/src/sentinel-types.ts +140 -0
- package/src/server.ts +189 -0
- package/src/session-archiver.ts +112 -0
- package/src/session-state-generator.ts +174 -0
- package/src/team-knowledge.ts +407 -0
- package/src/tools.ts +847 -0
- package/src/transcript-parser.ts +458 -0
- package/src/trpc-index.ts +214 -0
- package/src/validate-features-runner.ts +106 -0
- package/src/validation-engine.ts +358 -0
- package/dist/cli.js +0 -7890
- package/dist/server.js +0 -7008
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
3
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
4
|
+
|
|
5
|
+
// ============================================================
|
|
6
|
+
// Standalone feature validation runner
|
|
7
|
+
// Called by scripts/validate-features.sh
|
|
8
|
+
// Directly imports sentinel-db.ts (no MCP protocol needed)
|
|
9
|
+
// ============================================================
|
|
10
|
+
|
|
11
|
+
import Database from 'better-sqlite3';
|
|
12
|
+
import { resolve } from 'path';
|
|
13
|
+
import { existsSync } from 'fs';
|
|
14
|
+
import { getProjectRoot, getResolvedPaths } from './config.ts';
|
|
15
|
+
|
|
16
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
17
|
+
|
|
18
|
+
function main(): void {
|
|
19
|
+
const dbPath = getResolvedPaths().dataDbPath;
|
|
20
|
+
|
|
21
|
+
if (!existsSync(dbPath)) {
|
|
22
|
+
console.log('Sentinel: No data DB found - skipping feature validation (run sync first)');
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let db: Database.Database;
|
|
27
|
+
try {
|
|
28
|
+
db = new Database(dbPath, { readonly: true });
|
|
29
|
+
db.pragma('journal_mode = WAL');
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.log('Sentinel: Could not open data DB - skipping feature validation');
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// Check if sentinel tables exist
|
|
37
|
+
const tableExists = db.prepare(
|
|
38
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='massu_sentinel'"
|
|
39
|
+
).get();
|
|
40
|
+
|
|
41
|
+
if (!tableExists) {
|
|
42
|
+
console.log('Sentinel: Feature registry not initialized - skipping (run sync first)');
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Count active features
|
|
47
|
+
const totalActive = db.prepare(
|
|
48
|
+
"SELECT COUNT(*) as count FROM massu_sentinel WHERE status = 'active'"
|
|
49
|
+
).get() as { count: number };
|
|
50
|
+
|
|
51
|
+
if (totalActive.count === 0) {
|
|
52
|
+
console.log('Sentinel: No active features registered - skipping validation');
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check for orphaned features (active features with missing primary component files)
|
|
57
|
+
const orphaned = db.prepare(`
|
|
58
|
+
SELECT s.feature_key, s.title, s.priority, c.component_file
|
|
59
|
+
FROM massu_sentinel s
|
|
60
|
+
JOIN massu_sentinel_components c ON c.feature_id = s.id AND c.is_primary = 1
|
|
61
|
+
WHERE s.status = 'active'
|
|
62
|
+
ORDER BY s.priority DESC, s.domain, s.feature_key
|
|
63
|
+
`).all() as { feature_key: string; title: string; priority: string; component_file: string }[];
|
|
64
|
+
|
|
65
|
+
const missingFeatures: { feature_key: string; title: string; priority: string; missing_file: string }[] = [];
|
|
66
|
+
|
|
67
|
+
for (const row of orphaned) {
|
|
68
|
+
const absPath = resolve(PROJECT_ROOT, row.component_file);
|
|
69
|
+
if (!existsSync(absPath)) {
|
|
70
|
+
missingFeatures.push({
|
|
71
|
+
feature_key: row.feature_key,
|
|
72
|
+
title: row.title,
|
|
73
|
+
priority: row.priority,
|
|
74
|
+
missing_file: row.component_file,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(`Sentinel: ${totalActive.count} active features, checking primary components...`);
|
|
80
|
+
|
|
81
|
+
if (missingFeatures.length === 0) {
|
|
82
|
+
console.log('Sentinel: All active features have living primary components. PASS');
|
|
83
|
+
process.exit(0);
|
|
84
|
+
} else {
|
|
85
|
+
console.error(`Sentinel: ${missingFeatures.length} features have MISSING primary components:`);
|
|
86
|
+
for (const f of missingFeatures) {
|
|
87
|
+
console.error(` [${f.priority}] ${f.feature_key}: ${f.title}`);
|
|
88
|
+
console.error(` Missing: ${f.missing_file}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const criticalCount = missingFeatures.filter(f => f.priority === 'critical').length;
|
|
92
|
+
if (criticalCount > 0) {
|
|
93
|
+
console.error(`\nFAIL: ${criticalCount} CRITICAL features are orphaned. Fix before committing.`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
} else {
|
|
96
|
+
console.warn(`\nWARN: ${missingFeatures.length} features are orphaned (non-critical). Consider updating registry.`);
|
|
97
|
+
// Non-critical orphans are warnings, not blockers
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} finally {
|
|
102
|
+
db.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
main();
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
import type Database from 'better-sqlite3';
|
|
5
|
+
import type { ToolDefinition, ToolResult } from './tools.ts';
|
|
6
|
+
import { getConfig } from './config.ts';
|
|
7
|
+
import { resolveImportPath } from './import-resolver.ts';
|
|
8
|
+
import { existsSync, readFileSync } from 'fs';
|
|
9
|
+
import { ensureWithinRoot, globToSafeRegex, safeRegex } from './security-utils.ts';
|
|
10
|
+
|
|
11
|
+
// ============================================================
|
|
12
|
+
// AI Output Validation Engine
|
|
13
|
+
// ============================================================
|
|
14
|
+
|
|
15
|
+
/** Prefix a base tool name with the configured tool prefix. */
|
|
16
|
+
function p(baseName: string): string {
|
|
17
|
+
return `${getConfig().toolPrefix}_${baseName}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ValidationCheck {
|
|
21
|
+
name: string;
|
|
22
|
+
severity: 'info' | 'warning' | 'error' | 'critical';
|
|
23
|
+
message: string;
|
|
24
|
+
line?: number;
|
|
25
|
+
file?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get validation checks from config, or defaults.
|
|
30
|
+
* Derives pattern checks from the project's configured rules.
|
|
31
|
+
*/
|
|
32
|
+
function getValidationChecks(): Record<string, boolean> {
|
|
33
|
+
return getConfig().governance?.validation?.checks ?? {
|
|
34
|
+
rule_compliance: true,
|
|
35
|
+
import_existence: true,
|
|
36
|
+
naming_conventions: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get custom validation patterns from config.
|
|
42
|
+
*/
|
|
43
|
+
function getCustomPatterns(): Array<{ pattern: string; severity: string; message: string }> {
|
|
44
|
+
return getConfig().governance?.validation?.custom_patterns ?? [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validate a file against configured rules and custom patterns.
|
|
49
|
+
*/
|
|
50
|
+
export function validateFile(
|
|
51
|
+
filePath: string,
|
|
52
|
+
projectRoot: string
|
|
53
|
+
): ValidationCheck[] {
|
|
54
|
+
const checks: ValidationCheck[] = [];
|
|
55
|
+
const config = getConfig();
|
|
56
|
+
const activeChecks = getValidationChecks();
|
|
57
|
+
const customPatterns = getCustomPatterns();
|
|
58
|
+
|
|
59
|
+
let absPath: string;
|
|
60
|
+
try {
|
|
61
|
+
absPath = ensureWithinRoot(filePath, projectRoot);
|
|
62
|
+
} catch {
|
|
63
|
+
checks.push({
|
|
64
|
+
name: 'path_traversal',
|
|
65
|
+
severity: 'critical',
|
|
66
|
+
message: `Path traversal blocked: ${filePath}`,
|
|
67
|
+
file: filePath,
|
|
68
|
+
});
|
|
69
|
+
return checks;
|
|
70
|
+
}
|
|
71
|
+
if (!existsSync(absPath)) {
|
|
72
|
+
checks.push({
|
|
73
|
+
name: 'file_exists',
|
|
74
|
+
severity: 'error',
|
|
75
|
+
message: `File not found: ${filePath}`,
|
|
76
|
+
file: filePath,
|
|
77
|
+
});
|
|
78
|
+
return checks;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const source = readFileSync(absPath, 'utf-8');
|
|
82
|
+
const lines = source.split('\n');
|
|
83
|
+
|
|
84
|
+
// Rule compliance check - uses project rules
|
|
85
|
+
if (activeChecks.rule_compliance !== false) {
|
|
86
|
+
// Check rules from config
|
|
87
|
+
for (const ruleSet of config.rules) {
|
|
88
|
+
const rulePattern = globToSafeRegex(ruleSet.pattern);
|
|
89
|
+
if (rulePattern.test(filePath)) {
|
|
90
|
+
for (const rule of ruleSet.rules) {
|
|
91
|
+
// Rules are human-readable; we can't automatically enforce all of them,
|
|
92
|
+
// but we can check for patterns that indicate violations
|
|
93
|
+
checks.push({
|
|
94
|
+
name: 'rule_applicable',
|
|
95
|
+
severity: 'info',
|
|
96
|
+
message: `Rule applies: ${rule}`,
|
|
97
|
+
file: filePath,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Import existence check
|
|
105
|
+
if (activeChecks.import_existence !== false) {
|
|
106
|
+
for (let i = 0; i < lines.length; i++) {
|
|
107
|
+
const line = lines[i];
|
|
108
|
+
const importMatch = line.match(/^\s*import\s+.*from\s+['"]([^'"]+)['"]/);
|
|
109
|
+
if (importMatch) {
|
|
110
|
+
const specifier = importMatch[1];
|
|
111
|
+
// Only check relative imports
|
|
112
|
+
if (specifier.startsWith('.') || specifier.startsWith('@/')) {
|
|
113
|
+
const resolved = resolveImportPath(specifier, filePath);
|
|
114
|
+
if (!resolved) {
|
|
115
|
+
checks.push({
|
|
116
|
+
name: 'import_hallucination',
|
|
117
|
+
severity: 'error',
|
|
118
|
+
message: `Import target does not exist: ${specifier}`,
|
|
119
|
+
line: i + 1,
|
|
120
|
+
file: filePath,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Custom patterns check - uses safeRegex to prevent ReDoS from config
|
|
129
|
+
for (const customPattern of customPatterns) {
|
|
130
|
+
const regex = safeRegex(customPattern.pattern);
|
|
131
|
+
if (!regex) {
|
|
132
|
+
checks.push({
|
|
133
|
+
name: 'config_warning',
|
|
134
|
+
severity: 'warning',
|
|
135
|
+
message: `Custom pattern rejected (invalid or unsafe regex): ${customPattern.pattern.slice(0, 50)}`,
|
|
136
|
+
file: filePath,
|
|
137
|
+
});
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
for (let i = 0; i < lines.length; i++) {
|
|
141
|
+
if (regex.test(lines[i])) {
|
|
142
|
+
checks.push({
|
|
143
|
+
name: 'custom_pattern',
|
|
144
|
+
severity: customPattern.severity as 'info' | 'warning' | 'error' | 'critical',
|
|
145
|
+
message: customPattern.message,
|
|
146
|
+
line: i + 1,
|
|
147
|
+
file: filePath,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// DB access pattern check (if configured)
|
|
154
|
+
if (config.dbAccessPattern) {
|
|
155
|
+
const wrongPattern = config.dbAccessPattern === 'ctx.db.{table}'
|
|
156
|
+
? /ctx\.prisma\./
|
|
157
|
+
: null;
|
|
158
|
+
if (wrongPattern) {
|
|
159
|
+
for (let i = 0; i < lines.length; i++) {
|
|
160
|
+
if (wrongPattern.test(lines[i])) {
|
|
161
|
+
checks.push({
|
|
162
|
+
name: 'db_access_pattern',
|
|
163
|
+
severity: 'error',
|
|
164
|
+
message: `Wrong DB access pattern. Use ${config.dbAccessPattern}`,
|
|
165
|
+
line: i + 1,
|
|
166
|
+
file: filePath,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return checks;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Store validation results.
|
|
178
|
+
*/
|
|
179
|
+
export function storeValidationResult(
|
|
180
|
+
db: Database.Database,
|
|
181
|
+
filePath: string,
|
|
182
|
+
checks: ValidationCheck[],
|
|
183
|
+
sessionId?: string,
|
|
184
|
+
validationType = 'file_validation'
|
|
185
|
+
): void {
|
|
186
|
+
const errors = checks.filter(c => c.severity === 'error' || c.severity === 'critical');
|
|
187
|
+
const warnings = checks.filter(c => c.severity === 'warning');
|
|
188
|
+
const passed = errors.length === 0;
|
|
189
|
+
const rulesViolated = [...errors, ...warnings].map(c => c.name).join(', ');
|
|
190
|
+
|
|
191
|
+
db.prepare(`
|
|
192
|
+
INSERT INTO validation_results (session_id, file_path, validation_type, passed, details, rules_violated)
|
|
193
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
194
|
+
`).run(
|
|
195
|
+
sessionId ?? null, filePath, validationType,
|
|
196
|
+
passed ? 1 : 0, JSON.stringify(checks), rulesViolated || null
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ============================================================
|
|
201
|
+
// MCP Tool Definitions & Handlers
|
|
202
|
+
// ============================================================
|
|
203
|
+
|
|
204
|
+
export function getValidationToolDefinitions(): ToolDefinition[] {
|
|
205
|
+
return [
|
|
206
|
+
{
|
|
207
|
+
name: p('validation_check'),
|
|
208
|
+
description: 'Validate a file against project rules and custom patterns. Checks import existence, rule compliance, DB access patterns, and custom patterns.',
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
properties: {
|
|
212
|
+
file: { type: 'string', description: 'File path relative to project root' },
|
|
213
|
+
session_id: { type: 'string', description: 'Session ID for tracking (optional)' },
|
|
214
|
+
},
|
|
215
|
+
required: ['file'],
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: p('validation_report'),
|
|
220
|
+
description: 'Validation summary across recent sessions. Shows error/warning trends and most-violated rules.',
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
properties: {
|
|
224
|
+
days: { type: 'number', description: 'Days to look back (default: 7)' },
|
|
225
|
+
},
|
|
226
|
+
required: [],
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const VALIDATION_BASE_NAMES = new Set(['validation_check', 'validation_report']);
|
|
233
|
+
|
|
234
|
+
export function isValidationTool(name: string): boolean {
|
|
235
|
+
const pfx = getConfig().toolPrefix + '_';
|
|
236
|
+
const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
|
|
237
|
+
return VALIDATION_BASE_NAMES.has(baseName);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function handleValidationToolCall(
|
|
241
|
+
name: string,
|
|
242
|
+
args: Record<string, unknown>,
|
|
243
|
+
memoryDb: Database.Database
|
|
244
|
+
): ToolResult {
|
|
245
|
+
try {
|
|
246
|
+
const pfx = getConfig().toolPrefix + '_';
|
|
247
|
+
const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
|
|
248
|
+
|
|
249
|
+
switch (baseName) {
|
|
250
|
+
case 'validation_check':
|
|
251
|
+
return handleValidateFile(args, memoryDb);
|
|
252
|
+
case 'validation_report':
|
|
253
|
+
return handleValidationReport(args, memoryDb);
|
|
254
|
+
default:
|
|
255
|
+
return text(`Unknown validation tool: ${name}`);
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
return text(`Error in ${name}: ${error instanceof Error ? error.message : String(error)}\n\nUsage: ${p('validation_check')} { file: "src/path/file.ts" }, ${p('validation_report')} { days: 7 }`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function handleValidateFile(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
263
|
+
const file = args.file as string;
|
|
264
|
+
if (!file) return text(`Usage: ${p('validation_check')} { file: "src/path/file.ts" } - Validate a file against project rules and custom patterns.`);
|
|
265
|
+
|
|
266
|
+
const config = getConfig();
|
|
267
|
+
const checks = validateFile(file, config.project.root);
|
|
268
|
+
|
|
269
|
+
// Store results
|
|
270
|
+
storeValidationResult(db, file, checks, args.session_id as string | undefined);
|
|
271
|
+
|
|
272
|
+
if (checks.length === 0) {
|
|
273
|
+
return text(`## Validation: ${file}\n\nNo issues found. File passes all configured validation checks.`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const errors = checks.filter(c => c.severity === 'error' || c.severity === 'critical');
|
|
277
|
+
const warnings = checks.filter(c => c.severity === 'warning');
|
|
278
|
+
const info = checks.filter(c => c.severity === 'info');
|
|
279
|
+
|
|
280
|
+
const lines = [
|
|
281
|
+
`## Validation: ${file}`,
|
|
282
|
+
`Errors: ${errors.length} | Warnings: ${warnings.length} | Info: ${info.length}`,
|
|
283
|
+
'',
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
if (errors.length > 0) {
|
|
287
|
+
lines.push('### Errors');
|
|
288
|
+
for (const check of errors) {
|
|
289
|
+
const loc = check.line ? `:${check.line}` : '';
|
|
290
|
+
lines.push(`- [${check.severity.toUpperCase()}] ${check.name}${loc}: ${check.message}`);
|
|
291
|
+
}
|
|
292
|
+
lines.push('');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (warnings.length > 0) {
|
|
296
|
+
lines.push('### Warnings');
|
|
297
|
+
for (const check of warnings) {
|
|
298
|
+
const loc = check.line ? `:${check.line}` : '';
|
|
299
|
+
lines.push(`- [WARN] ${check.name}${loc}: ${check.message}`);
|
|
300
|
+
}
|
|
301
|
+
lines.push('');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (info.length > 0) {
|
|
305
|
+
lines.push('### Info');
|
|
306
|
+
for (const check of info) {
|
|
307
|
+
lines.push(`- ${check.name}: ${check.message}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return text(lines.join('\n'));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function handleValidationReport(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
315
|
+
const days = (args.days as number) ?? 7;
|
|
316
|
+
|
|
317
|
+
const results = db.prepare(`
|
|
318
|
+
SELECT file_path, passed, rules_violated, created_at
|
|
319
|
+
FROM validation_results
|
|
320
|
+
WHERE created_at >= datetime('now', ?)
|
|
321
|
+
ORDER BY created_at DESC
|
|
322
|
+
`).all(`-${days} days`) as Array<Record<string, unknown>>;
|
|
323
|
+
|
|
324
|
+
if (results.length === 0) {
|
|
325
|
+
return text(`No validation results found in the last ${days} days. Run ${p('validation_check')} { file: "src/path/to/file.ts" } on specific files to generate validation data, or try a longer time range with { days: 90 }.`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const passedCount = results.filter(r => r.passed === 1).length;
|
|
329
|
+
const failedCount = results.filter(r => r.passed === 0).length;
|
|
330
|
+
|
|
331
|
+
const lines = [
|
|
332
|
+
`## Validation Report (${days} days)`,
|
|
333
|
+
`Files validated: ${results.length}`,
|
|
334
|
+
`Passed: ${passedCount}`,
|
|
335
|
+
`Failed: ${failedCount}`,
|
|
336
|
+
'',
|
|
337
|
+
'### Failed Validations',
|
|
338
|
+
'| File | Rules Violated | Date |',
|
|
339
|
+
'|------|----------------|------|',
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
const failedResults = results.filter(r => r.passed === 0);
|
|
343
|
+
for (const r of failedResults.slice(0, 30)) {
|
|
344
|
+
const filename = (r.file_path as string).split('/').pop();
|
|
345
|
+
const rules = (r.rules_violated as string) ?? '-';
|
|
346
|
+
lines.push(`| ${filename} | ${rules.slice(0, 60)} | ${(r.created_at as string).slice(0, 10)} |`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (failedResults.length > 30) {
|
|
350
|
+
lines.push(`... and ${failedResults.length - 30} more`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return text(lines.join('\n'));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function text(content: string): ToolResult {
|
|
357
|
+
return { content: [{ type: 'text', text: content }] };
|
|
358
|
+
}
|