@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
package/src/analytics.ts
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
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
|
+
|
|
8
|
+
// ============================================================
|
|
9
|
+
// Quality Trend Analytics
|
|
10
|
+
// ============================================================
|
|
11
|
+
|
|
12
|
+
/** Prefix a base tool name with the configured tool prefix. */
|
|
13
|
+
function p(baseName: string): string {
|
|
14
|
+
return `${getConfig().toolPrefix}_${baseName}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface QualityBreakdown {
|
|
18
|
+
security: number;
|
|
19
|
+
architecture: number;
|
|
20
|
+
coupling: number;
|
|
21
|
+
tests: number;
|
|
22
|
+
rule_compliance: number;
|
|
23
|
+
[key: string]: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Default scoring weights. Can be overridden via config.analytics.quality.weights */
|
|
27
|
+
const DEFAULT_WEIGHTS: Record<string, number> = {
|
|
28
|
+
bug_found: -5,
|
|
29
|
+
vr_failure: -10,
|
|
30
|
+
incident: -20,
|
|
31
|
+
cr_violation: -3,
|
|
32
|
+
vr_pass: 2,
|
|
33
|
+
clean_commit: 5,
|
|
34
|
+
successful_verification: 3,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Default quality categories */
|
|
38
|
+
const DEFAULT_CATEGORIES = ['security', 'architecture', 'coupling', 'tests', 'rule_compliance'];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the scoring weights from config or defaults.
|
|
42
|
+
*/
|
|
43
|
+
function getWeights(): Record<string, number> {
|
|
44
|
+
return getConfig().analytics?.quality?.weights ?? DEFAULT_WEIGHTS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the quality categories from config or defaults.
|
|
49
|
+
*/
|
|
50
|
+
function getCategories(): string[] {
|
|
51
|
+
return getConfig().analytics?.quality?.categories ?? DEFAULT_CATEGORIES;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Calculate a quality score from observations in a session.
|
|
56
|
+
* Score starts at 50 and adjusts based on weighted events.
|
|
57
|
+
*/
|
|
58
|
+
export function calculateQualityScore(
|
|
59
|
+
db: Database.Database,
|
|
60
|
+
sessionId: string
|
|
61
|
+
): { score: number; breakdown: QualityBreakdown } {
|
|
62
|
+
const weights = getWeights();
|
|
63
|
+
const categories = getCategories();
|
|
64
|
+
|
|
65
|
+
const observations = db.prepare(
|
|
66
|
+
'SELECT type, detail FROM observations WHERE session_id = ?'
|
|
67
|
+
).all(sessionId) as Array<{ type: string; detail: string }>;
|
|
68
|
+
|
|
69
|
+
let score = 50; // Base score
|
|
70
|
+
const breakdown: QualityBreakdown = Object.fromEntries(
|
|
71
|
+
categories.map(c => [c, 0])
|
|
72
|
+
) as QualityBreakdown;
|
|
73
|
+
|
|
74
|
+
for (const obs of observations) {
|
|
75
|
+
const weight = weights[obs.type] ?? 0;
|
|
76
|
+
score += weight;
|
|
77
|
+
|
|
78
|
+
// Categorize observation
|
|
79
|
+
const desc = (obs.detail ?? '').toLowerCase();
|
|
80
|
+
for (const category of categories) {
|
|
81
|
+
if (desc.includes(category)) {
|
|
82
|
+
breakdown[category] += weight;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
score: Math.max(0, Math.min(100, score)),
|
|
89
|
+
breakdown,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Store a quality score for a session.
|
|
95
|
+
*/
|
|
96
|
+
export function storeQualityScore(
|
|
97
|
+
db: Database.Database,
|
|
98
|
+
sessionId: string,
|
|
99
|
+
score: number,
|
|
100
|
+
breakdown: QualityBreakdown
|
|
101
|
+
): void {
|
|
102
|
+
db.prepare(`
|
|
103
|
+
INSERT INTO session_quality_scores
|
|
104
|
+
(session_id, score, security_score, architecture_score, coupling_score, test_score, rule_compliance_score)
|
|
105
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
106
|
+
`).run(
|
|
107
|
+
sessionId, score,
|
|
108
|
+
breakdown.security ?? 0,
|
|
109
|
+
breakdown.architecture ?? 0,
|
|
110
|
+
breakdown.coupling ?? 0,
|
|
111
|
+
breakdown.tests ?? 0,
|
|
112
|
+
breakdown.rule_compliance ?? 0
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Backfill quality scores for sessions that don't have them.
|
|
118
|
+
*/
|
|
119
|
+
export function backfillQualityScores(db: Database.Database): number {
|
|
120
|
+
const sessions = db.prepare(`
|
|
121
|
+
SELECT DISTINCT s.session_id
|
|
122
|
+
FROM sessions s
|
|
123
|
+
LEFT JOIN session_quality_scores q ON s.session_id = q.session_id
|
|
124
|
+
WHERE q.session_id IS NULL
|
|
125
|
+
`).all() as Array<{ session_id: string }>;
|
|
126
|
+
|
|
127
|
+
let backfilled = 0;
|
|
128
|
+
for (const session of sessions) {
|
|
129
|
+
const { score, breakdown } = calculateQualityScore(db, session.session_id);
|
|
130
|
+
storeQualityScore(db, session.session_id, score, breakdown);
|
|
131
|
+
backfilled++;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return backfilled;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================================
|
|
138
|
+
// MCP Tool Definitions & Handlers
|
|
139
|
+
// ============================================================
|
|
140
|
+
|
|
141
|
+
export function getAnalyticsToolDefinitions(): ToolDefinition[] {
|
|
142
|
+
return [
|
|
143
|
+
{
|
|
144
|
+
name: p('quality_score'),
|
|
145
|
+
description: 'Calculate and store quality score for a session based on observations. Shows breakdown by category.',
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: 'object',
|
|
148
|
+
properties: {
|
|
149
|
+
session_id: { type: 'string', description: 'Session ID to score' },
|
|
150
|
+
},
|
|
151
|
+
required: ['session_id'],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: p('quality_trend'),
|
|
156
|
+
description: 'Quality trend over recent sessions. Shows score progression and identifies improving/declining areas.',
|
|
157
|
+
inputSchema: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
days: { type: 'number', description: 'Number of days to look back (default: 30)' },
|
|
161
|
+
},
|
|
162
|
+
required: [],
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: p('quality_report'),
|
|
167
|
+
description: 'Comprehensive quality report with averages, trends, and recommendations.',
|
|
168
|
+
inputSchema: {
|
|
169
|
+
type: 'object',
|
|
170
|
+
properties: {
|
|
171
|
+
days: { type: 'number', description: 'Days to cover (default: 30)' },
|
|
172
|
+
},
|
|
173
|
+
required: [],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const ANALYTICS_BASE_NAMES = new Set(['quality_score', 'quality_trend', 'quality_report']);
|
|
180
|
+
|
|
181
|
+
export function isAnalyticsTool(name: string): boolean {
|
|
182
|
+
const pfx = getConfig().toolPrefix + '_';
|
|
183
|
+
const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
|
|
184
|
+
return ANALYTICS_BASE_NAMES.has(baseName);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function handleAnalyticsToolCall(
|
|
188
|
+
name: string,
|
|
189
|
+
args: Record<string, unknown>,
|
|
190
|
+
memoryDb: Database.Database
|
|
191
|
+
): ToolResult {
|
|
192
|
+
try {
|
|
193
|
+
const pfx = getConfig().toolPrefix + '_';
|
|
194
|
+
const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
|
|
195
|
+
|
|
196
|
+
switch (baseName) {
|
|
197
|
+
case 'quality_score':
|
|
198
|
+
return handleQualityScore(args, memoryDb);
|
|
199
|
+
case 'quality_trend':
|
|
200
|
+
return handleQualityTrend(args, memoryDb);
|
|
201
|
+
case 'quality_report':
|
|
202
|
+
return handleQualityReport(args, memoryDb);
|
|
203
|
+
default:
|
|
204
|
+
return text(`Unknown analytics tool: ${name}`);
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
return text(`Error in ${name}: ${error instanceof Error ? error.message : String(error)}\n\nUsage: ${p('quality_score')} { session_id: "..." }, ${p('quality_trend')} { days: 30 }`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function handleQualityScore(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
212
|
+
const sessionId = args.session_id as string;
|
|
213
|
+
if (!sessionId) return text(`Usage: ${p('quality_score')} { session_id: "abc123" } - Calculate quality score for a specific session.`);
|
|
214
|
+
|
|
215
|
+
const { score, breakdown } = calculateQualityScore(db, sessionId);
|
|
216
|
+
storeQualityScore(db, sessionId, score, breakdown);
|
|
217
|
+
|
|
218
|
+
const categories = getCategories();
|
|
219
|
+
const lines = [
|
|
220
|
+
`## Quality Score: ${score}/100`,
|
|
221
|
+
'',
|
|
222
|
+
'### Breakdown',
|
|
223
|
+
'| Category | Impact |',
|
|
224
|
+
'|----------|--------|',
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
for (const category of categories) {
|
|
228
|
+
const impact = breakdown[category] ?? 0;
|
|
229
|
+
const indicator = impact > 0 ? '+' : impact < 0 ? '' : ' ';
|
|
230
|
+
lines.push(`| ${category} | ${indicator}${impact} |`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return text(lines.join('\n'));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function handleQualityTrend(args: Record<string, unknown>, db: Database.Database, retried = false): ToolResult {
|
|
237
|
+
const days = (args.days as number) ?? 30;
|
|
238
|
+
|
|
239
|
+
const scores = db.prepare(`
|
|
240
|
+
SELECT session_id, score, security_score, architecture_score, coupling_score, test_score, rule_compliance_score, created_at
|
|
241
|
+
FROM session_quality_scores
|
|
242
|
+
WHERE created_at >= datetime('now', ?)
|
|
243
|
+
ORDER BY created_at ASC
|
|
244
|
+
`).all(`-${days} days`) as Array<{
|
|
245
|
+
session_id: string;
|
|
246
|
+
score: number;
|
|
247
|
+
created_at: string;
|
|
248
|
+
}>;
|
|
249
|
+
|
|
250
|
+
if (scores.length === 0) {
|
|
251
|
+
if (!retried) {
|
|
252
|
+
const backfilled = backfillQualityScores(db);
|
|
253
|
+
if (backfilled > 0) {
|
|
254
|
+
return handleQualityTrend(args, db, true);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return text(`No quality scores found in the last ${days} days. Quality scores are calculated automatically from session observations (bugfixes, VR checks, incidents). Try: ${p('quality_score')} { session_id: "..." } to score a specific session, or try a longer time range with { days: 90 }.`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const avg = scores.reduce((sum, s) => sum + s.score, 0) / scores.length;
|
|
261
|
+
const recent = scores.slice(-5);
|
|
262
|
+
const recentAvg = recent.reduce((sum, s) => sum + s.score, 0) / recent.length;
|
|
263
|
+
|
|
264
|
+
const trend = recentAvg > avg ? 'IMPROVING' : recentAvg < avg - 5 ? 'DECLINING' : 'STABLE';
|
|
265
|
+
|
|
266
|
+
const lines = [
|
|
267
|
+
`## Quality Trend (${days} days)`,
|
|
268
|
+
`Sessions scored: ${scores.length}`,
|
|
269
|
+
`Average: ${avg.toFixed(1)}`,
|
|
270
|
+
`Recent (last 5): ${recentAvg.toFixed(1)} [${trend}]`,
|
|
271
|
+
'',
|
|
272
|
+
'### Recent Scores',
|
|
273
|
+
'| Session | Score | Date |',
|
|
274
|
+
'|---------|-------|------|',
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
for (const s of scores.slice(-10)) {
|
|
278
|
+
lines.push(`| ${s.session_id.slice(0, 8)}... | ${s.score} | ${s.created_at} |`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return text(lines.join('\n'));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function handleQualityReport(args: Record<string, unknown>, db: Database.Database, retried = false): ToolResult {
|
|
285
|
+
const days = (args.days as number) ?? 30;
|
|
286
|
+
|
|
287
|
+
const scores = db.prepare(`
|
|
288
|
+
SELECT score, security_score, architecture_score, coupling_score, test_score, rule_compliance_score
|
|
289
|
+
FROM session_quality_scores
|
|
290
|
+
WHERE created_at >= datetime('now', ?)
|
|
291
|
+
`).all(`-${days} days`) as Array<Record<string, number>>;
|
|
292
|
+
|
|
293
|
+
if (scores.length === 0) {
|
|
294
|
+
if (!retried) {
|
|
295
|
+
const backfilled = backfillQualityScores(db);
|
|
296
|
+
if (backfilled > 0) {
|
|
297
|
+
return handleQualityReport(args, db, true);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return text(`No quality data available for the last ${days} days. Quality scores are calculated from session observations (bugfixes, VR checks, incidents). Try: ${p('quality_score')} { session_id: "..." } to score individual sessions first, or try a longer time range with { days: 90 }.`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const avg = scores.reduce((sum, s) => sum + s.score, 0) / scores.length;
|
|
304
|
+
const max = Math.max(...scores.map(s => s.score));
|
|
305
|
+
const min = Math.min(...scores.map(s => s.score));
|
|
306
|
+
|
|
307
|
+
const categoryColumns: Record<string, string> = {
|
|
308
|
+
security: 'security_score',
|
|
309
|
+
architecture: 'architecture_score',
|
|
310
|
+
coupling: 'coupling_score',
|
|
311
|
+
tests: 'test_score',
|
|
312
|
+
rule_compliance: 'rule_compliance_score',
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const categories = getCategories();
|
|
316
|
+
const categoryTotals: Record<string, number> = {};
|
|
317
|
+
for (const category of categories) {
|
|
318
|
+
categoryTotals[category] = 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
for (const s of scores) {
|
|
322
|
+
for (const category of categories) {
|
|
323
|
+
const col = categoryColumns[category];
|
|
324
|
+
if (col) {
|
|
325
|
+
categoryTotals[category] += s[col] ?? 0;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const lines = [
|
|
331
|
+
`## Quality Report (${days} days)`,
|
|
332
|
+
'',
|
|
333
|
+
'### Summary',
|
|
334
|
+
`| Metric | Value |`,
|
|
335
|
+
`|--------|-------|`,
|
|
336
|
+
`| Sessions | ${scores.length} |`,
|
|
337
|
+
`| Average Score | ${avg.toFixed(1)} |`,
|
|
338
|
+
`| Best Session | ${max} |`,
|
|
339
|
+
`| Worst Session | ${min} |`,
|
|
340
|
+
'',
|
|
341
|
+
'### Category Impact (Cumulative)',
|
|
342
|
+
'| Category | Total Impact | Avg/Session |',
|
|
343
|
+
'|----------|-------------|-------------|',
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
for (const category of categories) {
|
|
347
|
+
const total = categoryTotals[category];
|
|
348
|
+
const avgCat = total / scores.length;
|
|
349
|
+
lines.push(`| ${category} | ${total > 0 ? '+' : ''}${total} | ${avgCat > 0 ? '+' : ''}${avgCat.toFixed(1)} |`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Recommendations
|
|
353
|
+
const worstCategory = categories.reduce((worst, cat) =>
|
|
354
|
+
categoryTotals[cat] < categoryTotals[worst] ? cat : worst, categories[0]);
|
|
355
|
+
|
|
356
|
+
lines.push('');
|
|
357
|
+
lines.push('### Recommendations');
|
|
358
|
+
if (avg < 40) {
|
|
359
|
+
lines.push('- **Critical**: Average quality below 40. Focus on reducing incidents and VR failures.');
|
|
360
|
+
}
|
|
361
|
+
if (categoryTotals[worstCategory] < -10) {
|
|
362
|
+
lines.push(`- **Focus Area**: ${worstCategory} has the most negative impact (${categoryTotals[worstCategory]}). Prioritize improvements here.`);
|
|
363
|
+
}
|
|
364
|
+
if (avg >= 70) {
|
|
365
|
+
lines.push('- Quality is good. Maintain current practices and focus on consistency.');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return text(lines.join('\n'));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function text(content: string): ToolResult {
|
|
372
|
+
return { content: [{ type: 'text', text: content }] };
|
|
373
|
+
}
|