@massu/core 1.10.8 → 1.12.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.
- package/CHANGELOG.md +1334 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +581 -448
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +67 -19
- package/dist/hooks/pre-compact.js +28 -3
- package/dist/hooks/pre-delete-check.js +53 -27
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +31 -6
- package/dist/hooks/session-start.js +8709 -8459
- package/dist/hooks/user-prompt.js +25 -1
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/framework-supports.ts +0 -85
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1206
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- package/src/watch/state.ts +0 -178
|
@@ -1,319 +0,0 @@
|
|
|
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
|
-
// Regression Detection
|
|
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
|
-
/** Default health thresholds. Configurable via regression.health_thresholds */
|
|
18
|
-
const DEFAULT_HEALTH_THRESHOLDS = {
|
|
19
|
-
healthy: 80,
|
|
20
|
-
warning: 50,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Get health thresholds from config or defaults.
|
|
25
|
-
*/
|
|
26
|
-
function getHealthThresholds(): { healthy: number; warning: number } {
|
|
27
|
-
const configured = getConfig().regression?.health_thresholds;
|
|
28
|
-
return {
|
|
29
|
-
healthy: configured?.healthy ?? DEFAULT_HEALTH_THRESHOLDS.healthy,
|
|
30
|
-
warning: configured?.warning ?? DEFAULT_HEALTH_THRESHOLDS.warning,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Calculate feature health score based on modification/test gaps.
|
|
36
|
-
* 0 = critical, 100 = healthy.
|
|
37
|
-
*/
|
|
38
|
-
export function calculateHealthScore(
|
|
39
|
-
testsPassing: number,
|
|
40
|
-
testsFailing: number,
|
|
41
|
-
modificationsSinceTest: number,
|
|
42
|
-
lastTested: string | null,
|
|
43
|
-
lastModified: string | null
|
|
44
|
-
): number {
|
|
45
|
-
let score = 100;
|
|
46
|
-
|
|
47
|
-
// Test failures
|
|
48
|
-
if (testsFailing > 0) {
|
|
49
|
-
score -= Math.min(40, testsFailing * 10);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Modifications since last test
|
|
53
|
-
if (modificationsSinceTest > 0) {
|
|
54
|
-
score -= Math.min(30, modificationsSinceTest * 5);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Time gap between modification and test
|
|
58
|
-
if (lastModified && lastTested) {
|
|
59
|
-
const modDate = new Date(lastModified).getTime();
|
|
60
|
-
const testDate = new Date(lastTested).getTime();
|
|
61
|
-
if (modDate > testDate) {
|
|
62
|
-
const daysSinceTest = (modDate - testDate) / (1000 * 60 * 60 * 24);
|
|
63
|
-
score -= Math.min(20, Math.floor(daysSinceTest * 2));
|
|
64
|
-
}
|
|
65
|
-
} else if (lastModified && !lastTested) {
|
|
66
|
-
// Modified but never tested
|
|
67
|
-
score -= 30;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return Math.max(0, score);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Update feature health when a file is modified.
|
|
75
|
-
*/
|
|
76
|
-
export function trackModification(
|
|
77
|
-
db: Database.Database,
|
|
78
|
-
featureKey: string
|
|
79
|
-
): void {
|
|
80
|
-
const existing = db.prepare(
|
|
81
|
-
'SELECT * FROM feature_health WHERE feature_key = ?'
|
|
82
|
-
).get(featureKey) as Record<string, unknown> | undefined;
|
|
83
|
-
|
|
84
|
-
if (existing) {
|
|
85
|
-
db.prepare(`
|
|
86
|
-
UPDATE feature_health
|
|
87
|
-
SET last_modified = datetime('now'),
|
|
88
|
-
modifications_since_test = modifications_since_test + 1,
|
|
89
|
-
health_score = ?
|
|
90
|
-
WHERE feature_key = ?
|
|
91
|
-
`).run(
|
|
92
|
-
calculateHealthScore(
|
|
93
|
-
(existing.tests_passing as number) ?? 0,
|
|
94
|
-
(existing.tests_failing as number) ?? 0,
|
|
95
|
-
((existing.modifications_since_test as number) ?? 0) + 1,
|
|
96
|
-
existing.last_tested as string | null,
|
|
97
|
-
new Date().toISOString()
|
|
98
|
-
),
|
|
99
|
-
featureKey
|
|
100
|
-
);
|
|
101
|
-
} else {
|
|
102
|
-
db.prepare(`
|
|
103
|
-
INSERT INTO feature_health
|
|
104
|
-
(feature_key, last_modified, modifications_since_test, health_score, tests_passing, tests_failing)
|
|
105
|
-
VALUES (?, datetime('now'), 1, 70, 0, 0)
|
|
106
|
-
`).run(featureKey);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Record test results for a feature.
|
|
112
|
-
*/
|
|
113
|
-
export function recordTestResult(
|
|
114
|
-
db: Database.Database,
|
|
115
|
-
featureKey: string,
|
|
116
|
-
passing: number,
|
|
117
|
-
failing: number
|
|
118
|
-
): void {
|
|
119
|
-
const existing = db.prepare(
|
|
120
|
-
'SELECT * FROM feature_health WHERE feature_key = ?'
|
|
121
|
-
).get(featureKey) as Record<string, unknown> | undefined;
|
|
122
|
-
|
|
123
|
-
const healthScore = calculateHealthScore(passing, failing, 0, new Date().toISOString(), existing?.last_modified as string | null);
|
|
124
|
-
|
|
125
|
-
db.prepare(`
|
|
126
|
-
INSERT INTO feature_health
|
|
127
|
-
(feature_key, last_tested, test_coverage_pct, health_score, tests_passing, tests_failing, modifications_since_test)
|
|
128
|
-
VALUES (?, datetime('now'), ?, ?, ?, ?, 0)
|
|
129
|
-
ON CONFLICT(feature_key) DO UPDATE SET
|
|
130
|
-
last_tested = datetime('now'),
|
|
131
|
-
health_score = ?,
|
|
132
|
-
tests_passing = ?,
|
|
133
|
-
tests_failing = ?,
|
|
134
|
-
modifications_since_test = 0
|
|
135
|
-
`).run(
|
|
136
|
-
featureKey, passing > 0 ? (passing / (passing + failing)) * 100 : 0,
|
|
137
|
-
healthScore, passing, failing,
|
|
138
|
-
healthScore, passing, failing
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Build alerts for unhealthy features.
|
|
144
|
-
*/
|
|
145
|
-
function buildAlerts(feature: Record<string, unknown>): string[] {
|
|
146
|
-
const alerts: string[] = [];
|
|
147
|
-
|
|
148
|
-
if ((feature.tests_failing as number) > 0) {
|
|
149
|
-
alerts.push(`${feature.tests_failing} tests failing`);
|
|
150
|
-
}
|
|
151
|
-
if ((feature.modifications_since_test as number) > 3) {
|
|
152
|
-
alerts.push(`${feature.modifications_since_test} modifications since last test`);
|
|
153
|
-
}
|
|
154
|
-
if (!feature.last_tested && feature.last_modified) {
|
|
155
|
-
alerts.push('Never tested');
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return alerts;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ============================================================
|
|
162
|
-
// MCP Tool Definitions & Handlers
|
|
163
|
-
// ============================================================
|
|
164
|
-
|
|
165
|
-
export function getRegressionToolDefinitions(): ToolDefinition[] {
|
|
166
|
-
return [
|
|
167
|
-
{
|
|
168
|
-
name: p('feature_health'),
|
|
169
|
-
description: 'Feature health dashboard. Shows health scores, modification/test gaps, and alerts for registered features.',
|
|
170
|
-
inputSchema: {
|
|
171
|
-
type: 'object',
|
|
172
|
-
properties: {
|
|
173
|
-
unhealthy_only: { type: 'boolean', description: 'Show only features with health below warning threshold (default: false)' },
|
|
174
|
-
},
|
|
175
|
-
required: [],
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
name: p('regression_risk'),
|
|
180
|
-
description: 'Check if recent changes risk regression. Shows affected features, test coverage status, and risk assessment.',
|
|
181
|
-
inputSchema: {
|
|
182
|
-
type: 'object',
|
|
183
|
-
properties: {},
|
|
184
|
-
required: [],
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
];
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const REGRESSION_BASE_NAMES = new Set(['feature_health', 'regression_risk']);
|
|
191
|
-
|
|
192
|
-
export function isRegressionTool(name: string): boolean {
|
|
193
|
-
const pfx = getConfig().toolPrefix + '_';
|
|
194
|
-
const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
|
|
195
|
-
return REGRESSION_BASE_NAMES.has(baseName);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function handleRegressionToolCall(
|
|
199
|
-
name: string,
|
|
200
|
-
args: Record<string, unknown>,
|
|
201
|
-
memoryDb: Database.Database
|
|
202
|
-
): ToolResult {
|
|
203
|
-
try {
|
|
204
|
-
const pfx = getConfig().toolPrefix + '_';
|
|
205
|
-
const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
|
|
206
|
-
|
|
207
|
-
switch (baseName) {
|
|
208
|
-
case 'feature_health':
|
|
209
|
-
return handleFeatureHealth(args, memoryDb);
|
|
210
|
-
case 'regression_risk':
|
|
211
|
-
return handleRegressionCheck(args, memoryDb);
|
|
212
|
-
default:
|
|
213
|
-
return text(`Unknown regression tool: ${name}`);
|
|
214
|
-
}
|
|
215
|
-
} catch (error) {
|
|
216
|
-
return text(`Error in ${name}: ${error instanceof Error ? error.message : String(error)}\n\nUsage: ${p('feature_health')} { unhealthy_only: true }, ${p('regression_risk')} {}`);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function handleFeatureHealth(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
221
|
-
const unhealthyOnly = args.unhealthy_only as boolean | undefined;
|
|
222
|
-
const thresholds = getHealthThresholds();
|
|
223
|
-
|
|
224
|
-
let sql = 'SELECT * FROM feature_health';
|
|
225
|
-
const params: (string | number)[] = [];
|
|
226
|
-
|
|
227
|
-
if (unhealthyOnly) {
|
|
228
|
-
sql += ' WHERE health_score < ?';
|
|
229
|
-
params.push(thresholds.healthy);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
sql += ' ORDER BY health_score ASC';
|
|
233
|
-
|
|
234
|
-
const features = db.prepare(sql).all(...params) as Array<Record<string, unknown>>;
|
|
235
|
-
|
|
236
|
-
if (features.length === 0) {
|
|
237
|
-
const filterMsg = unhealthyOnly
|
|
238
|
-
? `No unhealthy features found (threshold: ${thresholds.healthy}). All tracked features are currently healthy. Use ${p('feature_health')} {} without filters to see all features.`
|
|
239
|
-
: `No feature health data available yet. Feature health is tracked automatically when files in registered features are modified and tested. Try: ${p('regression_risk')} {} to check for untested modifications.`;
|
|
240
|
-
return text(filterMsg);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const lines = [
|
|
244
|
-
`## Feature Health Dashboard`,
|
|
245
|
-
`Features tracked: ${features.length}`,
|
|
246
|
-
'',
|
|
247
|
-
'| Feature | Health | Tests P/F | Mods Since Test | Alerts |',
|
|
248
|
-
'|---------|--------|-----------|-----------------|--------|',
|
|
249
|
-
];
|
|
250
|
-
|
|
251
|
-
for (const f of features) {
|
|
252
|
-
const alerts = buildAlerts(f);
|
|
253
|
-
const healthScore = f.health_score as number;
|
|
254
|
-
const healthIndicator = healthScore >= thresholds.healthy ? 'OK'
|
|
255
|
-
: healthScore >= thresholds.warning ? 'WARN'
|
|
256
|
-
: 'CRIT';
|
|
257
|
-
|
|
258
|
-
lines.push(
|
|
259
|
-
`| ${f.feature_key} | ${healthScore} [${healthIndicator}] | ${f.tests_passing ?? 0}/${f.tests_failing ?? 0} | ${f.modifications_since_test ?? 0} | ${alerts.join('; ') || '-'} |`
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return text(lines.join('\n'));
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function handleRegressionCheck(_args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
267
|
-
const thresholds = getHealthThresholds();
|
|
268
|
-
|
|
269
|
-
const recentlyModified = db.prepare(`
|
|
270
|
-
SELECT feature_key, health_score, modifications_since_test, tests_failing, last_modified, last_tested
|
|
271
|
-
FROM feature_health
|
|
272
|
-
WHERE modifications_since_test > 0
|
|
273
|
-
ORDER BY modifications_since_test DESC
|
|
274
|
-
`).all() as Array<Record<string, unknown>>;
|
|
275
|
-
|
|
276
|
-
if (recentlyModified.length === 0) {
|
|
277
|
-
return text(`No features have been modified since their last test run. Low regression risk. Use ${p('feature_health')} {} to see the full feature health dashboard.`);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const highRisk = recentlyModified.filter(f => (f.health_score as number) < thresholds.warning);
|
|
281
|
-
const mediumRisk = recentlyModified.filter(f => (f.health_score as number) >= thresholds.warning && (f.health_score as number) < thresholds.healthy);
|
|
282
|
-
const lowRisk = recentlyModified.filter(f => (f.health_score as number) >= thresholds.healthy);
|
|
283
|
-
|
|
284
|
-
const lines = [
|
|
285
|
-
`## Regression Risk Assessment`,
|
|
286
|
-
`Features with untested modifications: ${recentlyModified.length}`,
|
|
287
|
-
`High risk: ${highRisk.length} | Medium: ${mediumRisk.length} | Low: ${lowRisk.length}`,
|
|
288
|
-
'',
|
|
289
|
-
];
|
|
290
|
-
|
|
291
|
-
if (highRisk.length > 0) {
|
|
292
|
-
lines.push('### HIGH RISK (test immediately)');
|
|
293
|
-
for (const f of highRisk) {
|
|
294
|
-
lines.push(`- **${f.feature_key}** (health: ${f.health_score}, ${f.modifications_since_test} untested modifications)`);
|
|
295
|
-
}
|
|
296
|
-
lines.push('');
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (mediumRisk.length > 0) {
|
|
300
|
-
lines.push('### Medium Risk');
|
|
301
|
-
for (const f of mediumRisk) {
|
|
302
|
-
lines.push(`- ${f.feature_key} (health: ${f.health_score}, ${f.modifications_since_test} untested modifications)`);
|
|
303
|
-
}
|
|
304
|
-
lines.push('');
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (lowRisk.length > 0) {
|
|
308
|
-
lines.push('### Low Risk');
|
|
309
|
-
for (const f of lowRisk) {
|
|
310
|
-
lines.push(`- ${f.feature_key} (health: ${f.health_score})`);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return text(lines.join('\n'));
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function text(content: string): ToolResult {
|
|
318
|
-
return { content: [{ type: 'text', text: content }] };
|
|
319
|
-
}
|
package/src/rules.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
import { getConfig } from './config.ts';
|
|
5
|
-
|
|
6
|
-
export interface PatternRule {
|
|
7
|
-
/** Glob pattern to match file paths against */
|
|
8
|
-
match: string;
|
|
9
|
-
/** List of rules that apply to matched files */
|
|
10
|
-
rules: string[];
|
|
11
|
-
/** Severity: CRITICAL rules are schema mismatches or Edge Runtime violations */
|
|
12
|
-
severity?: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
|
|
13
|
-
/** Pattern file to reference for details */
|
|
14
|
-
patternFile?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Get pattern rules from config.
|
|
19
|
-
* Converts the config format (pattern/rules) to the internal PatternRule format.
|
|
20
|
-
*/
|
|
21
|
-
function getPatternRules(): PatternRule[] {
|
|
22
|
-
return getConfig().rules.map((r) => ({
|
|
23
|
-
match: r.pattern,
|
|
24
|
-
rules: r.rules,
|
|
25
|
-
}));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Match a file path against all pattern rules and return applicable rules.
|
|
30
|
-
*/
|
|
31
|
-
export function matchRules(filePath: string): PatternRule[] {
|
|
32
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
33
|
-
const rules = getPatternRules();
|
|
34
|
-
return rules.filter((rule) => globMatch(normalized, rule.match));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Simple glob matching for pattern rules.
|
|
39
|
-
* Supports **, *, and ? wildcards.
|
|
40
|
-
*/
|
|
41
|
-
export function globMatch(filePath: string, pattern: string): boolean {
|
|
42
|
-
// Convert glob to regex using placeholders to avoid replacement conflicts
|
|
43
|
-
let regexStr = pattern
|
|
44
|
-
.replace(/\*\*\//g, '\0GLOBSTARSLASH\0') // **/ placeholder
|
|
45
|
-
.replace(/\*\*/g, '\0GLOBSTAR\0') // ** placeholder
|
|
46
|
-
.replace(/\*/g, '\0STAR\0') // * placeholder
|
|
47
|
-
.replace(/\?/g, '\0QUESTION\0') // ? placeholder
|
|
48
|
-
.replace(/\./g, '\\.') // escape dots
|
|
49
|
-
.replace(/\0GLOBSTARSLASH\0/g, '(?:.*/)?') // **/ = zero or more directories
|
|
50
|
-
.replace(/\0GLOBSTAR\0/g, '.*') // ** = anything
|
|
51
|
-
.replace(/\0STAR\0/g, '[^/]*') // * = non-slash chars
|
|
52
|
-
.replace(/\0QUESTION\0/g, '.'); // ? = single char
|
|
53
|
-
|
|
54
|
-
// Anchor pattern
|
|
55
|
-
const regex = new RegExp(`(^|/)${regexStr}($|/)`);
|
|
56
|
-
return regex.test(filePath);
|
|
57
|
-
}
|
package/src/schema-mapper.ts
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
5
|
-
import { resolve, join } from 'path';
|
|
6
|
-
import { getConfig, getResolvedPaths, getProjectRoot } from './config.ts';
|
|
7
|
-
|
|
8
|
-
export interface SchemaModel {
|
|
9
|
-
name: string;
|
|
10
|
-
tableName: string;
|
|
11
|
-
fields: SchemaField[];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface SchemaField {
|
|
15
|
-
name: string;
|
|
16
|
-
type: string;
|
|
17
|
-
nullable: boolean;
|
|
18
|
-
isRelation: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface ColumnUsage {
|
|
22
|
-
file: string;
|
|
23
|
-
line: number;
|
|
24
|
-
usage: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface SchemaMismatch {
|
|
28
|
-
table: string;
|
|
29
|
-
codeColumn: string;
|
|
30
|
-
actualColumns: string[];
|
|
31
|
-
files: string[];
|
|
32
|
-
severity: 'CRITICAL' | 'HIGH';
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Parse the Prisma schema file and extract all models with their fields.
|
|
37
|
-
*/
|
|
38
|
-
export function parsePrismaSchema(): SchemaModel[] {
|
|
39
|
-
const schemaPath = getResolvedPaths().prismaSchemaPath;
|
|
40
|
-
if (!existsSync(schemaPath)) {
|
|
41
|
-
throw new Error(`Prisma schema not found at ${schemaPath}`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const source = readFileSync(schemaPath, 'utf-8');
|
|
45
|
-
const models: SchemaModel[] = [];
|
|
46
|
-
const sourceLines = source.split('\n');
|
|
47
|
-
|
|
48
|
-
// Parse models by tracking brace depth instead of regex
|
|
49
|
-
let i = 0;
|
|
50
|
-
while (i < sourceLines.length) {
|
|
51
|
-
const line = sourceLines[i].trim();
|
|
52
|
-
const modelMatch = line.match(/^model\s+(\w+)\s*\{/);
|
|
53
|
-
|
|
54
|
-
if (modelMatch) {
|
|
55
|
-
const modelName = modelMatch[1];
|
|
56
|
-
const fields: SchemaField[] = [];
|
|
57
|
-
let braceDepth = 1;
|
|
58
|
-
i++;
|
|
59
|
-
|
|
60
|
-
const bodyLines: string[] = [];
|
|
61
|
-
while (i < sourceLines.length && braceDepth > 0) {
|
|
62
|
-
const bodyLine = sourceLines[i];
|
|
63
|
-
for (const ch of bodyLine) {
|
|
64
|
-
if (ch === '{') braceDepth++;
|
|
65
|
-
if (ch === '}') braceDepth--;
|
|
66
|
-
}
|
|
67
|
-
if (braceDepth > 0) {
|
|
68
|
-
bodyLines.push(bodyLine);
|
|
69
|
-
}
|
|
70
|
-
i++;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
for (const bodyLine of bodyLines) {
|
|
74
|
-
const trimmed = bodyLine.trim();
|
|
75
|
-
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) continue;
|
|
76
|
-
|
|
77
|
-
// Parse field: fieldName Type? @annotations
|
|
78
|
-
// Must handle large whitespace padding in the schema
|
|
79
|
-
const fieldMatch = trimmed.match(/^(\w+)\s+(\w+)(\?)?(\[\])?\s*(.*)?$/);
|
|
80
|
-
if (fieldMatch) {
|
|
81
|
-
const fieldName = fieldMatch[1];
|
|
82
|
-
const fieldType = fieldMatch[2];
|
|
83
|
-
const nullable = !!fieldMatch[3];
|
|
84
|
-
const annotations = fieldMatch[5] || '';
|
|
85
|
-
|
|
86
|
-
// Skip @relation fields (they're virtual)
|
|
87
|
-
const isRelation = annotations.includes('@relation') || fieldType[0] === fieldType[0].toUpperCase() && !['String', 'Int', 'Float', 'Boolean', 'DateTime', 'Json', 'Decimal', 'BigInt', 'Bytes'].includes(fieldType);
|
|
88
|
-
|
|
89
|
-
fields.push({
|
|
90
|
-
name: fieldName,
|
|
91
|
-
type: fieldType + (fieldMatch[4] || '') + (nullable ? '?' : ''),
|
|
92
|
-
nullable,
|
|
93
|
-
isRelation,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Derive table name from @@map or use model name directly (Prisma convention)
|
|
99
|
-
const body = bodyLines.join('\n');
|
|
100
|
-
const mapMatch = body.match(/@@map\("([^"]+)"\)/);
|
|
101
|
-
const tableName = mapMatch ? mapMatch[1] : toSnakeCase(modelName);
|
|
102
|
-
|
|
103
|
-
models.push({ name: modelName, tableName, fields });
|
|
104
|
-
} else {
|
|
105
|
-
i++;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return models;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function toSnakeCase(str: string): string {
|
|
113
|
-
return str
|
|
114
|
-
.replace(/([A-Z])/g, '_$1')
|
|
115
|
-
.toLowerCase()
|
|
116
|
-
.replace(/^_/, '');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Find all references to a table's columns in router files.
|
|
121
|
-
*/
|
|
122
|
-
export function findColumnUsageInRouters(tableName: string): Map<string, ColumnUsage[]> {
|
|
123
|
-
const usage = new Map<string, ColumnUsage[]>();
|
|
124
|
-
const routersDir = getResolvedPaths().routersDir;
|
|
125
|
-
|
|
126
|
-
if (!existsSync(routersDir)) return usage;
|
|
127
|
-
|
|
128
|
-
scanDirectory(routersDir, tableName, usage);
|
|
129
|
-
return usage;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function scanDirectory(dir: string, tableName: string, usage: Map<string, ColumnUsage[]>): void {
|
|
133
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
134
|
-
for (const entry of entries) {
|
|
135
|
-
const fullPath = join(dir, entry.name);
|
|
136
|
-
if (entry.isDirectory()) {
|
|
137
|
-
scanDirectory(fullPath, tableName, usage);
|
|
138
|
-
} else if (entry.name.endsWith('.ts')) {
|
|
139
|
-
scanFile(fullPath, tableName, usage);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function scanFile(absPath: string, tableName: string, usage: Map<string, ColumnUsage[]>): void {
|
|
145
|
-
try {
|
|
146
|
-
const source = readFileSync(absPath, 'utf-8');
|
|
147
|
-
|
|
148
|
-
// Check if this file references the table
|
|
149
|
-
if (!source.includes(tableName)) return;
|
|
150
|
-
|
|
151
|
-
const relPath = absPath.slice(getProjectRoot().length + 1);
|
|
152
|
-
const lines = source.split('\n');
|
|
153
|
-
|
|
154
|
-
for (let i = 0; i < lines.length; i++) {
|
|
155
|
-
const line = lines[i];
|
|
156
|
-
|
|
157
|
-
// Look for property access patterns: tableName.columnName or { columnName: ... } near table references
|
|
158
|
-
// Pattern 1: where: { columnName: value }
|
|
159
|
-
const whereMatch = line.match(/(\w+)\s*:\s*(?:\{|[^,}]+)/g);
|
|
160
|
-
if (whereMatch) {
|
|
161
|
-
for (const m of whereMatch) {
|
|
162
|
-
const colName = m.split(':')[0].trim();
|
|
163
|
-
if (colName && !['where', 'data', 'select', 'orderBy', 'include', 'const', 'let', 'return', 'if', 'else', 'async', 'await'].includes(colName)) {
|
|
164
|
-
if (!usage.has(colName)) usage.set(colName, []);
|
|
165
|
-
usage.get(colName)!.push({ file: relPath, line: i + 1, usage: line.trim() });
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
} catch {
|
|
171
|
-
// Skip unreadable
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Detect column name mismatches between code and schema.
|
|
177
|
-
*/
|
|
178
|
-
export function detectMismatches(models: SchemaModel[]): SchemaMismatch[] {
|
|
179
|
-
const mismatches: SchemaMismatch[] = [];
|
|
180
|
-
|
|
181
|
-
const knownMismatches = getConfig().knownMismatches ?? {};
|
|
182
|
-
|
|
183
|
-
for (const [tableName, wrongColumns] of Object.entries(knownMismatches)) {
|
|
184
|
-
const model = models.find(m => m.tableName === tableName);
|
|
185
|
-
if (!model) continue;
|
|
186
|
-
|
|
187
|
-
const actualColumnNames = model.fields.map(f => f.name);
|
|
188
|
-
|
|
189
|
-
for (const [wrongCol, correctCol] of Object.entries(wrongColumns)) {
|
|
190
|
-
// Search for the wrong column name in code
|
|
191
|
-
const routersDir = getResolvedPaths().routersDir;
|
|
192
|
-
const files = findFilesUsingColumn(routersDir, wrongCol, tableName);
|
|
193
|
-
|
|
194
|
-
if (files.length > 0) {
|
|
195
|
-
mismatches.push({
|
|
196
|
-
table: tableName,
|
|
197
|
-
codeColumn: wrongCol,
|
|
198
|
-
actualColumns: actualColumnNames,
|
|
199
|
-
files,
|
|
200
|
-
severity: 'CRITICAL',
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return mismatches;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function findFilesUsingColumn(dir: string, column: string, tableName: string): string[] {
|
|
210
|
-
const result: string[] = [];
|
|
211
|
-
if (!existsSync(dir)) return result;
|
|
212
|
-
|
|
213
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
214
|
-
for (const entry of entries) {
|
|
215
|
-
const fullPath = join(dir, entry.name);
|
|
216
|
-
if (entry.isDirectory()) {
|
|
217
|
-
result.push(...findFilesUsingColumn(fullPath, column, tableName));
|
|
218
|
-
} else if (entry.name.endsWith('.ts')) {
|
|
219
|
-
try {
|
|
220
|
-
const source = readFileSync(fullPath, 'utf-8');
|
|
221
|
-
// Only flag if both the table name and wrong column are in the file
|
|
222
|
-
if (source.includes(tableName) && source.includes(column)) {
|
|
223
|
-
result.push(fullPath.slice(getProjectRoot().length + 1));
|
|
224
|
-
}
|
|
225
|
-
} catch {
|
|
226
|
-
// Skip
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return result;
|
|
232
|
-
}
|