@massu/core 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/LICENSE +71 -0
  2. package/dist/hooks/cost-tracker.js +127 -11493
  3. package/dist/hooks/post-edit-context.js +125 -11491
  4. package/dist/hooks/post-tool-use.js +127 -11493
  5. package/dist/hooks/pre-compact.js +127 -11493
  6. package/dist/hooks/pre-delete-check.js +126 -11492
  7. package/dist/hooks/quality-event.js +127 -11493
  8. package/dist/hooks/session-end.js +127 -11493
  9. package/dist/hooks/session-start.js +127 -11493
  10. package/dist/hooks/user-prompt.js +127 -11493
  11. package/package.json +9 -8
  12. package/src/__tests__/adr-generator.test.ts +260 -0
  13. package/src/__tests__/analytics.test.ts +282 -0
  14. package/src/__tests__/audit-trail.test.ts +382 -0
  15. package/src/__tests__/backfill-sessions.test.ts +690 -0
  16. package/src/__tests__/cli.test.ts +290 -0
  17. package/src/__tests__/cloud-sync.test.ts +261 -0
  18. package/src/__tests__/config-sections.test.ts +359 -0
  19. package/src/__tests__/config.test.ts +732 -0
  20. package/src/__tests__/cost-tracker.test.ts +348 -0
  21. package/src/__tests__/db.test.ts +177 -0
  22. package/src/__tests__/dependency-scorer.test.ts +325 -0
  23. package/src/__tests__/docs-integration.test.ts +178 -0
  24. package/src/__tests__/docs-tools.test.ts +199 -0
  25. package/src/__tests__/domains.test.ts +236 -0
  26. package/src/__tests__/hooks.test.ts +221 -0
  27. package/src/__tests__/import-resolver.test.ts +95 -0
  28. package/src/__tests__/integration/path-traversal.test.ts +134 -0
  29. package/src/__tests__/integration/pricing-consistency.test.ts +88 -0
  30. package/src/__tests__/integration/tool-registration.test.ts +146 -0
  31. package/src/__tests__/memory-db.test.ts +404 -0
  32. package/src/__tests__/memory-enhancements.test.ts +316 -0
  33. package/src/__tests__/memory-tools.test.ts +199 -0
  34. package/src/__tests__/middleware-tree.test.ts +177 -0
  35. package/src/__tests__/observability-tools.test.ts +595 -0
  36. package/src/__tests__/observability.test.ts +437 -0
  37. package/src/__tests__/observation-extractor.test.ts +167 -0
  38. package/src/__tests__/page-deps.test.ts +60 -0
  39. package/src/__tests__/prompt-analyzer.test.ts +298 -0
  40. package/src/__tests__/regression-detector.test.ts +295 -0
  41. package/src/__tests__/rules.test.ts +87 -0
  42. package/src/__tests__/schema-mapper.test.ts +29 -0
  43. package/src/__tests__/security-scorer.test.ts +238 -0
  44. package/src/__tests__/security-utils.test.ts +175 -0
  45. package/src/__tests__/sentinel-db.test.ts +491 -0
  46. package/src/__tests__/sentinel-scanner.test.ts +750 -0
  47. package/src/__tests__/sentinel-tools.test.ts +324 -0
  48. package/src/__tests__/sentinel-types.test.ts +750 -0
  49. package/src/__tests__/server.test.ts +452 -0
  50. package/src/__tests__/session-archiver.test.ts +524 -0
  51. package/src/__tests__/session-state-generator.test.ts +900 -0
  52. package/src/__tests__/team-knowledge.test.ts +327 -0
  53. package/src/__tests__/tools.test.ts +340 -0
  54. package/src/__tests__/transcript-parser.test.ts +195 -0
  55. package/src/__tests__/trpc-index.test.ts +25 -0
  56. package/src/__tests__/validate-features-runner.test.ts +517 -0
  57. package/src/__tests__/validation-engine.test.ts +300 -0
  58. package/src/adr-generator.ts +285 -0
  59. package/src/analytics.ts +367 -0
  60. package/src/audit-trail.ts +443 -0
  61. package/src/backfill-sessions.ts +180 -0
  62. package/src/cli.ts +105 -0
  63. package/src/cloud-sync.ts +194 -0
  64. package/src/commands/doctor.ts +300 -0
  65. package/src/commands/init.ts +399 -0
  66. package/src/commands/install-hooks.ts +26 -0
  67. package/src/config.ts +357 -0
  68. package/src/core-tools.ts +685 -0
  69. package/src/cost-tracker.ts +350 -0
  70. package/src/db.ts +233 -0
  71. package/src/dependency-scorer.ts +330 -0
  72. package/src/docs-map.json +100 -0
  73. package/src/docs-tools.ts +514 -0
  74. package/src/domains.ts +181 -0
  75. package/src/hooks/cost-tracker.ts +66 -0
  76. package/src/hooks/intent-suggester.ts +131 -0
  77. package/src/hooks/post-edit-context.ts +91 -0
  78. package/src/hooks/post-tool-use.ts +175 -0
  79. package/src/hooks/pre-compact.ts +146 -0
  80. package/src/hooks/pre-delete-check.ts +153 -0
  81. package/src/hooks/quality-event.ts +127 -0
  82. package/src/hooks/security-gate.ts +121 -0
  83. package/src/hooks/session-end.ts +467 -0
  84. package/src/hooks/session-start.ts +210 -0
  85. package/src/hooks/user-prompt.ts +91 -0
  86. package/src/import-resolver.ts +224 -0
  87. package/src/memory-db.ts +48 -0
  88. package/src/memory-queries.ts +804 -0
  89. package/src/memory-schema.ts +546 -0
  90. package/src/memory-tools.ts +392 -0
  91. package/src/middleware-tree.ts +70 -0
  92. package/src/observability-tools.ts +332 -0
  93. package/src/observation-extractor.ts +411 -0
  94. package/src/page-deps.ts +283 -0
  95. package/src/prompt-analyzer.ts +325 -0
  96. package/src/regression-detector.ts +313 -0
  97. package/src/rules.ts +57 -0
  98. package/src/schema-mapper.ts +232 -0
  99. package/src/security-scorer.ts +398 -0
  100. package/src/security-utils.ts +133 -0
  101. package/src/sentinel-db.ts +623 -0
  102. package/src/sentinel-scanner.ts +405 -0
  103. package/src/sentinel-tools.ts +515 -0
  104. package/src/sentinel-types.ts +140 -0
  105. package/src/server.ts +190 -0
  106. package/src/session-archiver.ts +112 -0
  107. package/src/session-state-generator.ts +174 -0
  108. package/src/team-knowledge.ts +400 -0
  109. package/src/tool-helpers.ts +41 -0
  110. package/src/tools.ts +111 -0
  111. package/src/transcript-parser.ts +458 -0
  112. package/src/trpc-index.ts +214 -0
  113. package/src/validate-features-runner.ts +107 -0
  114. package/src/validation-engine.ts +351 -0
