@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.
Files changed (178) hide show
  1. package/CHANGELOG.md +1334 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +581 -448
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +67 -19
  16. package/dist/hooks/pre-compact.js +28 -3
  17. package/dist/hooks/pre-delete-check.js +53 -27
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +31 -6
  23. package/dist/hooks/session-start.js +8709 -8459
  24. package/dist/hooks/user-prompt.js +25 -1
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/framework-supports.ts +0 -85
  110. package/src/lib/gitToplevel.ts +0 -22
  111. package/src/lib/hook-registry.ts +0 -43
  112. package/src/lib/installLock.ts +0 -66
  113. package/src/lib/memory-path.ts +0 -49
  114. package/src/lib/pidLiveness.ts +0 -67
  115. package/src/lib/settings-local.ts +0 -110
  116. package/src/lib/sql-table-names.ts +0 -71
  117. package/src/license.ts +0 -547
  118. package/src/lsp/auto-detect.ts +0 -98
  119. package/src/lsp/client.ts +0 -776
  120. package/src/lsp/enrich.ts +0 -127
  121. package/src/lsp/types.ts +0 -221
  122. package/src/memory-db.ts +0 -1614
  123. package/src/memory-file-ingest.ts +0 -128
  124. package/src/memory-tools.ts +0 -424
  125. package/src/middleware-tree.ts +0 -71
  126. package/src/observability-tools.ts +0 -343
  127. package/src/observation-extractor.ts +0 -418
  128. package/src/page-deps.ts +0 -285
  129. package/src/permissions.ts +0 -422
  130. package/src/prompt-analyzer.ts +0 -332
  131. package/src/python/coupling-detector.ts +0 -125
  132. package/src/python/domain-enforcer.ts +0 -84
  133. package/src/python/impact-analyzer.ts +0 -96
  134. package/src/python/import-parser.ts +0 -244
  135. package/src/python/import-resolver.ts +0 -136
  136. package/src/python/migration-indexer.ts +0 -116
  137. package/src/python/migration-parser.ts +0 -332
  138. package/src/python/model-indexer.ts +0 -71
  139. package/src/python/model-parser.ts +0 -279
  140. package/src/python/route-indexer.ts +0 -59
  141. package/src/python/route-parser.ts +0 -317
  142. package/src/python-tools.ts +0 -630
  143. package/src/regression-detector.ts +0 -319
  144. package/src/rules.ts +0 -57
  145. package/src/schema-mapper.ts +0 -232
  146. package/src/security/adapter-origin.ts +0 -130
  147. package/src/security/adapter-verifier.ts +0 -319
  148. package/src/security/atomic-write.ts +0 -164
  149. package/src/security/fetcher.ts +0 -200
  150. package/src/security/install-tracking.ts +0 -319
  151. package/src/security/license-pubkey.generated.ts +0 -30
  152. package/src/security/license-response-verifier.ts +0 -149
  153. package/src/security/local-fingerprint.ts +0 -225
  154. package/src/security/manifest-cache.ts +0 -333
  155. package/src/security/manifest-schema.ts +0 -129
  156. package/src/security/registry-pubkey.generated.ts +0 -35
  157. package/src/security/telemetry.ts +0 -320
  158. package/src/security-scorer.ts +0 -405
  159. package/src/security-utils.ts +0 -133
  160. package/src/sentinel-db.ts +0 -580
  161. package/src/sentinel-scanner.ts +0 -406
  162. package/src/sentinel-tools.ts +0 -513
  163. package/src/sentinel-types.ts +0 -140
  164. package/src/server-dispatch.ts +0 -225
  165. package/src/server.ts +0 -107
  166. package/src/session-archiver.ts +0 -111
  167. package/src/session-state-generator.ts +0 -174
  168. package/src/team-knowledge.ts +0 -407
  169. package/src/tool-db-needs.ts +0 -232
  170. package/src/tools.ts +0 -1206
  171. package/src/transcript-parser.ts +0 -458
  172. package/src/trpc-index.ts +0 -215
  173. package/src/validate-features-runner.ts +0 -107
  174. package/src/validation-engine.ts +0 -358
  175. package/src/watch/daemon.ts +0 -385
  176. package/src/watch/lockfile-detector.ts +0 -65
  177. package/src/watch/paths.ts +0 -279
  178. 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
- }
@@ -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
- }