@@ -0,0 +1,367 @@
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 './tool-helpers.ts';
6
+ import { p, text } from './tool-helpers.ts';
7
+ import { getConfig } from './config.ts';
8
+
9
+ // ============================================================
10
+ // Quality Trend Analytics
11
+ // ============================================================
12
+
13
+ export interface QualityBreakdown {
14
+ security: number;
15
+ architecture: number;
16
+ coupling: number;
17
+ tests: number;
18
+ rule_compliance: number;
19
+ [key: string]: number;
20
+ }
21
+
22
+ /** Default scoring weights. Can be overridden via config.analytics.quality.weights */
23
+ const DEFAULT_WEIGHTS: Record<string, number> = {
24
+ bug_found: -5,
25
+ vr_failure: -10,
26
+ incident: -20,
27
+ cr_violation: -3,
28
+ vr_pass: 2,
29
+ clean_commit: 5,
30
+ successful_verification: 3,
31
+ };
32
+
33
+ /** Default quality categories */
34
+ const DEFAULT_CATEGORIES = ['security', 'architecture', 'coupling', 'tests', 'rule_compliance'];
35
+
36
+ /**
37
+ * Get the scoring weights from config or defaults.
38
+ */
39
+ function getWeights(): Record<string, number> {
40
+ return getConfig().analytics?.quality?.weights ?? DEFAULT_WEIGHTS;
41
+ }
42
+
43
+ /**
44
+ * Get the quality categories from config or defaults.
45
+ */
46
+ function getCategories(): string[] {
47
+ return getConfig().analytics?.quality?.categories ?? DEFAULT_CATEGORIES;
48
+ }
49
+
50
+ /**
51
+ * Calculate a quality score from observations in a session.
52
+ * Score starts at 50 and adjusts based on weighted events.
53
+ */
54
+ export function calculateQualityScore(
55
+ db: Database.Database,
56
+ sessionId: string
57
+ ): { score: number; breakdown: QualityBreakdown } {
58
+ const weights = getWeights();
59
+ const categories = getCategories();
60
+
61
+ const observations = db.prepare(
62
+ 'SELECT type, detail FROM observations WHERE session_id = ?'
63
+ ).all(sessionId) as Array<{ type: string; detail: string }>;
64
+
65
+ let score = 50; // Base score
66
+ const breakdown: QualityBreakdown = Object.fromEntries(
67
+ categories.map(c => [c, 0])
68
+ ) as QualityBreakdown;
69
+
70
+ for (const obs of observations) {
71
+ const weight = weights[obs.type] ?? 0;
72
+ score += weight;
73
+
74
+ // Categorize observation
75
+ const desc = (obs.detail ?? '').toLowerCase();
76
+ for (const category of categories) {
77
+ if (desc.includes(category)) {
78
+ breakdown[category] += weight;
79
+ }
80
+ }
81
+ }
82
+
83
+ return {
84
+ score: Math.max(0, Math.min(100, score)),
85
+ breakdown,
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Store a quality score for a session.
91
+ */
92
+ export function storeQualityScore(
93
+ db: Database.Database,
94
+ sessionId: string,
95
+ score: number,
96
+ breakdown: QualityBreakdown
97
+ ): void {
98
+ db.prepare(`
99
+ INSERT INTO session_quality_scores
100
+ (session_id, score, security_score, architecture_score, coupling_score, test_score, rule_compliance_score)
101
+ VALUES (?, ?, ?, ?, ?, ?, ?)
102
+ `).run(
103
+ sessionId, score,
104
+ breakdown.security ?? 0,
105
+ breakdown.architecture ?? 0,
106
+ breakdown.coupling ?? 0,
107
+ breakdown.tests ?? 0,
108
+ breakdown.rule_compliance ?? 0
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Backfill quality scores for sessions that don't have them.
114
+ */
115
+ export function backfillQualityScores(db: Database.Database): number {
116
+ const sessions = db.prepare(`
117
+ SELECT DISTINCT s.session_id
118
+ FROM sessions s
119
+ LEFT JOIN session_quality_scores q ON s.session_id = q.session_id
120
+ WHERE q.session_id IS NULL
121
+ LIMIT 1000
122
+ `).all() as Array<{ session_id: string }>;
123
+
124
+ let backfilled = 0;
125
+ for (const session of sessions) {
126
+ const { score, breakdown } = calculateQualityScore(db, session.session_id);
127
+ storeQualityScore(db, session.session_id, score, breakdown);
128
+ backfilled++;
129
+ }
130
+
131
+ return backfilled;
132
+ }
133
+
134
+ // ============================================================
135
+ // MCP Tool Definitions & Handlers
136
+ // ============================================================
137
+
138
+ export function getAnalyticsToolDefinitions(): ToolDefinition[] {
139
+ return [
140
+ {
141
+ name: p('quality_score'),
142
+ description: 'Calculate and store quality score for a session based on observations. Shows breakdown by category.',
143
+ inputSchema: {
144
+ type: 'object',
145
+ properties: {
146
+ session_id: { type: 'string', description: 'Session ID to score' },
147
+ },
148
+ required: ['session_id'],
149
+ },
150
+ },
151
+ {
152
+ name: p('quality_trend'),
153
+ description: 'Quality trend over recent sessions. Shows score progression and identifies improving/declining areas.',
154
+ inputSchema: {
155
+ type: 'object',
156
+ properties: {
157
+ days: { type: 'number', description: 'Number of days to look back (default: 30)' },
158
+ },
159
+ required: [],
160
+ },
161
+ },
162
+ {
163
+ name: p('quality_report'),
164
+ description: 'Comprehensive quality report with averages, trends, and recommendations.',
165
+ inputSchema: {
166
+ type: 'object',
167
+ properties: {
168
+ days: { type: 'number', description: 'Days to cover (default: 30)' },
169
+ },
170
+ required: [],
171
+ },
172
+ },
173
+ ];
174
+ }
175
+
176
+ const ANALYTICS_BASE_NAMES = new Set(['quality_score', 'quality_trend', 'quality_report']);
177
+
178
+ export function isAnalyticsTool(name: string): boolean {
179
+ const pfx = getConfig().toolPrefix + '_';
180
+ const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
181
+ return ANALYTICS_BASE_NAMES.has(baseName);
182
+ }
183
+
184
+ export function handleAnalyticsToolCall(
185
+ name: string,
186
+ args: Record<string, unknown>,
187
+ memoryDb: Database.Database
188
+ ): ToolResult {
189
+ try {
190
+ const pfx = getConfig().toolPrefix + '_';
191
+ const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
192
+
193
+ switch (baseName) {
194
+ case 'quality_score':
195
+ return handleQualityScore(args, memoryDb);
196
+ case 'quality_trend':
197
+ return handleQualityTrend(args, memoryDb);
198
+ case 'quality_report':
199
+ return handleQualityReport(args, memoryDb);
200
+ default:
201
+ return text(`Unknown analytics tool: ${name}`);
202
+ }
203
+ } catch (error) {
204
+ return text(`Error in ${name}: ${error instanceof Error ? error.message : String(error)}\n\nUsage: ${p('quality_score')} { session_id: "..." }, ${p('quality_trend')} { days: 30 }`);
205
+ }
206
+ }
207
+
208
+ function handleQualityScore(args: Record<string, unknown>, db: Database.Database): ToolResult {
209
+ const sessionId = args.session_id as string;
210
+ if (!sessionId) return text(`Usage: ${p('quality_score')} { session_id: "abc123" } - Calculate quality score for a specific session.`);
211
+
212
+ const { score, breakdown } = calculateQualityScore(db, sessionId);
213
+ storeQualityScore(db, sessionId, score, breakdown);
214
+
215
+ const categories = getCategories();
216
+ const lines = [
217
+ `## Quality Score: ${score}/100`,
218
+ '',
219
+ '### Breakdown',
220
+ '| Category | Impact |',
221
+ '|----------|--------|',
222
+ ];
223
+
224
+ for (const category of categories) {
225
+ const impact = breakdown[category] ?? 0;
226
+ const indicator = impact > 0 ? '+' : impact < 0 ? '' : ' ';
227
+ lines.push(`| ${category} | ${indicator}${impact} |`);
228
+ }
229
+
230
+ return text(lines.join('\n'));
231
+ }
232
+
233
+ function handleQualityTrend(args: Record<string, unknown>, db: Database.Database, retried = false): ToolResult {
234
+ const days = (args.days as number) ?? 30;
235
+
236
+ const scores = db.prepare(`
237
+ SELECT session_id, score, security_score, architecture_score, coupling_score, test_score, rule_compliance_score, created_at
238
+ FROM session_quality_scores
239
+ WHERE created_at >= datetime('now', ?)
240
+ ORDER BY created_at ASC
241
+ `).all(`-${days} days`) as Array<{
242
+ session_id: string;
243
+ score: number;
244
+ created_at: string;
245
+ }>;
246
+
247
+ if (scores.length === 0) {
248
+ if (!retried) {
249
+ const backfilled = backfillQualityScores(db);
250
+ if (backfilled > 0) {
251
+ return handleQualityTrend(args, db, true);
252
+ }
253
+ }
254
+ 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 }.`);
255
+ }
256
+
257
+ const avg = scores.reduce((sum, s) => sum + s.score, 0) / scores.length;
258
+ const recent = scores.slice(-5);
259
+ const recentAvg = recent.reduce((sum, s) => sum + s.score, 0) / recent.length;
260
+
261
+ const trend = recentAvg > avg ? 'IMPROVING' : recentAvg < avg - 5 ? 'DECLINING' : 'STABLE';
262
+
263
+ const lines = [
264
+ `## Quality Trend (${days} days)`,
265
+ `Sessions scored: ${scores.length}`,
266
+ `Average: ${avg.toFixed(1)}`,
267
+ `Recent (last 5): ${recentAvg.toFixed(1)} [${trend}]`,
268
+ '',
269
+ '### Recent Scores',
270
+ '| Session | Score | Date |',
271
+ '|---------|-------|------|',
272
+ ];
273
+
274
+ for (const s of scores.slice(-10)) {
275
+ lines.push(`| ${s.session_id.slice(0, 8)}... | ${s.score} | ${s.created_at} |`);
276
+ }
277
+
278
+ return text(lines.join('\n'));
279
+ }
280
+
281
+ function handleQualityReport(args: Record<string, unknown>, db: Database.Database, retried = false): ToolResult {
282
+ const days = (args.days as number) ?? 30;
283
+
284
+ const scores = db.prepare(`
285
+ SELECT score, security_score, architecture_score, coupling_score, test_score, rule_compliance_score
286
+ FROM session_quality_scores
287
+ WHERE created_at >= datetime('now', ?)
288
+ `).all(`-${days} days`) as Array<Record<string, number>>;
289
+
290
+ if (scores.length === 0) {
291
+ if (!retried) {
292
+ const backfilled = backfillQualityScores(db);
293
+ if (backfilled > 0) {
294
+ return handleQualityReport(args, db, true);
295
+ }
296
+ }
297
+ 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 }.`);
298
+ }
299
+
300
+ const avg = scores.reduce((sum, s) => sum + s.score, 0) / scores.length;
301
+ const max = Math.max(...scores.map(s => s.score));
302
+ const min = Math.min(...scores.map(s => s.score));
303
+
304
+ const categoryColumns: Record<string, string> = {
305
+ security: 'security_score',
306
+ architecture: 'architecture_score',
307
+ coupling: 'coupling_score',
308
+ tests: 'test_score',
309
+ rule_compliance: 'rule_compliance_score',
310
+ };
311
+
312
+ const categories = getCategories();
313
+ const categoryTotals: Record<string, number> = {};
314
+ for (const category of categories) {
315
+ categoryTotals[category] = 0;
316
+ }
317
+
318
+ for (const s of scores) {
319
+ for (const category of categories) {
320
+ const col = categoryColumns[category];
321
+ if (col) {
322
+ categoryTotals[category] += s[col] ?? 0;
323
+ }
324
+ }
325
+ }
326
+
327
+ const lines = [
328
+ `## Quality Report (${days} days)`,
329
+ '',
330
+ '### Summary',
331
+ `| Metric | Value |`,
332
+ `|--------|-------|`,
333
+ `| Sessions | ${scores.length} |`,
334
+ `| Average Score | ${avg.toFixed(1)} |`,
335
+ `| Best Session | ${max} |`,
336
+ `| Worst Session | ${min} |`,
337
+ '',
338
+ '### Category Impact (Cumulative)',
339
+ '| Category | Total Impact | Avg/Session |',
340
+ '|----------|-------------|-------------|',
341
+ ];
342
+
343
+ for (const category of categories) {
344
+ const total = categoryTotals[category];
345
+ const avgCat = total / scores.length;
346
+ lines.push(`| ${category} | ${total > 0 ? '+' : ''}${total} | ${avgCat > 0 ? '+' : ''}${avgCat.toFixed(1)} |`);
347
+ }
348
+
349
+ // Recommendations
350
+ const worstCategory = categories.reduce((worst, cat) =>
351
+ categoryTotals[cat] < categoryTotals[worst] ? cat : worst, categories[0]);
352
+
353
+ lines.push('');
354
+ lines.push('### Recommendations');
355
+ if (avg < 40) {
356
+ lines.push('- **Critical**: Average quality below 40. Focus on reducing incidents and VR failures.');
357
+ }
358
+ if (categoryTotals[worstCategory] < -10) {
359
+ lines.push(`- **Focus Area**: ${worstCategory} has the most negative impact (${categoryTotals[worstCategory]}). Prioritize improvements here.`);
360
+ }
361
+ if (avg >= 70) {
362
+ lines.push('- Quality is good. Maintain current practices and focus on consistency.');
363
+ }
364
+
365
+ return text(lines.join('\n'));
366
+ }
367
+