@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,332 +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 { createHash } from 'crypto';
7
- import { getConfig } from './config.ts';
8
- import { escapeRegex, redactSensitiveContent } from './security-utils.ts';
9
-
10
- // ============================================================
11
- // Prompt Effectiveness Analysis
12
- // ============================================================
13
-
14
- /** Prefix a base tool name with the configured tool prefix. */
15
- function p(baseName: string): string {
16
- return `${getConfig().toolPrefix}_${baseName}`;
17
- }
18
-
19
- /** Default success/failure indicators. Can be overridden via config.analytics.prompts */
20
- const DEFAULT_SUCCESS_INDICATORS = ['committed', 'approved', 'looks good', 'perfect', 'great', 'thanks'];
21
- const DEFAULT_FAILURE_INDICATORS = ['revert', 'wrong', "that's not", 'undo', 'incorrect'];
22
- const DEFAULT_ABANDON_PATTERNS = /\b(nevermind|forget it|skip|let's move on|different|instead)\b/i;
23
-
24
- /**
25
- * Categorize a prompt by its intent.
26
- */
27
- export function categorizePrompt(promptText: string): string {
28
- const lower = promptText.toLowerCase();
29
-
30
- if (/\b(fix|bug|error|broken|issue|crash|fail)\b/.test(lower)) return 'bugfix';
31
- if (/\b(refactor|rename|move|extract|cleanup|reorganize)\b/.test(lower)) return 'refactor';
32
- if (/\b(what|how|why|where|when|explain|describe|tell me)\b/.test(lower)) return 'question';
33
- if (/^\/\w+/.test(promptText.trim())) return 'command';
34
- if (/\b(add|create|implement|build|new|feature)\b/.test(lower)) return 'feature';
35
-
36
- return 'feature'; // Default to feature for implementation requests
37
- }
38
-
39
- /**
40
- * Hash a prompt for deduplication/comparison.
41
- * Normalizes whitespace and lowercases before hashing.
42
- */
43
- export function hashPrompt(promptText: string): string {
44
- const normalized = promptText.toLowerCase().replace(/\s+/g, ' ').trim();
45
- return createHash('sha256').update(normalized).digest('hex').slice(0, 16);
46
- }
47
-
48
- /**
49
- * Detect outcome from subsequent conversation context.
50
- * Heuristic based on what follows a prompt.
51
- */
52
- export function detectOutcome(
53
- followUpPrompts: string[],
54
- assistantResponses: string[]
55
- ): { outcome: string; correctionsNeeded: number; followUpCount: number } {
56
- let correctionsNeeded = 0;
57
- let outcome = 'success';
58
-
59
- const correctionPatterns = /\b(no|wrong|that's not|fix this|try again|revert|undo|incorrect|not what)\b/i;
60
-
61
- const config = getConfig();
62
- const successIndicators = config.analytics?.prompts?.success_indicators ?? DEFAULT_SUCCESS_INDICATORS;
63
- // Escape regex special chars from config-provided indicators to prevent ReDoS
64
- const escapedIndicators = successIndicators.map(escapeRegex);
65
- const successRegex = new RegExp(`\\b(${escapedIndicators.join('|')})\\b`, 'i');
66
-
67
- for (const prompt of followUpPrompts) {
68
- if (correctionPatterns.test(prompt)) {
69
- correctionsNeeded++;
70
- }
71
- if (DEFAULT_ABANDON_PATTERNS.test(prompt)) {
72
- outcome = 'abandoned';
73
- break;
74
- }
75
- }
76
-
77
- // Check assistant responses for failure signals
78
- for (const response of assistantResponses) {
79
- if (/\b(error|failed|cannot|unable to)\b/i.test(response) && response.length < 200) {
80
- outcome = 'failure';
81
- }
82
- }
83
-
84
- // Determine final outcome
85
- if (outcome === 'abandoned') {
86
- // Keep abandoned
87
- } else if (correctionsNeeded >= 3) {
88
- outcome = 'partial';
89
- } else if (correctionsNeeded > 0) {
90
- outcome = 'partial';
91
- } else {
92
- // Check for success signals in follow-ups
93
- for (const prompt of followUpPrompts) {
94
- if (successRegex.test(prompt)) {
95
- outcome = 'success';
96
- break;
97
- }
98
- }
99
- }
100
-
101
- return {
102
- outcome,
103
- correctionsNeeded,
104
- followUpCount: followUpPrompts.length,
105
- };
106
- }
107
-
108
- /**
109
- * Analyze prompts from a session and store outcomes.
110
- */
111
- export function analyzeSessionPrompts(db: Database.Database, sessionId: string): number {
112
- const prompts = db.prepare(
113
- 'SELECT prompt_text, prompt_number FROM user_prompts WHERE session_id = ? ORDER BY prompt_number ASC'
114
- ).all(sessionId) as Array<{ prompt_text: string; prompt_number: number }>;
115
-
116
- if (prompts.length === 0) return 0;
117
-
118
- let stored = 0;
119
- for (let i = 0; i < prompts.length; i++) {
120
- const prompt = prompts[i];
121
- const followUps = prompts.slice(i + 1, i + 4).map(p => p.prompt_text);
122
-
123
- const category = categorizePrompt(prompt.prompt_text);
124
- const hash = hashPrompt(prompt.prompt_text);
125
- const { outcome, correctionsNeeded, followUpCount } = detectOutcome(followUps, []);
126
-
127
- // Check if already analyzed
128
- const existing = db.prepare(
129
- 'SELECT id FROM prompt_outcomes WHERE session_id = ? AND prompt_hash = ?'
130
- ).get(sessionId, hash);
131
- if (existing) continue;
132
-
133
- // Redact sensitive content (API keys, emails, tokens, paths) before storage
134
- const redactedText = redactSensitiveContent(prompt.prompt_text.slice(0, 2000));
135
-
136
- db.prepare(`
137
- INSERT INTO prompt_outcomes
138
- (session_id, prompt_hash, prompt_text, prompt_category, word_count, outcome,
139
- corrections_needed, follow_up_prompts)
140
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
141
- `).run(
142
- sessionId, hash, redactedText, category,
143
- prompt.prompt_text.split(/\s+/).length, outcome,
144
- correctionsNeeded, followUpCount
145
- );
146
- stored++;
147
- }
148
-
149
- return stored;
150
- }
151
-
152
- // ============================================================
153
- // MCP Tool Definitions & Handlers
154
- // ============================================================
155
-
156
- export function getPromptToolDefinitions(): ToolDefinition[] {
157
- return [
158
- {
159
- name: p('prompt_effectiveness'),
160
- description: 'Prompt effectiveness statistics by category. Shows success rates, average corrections needed, and best-performing prompt patterns.',
161
- inputSchema: {
162
- type: 'object',
163
- properties: {
164
- category: {
165
- type: 'string',
166
- description: 'Filter by category: feature, bugfix, refactor, question, command',
167
- },
168
- days: { type: 'number', description: 'Days to look back (default: 30)' },
169
- },
170
- required: [],
171
- },
172
- },
173
- {
174
- name: p('prompt_suggestions'),
175
- description: 'Suggest improvements for a prompt based on past outcomes. Finds similar prompts ranked by success rate.',
176
- inputSchema: {
177
- type: 'object',
178
- properties: {
179
- prompt: { type: 'string', description: 'The prompt text to analyze' },
180
- },
181
- required: ['prompt'],
182
- },
183
- },
184
- ];
185
- }
186
-
187
- const PROMPT_BASE_NAMES = new Set(['prompt_effectiveness', 'prompt_suggestions']);
188
-
189
- export function isPromptTool(name: string): boolean {
190
- const pfx = getConfig().toolPrefix + '_';
191
- const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
192
- return PROMPT_BASE_NAMES.has(baseName);
193
- }
194
-
195
- export function handlePromptToolCall(
196
- name: string,
197
- args: Record<string, unknown>,
198
- memoryDb: Database.Database
199
- ): ToolResult {
200
- try {
201
- const pfx = getConfig().toolPrefix + '_';
202
- const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
203
-
204
- switch (baseName) {
205
- case 'prompt_effectiveness':
206
- return handleEffectiveness(args, memoryDb);
207
- case 'prompt_suggestions':
208
- return handleSuggestions(args, memoryDb);
209
- default:
210
- return text(`Unknown prompt tool: ${name}`);
211
- }
212
- } catch (error) {
213
- return text(`Error in ${name}: ${error instanceof Error ? error.message : String(error)}\n\nUsage: ${p('prompt_effectiveness')} { days: 30 }, ${p('prompt_suggestions')} { prompt: "..." }`);
214
- }
215
- }
216
-
217
- function handleEffectiveness(args: Record<string, unknown>, db: Database.Database): ToolResult {
218
- const category = args.category as string | undefined;
219
- const days = (args.days as number) ?? 30;
220
-
221
- let sql = `
222
- SELECT prompt_category,
223
- COUNT(*) as total,
224
- SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) as successes,
225
- SUM(CASE WHEN outcome = 'partial' THEN 1 ELSE 0 END) as partials,
226
- SUM(CASE WHEN outcome = 'failure' THEN 1 ELSE 0 END) as failures,
227
- SUM(CASE WHEN outcome = 'abandoned' THEN 1 ELSE 0 END) as abandoned,
228
- AVG(corrections_needed) as avg_corrections,
229
- AVG(word_count) as avg_word_count
230
- FROM prompt_outcomes
231
- WHERE created_at >= datetime('now', ?)
232
- `;
233
- const params: (string | number)[] = [`-${days} days`];
234
-
235
- if (category) {
236
- sql += ' AND prompt_category = ?';
237
- params.push(category);
238
- }
239
-
240
- sql += ' GROUP BY prompt_category ORDER BY total DESC';
241
-
242
- const rows = db.prepare(sql).all(...params) as Array<Record<string, unknown>>;
243
-
244
- if (rows.length === 0) {
245
- return text(`No prompt outcomes found in the last ${days} days. Prompt analysis runs automatically at session end. Try a longer time range: ${p('prompt_effectiveness')} { days: 90 }, or use ${p('prompt_suggestions')} { prompt: "your text" } to analyze a prompt directly.`);
246
- }
247
-
248
- const lines = [
249
- `## Prompt Effectiveness (${days} days)`,
250
- '',
251
- '| Category | Total | Success % | Partial | Failed | Abandoned | Avg Corrections | Avg Words |',
252
- '|----------|-------|-----------|---------|--------|-----------|-----------------|-----------|',
253
- ];
254
-
255
- for (const row of rows) {
256
- const total = row.total as number;
257
- const successRate = total > 0 ? Math.round(((row.successes as number) / total) * 100) : 0;
258
- lines.push(
259
- `| ${row.prompt_category} | ${total} | ${successRate}% | ${row.partials} | ${row.failures} | ${row.abandoned} | ${(row.avg_corrections as number).toFixed(1)} | ${Math.round(row.avg_word_count as number)} |`
260
- );
261
- }
262
-
263
- return text(lines.join('\n'));
264
- }
265
-
266
- function handleSuggestions(args: Record<string, unknown>, db: Database.Database): ToolResult {
267
- const prompt = args.prompt as string;
268
- if (!prompt) return text(`Usage: ${p('prompt_suggestions')} { prompt: "your prompt text here" } - Analyzes a prompt and suggests improvements based on past outcomes.`);
269
-
270
- const category = categorizePrompt(prompt);
271
- const wordCount = prompt.split(/\s+/).length;
272
-
273
- // Find successful prompts in the same category with similar length
274
- const similar = db.prepare(`
275
- SELECT prompt_text, outcome, corrections_needed, word_count
276
- FROM prompt_outcomes
277
- WHERE prompt_category = ? AND outcome = 'success'
278
- ORDER BY ABS(word_count - ?) ASC
279
- LIMIT 5
280
- `).all(category, wordCount) as Array<{
281
- prompt_text: string;
282
- outcome: string;
283
- corrections_needed: number;
284
- word_count: number;
285
- }>;
286
-
287
- const lines = [
288
- `## Prompt Analysis`,
289
- `Category: ${category}`,
290
- `Word count: ${wordCount}`,
291
- '',
292
- ];
293
-
294
- // Suggestions based on patterns
295
- if (wordCount < 10) {
296
- lines.push('**Suggestion**: Short prompts often need follow-up corrections. Consider adding more context about:');
297
- lines.push('- Expected behavior or output');
298
- lines.push('- Specific files or components to modify');
299
- lines.push('- Constraints or patterns to follow');
300
- lines.push('');
301
- }
302
-
303
- if (similar.length > 0) {
304
- lines.push('### Successful Similar Prompts');
305
- for (const s of similar) {
306
- lines.push(`- [${s.word_count} words] ${s.prompt_text.slice(0, 150)}...`);
307
- }
308
- } else {
309
- lines.push('No similar successful prompts found in this category.');
310
- }
311
-
312
- // Category-specific stats
313
- const stats = db.prepare(`
314
- SELECT COUNT(*) as total,
315
- SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) as successes,
316
- AVG(corrections_needed) as avg_corrections
317
- FROM prompt_outcomes WHERE prompt_category = ?
318
- `).get(category) as { total: number; successes: number; avg_corrections: number };
319
-
320
- if (stats.total > 0) {
321
- lines.push('');
322
- lines.push(`### Category Stats: ${category}`);
323
- lines.push(`- Success rate: ${Math.round((stats.successes / stats.total) * 100)}%`);
324
- lines.push(`- Avg corrections needed: ${stats.avg_corrections.toFixed(1)}`);
325
- }
326
-
327
- return text(lines.join('\n'));
328
- }
329
-
330
- function text(content: string): ToolResult {
331
- return { content: [{ type: 'text', text: content }] };
332
- }
@@ -1,125 +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, readdirSync } from 'fs';
5
- import { join, relative } from 'path';
6
- import type Database from 'better-sqlite3';
7
- import { getProjectRoot, getConfig } from '../config.ts';
8
- import { t } from '../lib/sql-table-names.ts';
9
-
10
- interface CouplingMatch {
11
- frontendFile: string;
12
- line: number;
13
- callPattern: string;
14
- routeId: number;
15
- }
16
-
17
- /**
18
- * Scan frontend files for API calls matching Python routes.
19
- * Stores matches in massu_py_route_callers.
20
- */
21
- export function buildPythonCouplingIndex(dataDb: Database.Database): number {
22
- const projectRoot = getProjectRoot();
23
- const config = getConfig();
24
- const srcDir = join(projectRoot, config.paths.source);
25
-
26
- // Get all routes from DB
27
- const routes = dataDb.prepare(`SELECT id, method, path FROM ${t('py_routes')}`).all() as {
28
- id: number; method: string; path: string;
29
- }[];
30
-
31
- if (routes.length === 0) return 0;
32
-
33
- // Clear existing callers
34
- dataDb.exec(`DELETE FROM ${t('py_route_callers')}`);
35
-
36
- // Walk frontend files (TS/TSX/JS/JSX)
37
- const frontendFiles = walkFrontendFiles(srcDir);
38
-
39
- const insertStmt = dataDb.prepare(
40
- `INSERT INTO ${t('py_route_callers')} (route_id, frontend_file, line, call_pattern) VALUES (?, ?, ?, ?)`
41
- );
42
-
43
- let count = 0;
44
-
45
- // API call patterns to detect
46
- const apiPatterns = [
47
- /fetch\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // fetch('/api/...')
48
- /fetch\s*\(\s*[`'"]([^`'"]*\/api\/[^`'"]*)[`'"]/g, // fetch('http.../api/...')
49
- /axios\.\w+\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // axios.get('/api/...')
50
- /\.get\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // client.get('/api/...')
51
- /\.post\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // client.post('/api/...')
52
- /\.put\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // client.put('/api/...')
53
- /\.delete\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // client.delete('/api/...')
54
- /\.patch\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // client.patch('/api/...')
55
- ];
56
-
57
- dataDb.transaction(() => {
58
- for (const absFile of frontendFiles) {
59
- const relFile = relative(projectRoot, absFile);
60
- let source: string;
61
- try { source = readFileSync(absFile, 'utf-8'); } catch { continue; }
62
-
63
- const lines = source.split('\n');
64
- for (let i = 0; i < lines.length; i++) {
65
- const line = lines[i];
66
- for (const pattern of apiPatterns) {
67
- pattern.lastIndex = 0;
68
- let match;
69
- while ((match = pattern.exec(line)) !== null) {
70
- const urlPath = match[1];
71
- // Try to match against routes
72
- const matchedRoute = findMatchingRoute(urlPath, routes);
73
- if (matchedRoute) {
74
- insertStmt.run(matchedRoute.id, relFile, i + 1, match[0].slice(0, 200));
75
- count++;
76
- }
77
- }
78
- }
79
- }
80
- }
81
- })();
82
-
83
- return count;
84
- }
85
-
86
- function walkFrontendFiles(dir: string): string[] {
87
- const files: string[] = [];
88
- const exclude = ['node_modules', '.next', 'dist', '.git', '__pycache__', '.venv', 'venv'];
89
- try {
90
- const entries = readdirSync(dir, { withFileTypes: true });
91
- for (const entry of entries) {
92
- if (entry.isDirectory()) {
93
- if (exclude.includes(entry.name)) continue;
94
- files.push(...walkFrontendFiles(join(dir, entry.name)));
95
- } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
96
- files.push(join(dir, entry.name));
97
- }
98
- }
99
- } catch { /* dir not readable, skip */ }
100
- return files;
101
- }
102
-
103
- /**
104
- * Match a URL path against route definitions.
105
- * Handles path parameters: /api/users/{id} matches /api/users/123
106
- */
107
- /**
108
- * Escape special regex characters in a string.
109
- */
110
- function escapeRegex(s: string): string {
111
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
112
- }
113
-
114
- function findMatchingRoute(urlPath: string, routes: { id: number; method: string; path: string }[]): { id: number } | null {
115
- for (const route of routes) {
116
- // Escape regex-special chars in route path, then replace param placeholders
117
- const escaped = escapeRegex(route.path);
118
- const pattern = escaped.replace(/\\\{[^}]+\\\}/g, '[^/]+');
119
- const routeRegex = new RegExp('^' + pattern + '$');
120
- if (routeRegex.test(urlPath)) {
121
- return { id: route.id };
122
- }
123
- }
124
- return null;
125
- }
@@ -1,84 +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 { getConfig } from '../config.ts';
6
- import { t } from '../lib/sql-table-names.ts';
7
-
8
- export interface DomainViolation {
9
- sourceFile: string;
10
- sourceDomain: string;
11
- targetFile: string;
12
- targetDomain: string;
13
- }
14
-
15
- /**
16
- * Classify a Python file into its domain based on python.domains config.
17
- */
18
- export function classifyPythonFileDomain(file: string): string {
19
- const config = getConfig();
20
- const domains = config.python?.domains || [];
21
- const pythonRoot = config.python?.root || '';
22
-
23
- let modulePath = file;
24
- if (pythonRoot && modulePath.startsWith(pythonRoot + '/')) {
25
- modulePath = modulePath.slice(pythonRoot.length + 1);
26
- }
27
- modulePath = modulePath.replace(/\/__init__\.py$/, '').replace(/\.py$/, '').replace(/\//g, '.');
28
-
29
- for (const domain of domains) {
30
- for (const pkg of domain.packages) {
31
- if (modulePath.startsWith(pkg) || modulePath === pkg) {
32
- return domain.name;
33
- }
34
- }
35
- }
36
- return 'Unknown';
37
- }
38
-
39
- /**
40
- * Find all cross-domain import violations in the Python import graph.
41
- */
42
- export function findPythonDomainViolations(dataDb: Database.Database): DomainViolation[] {
43
- const config = getConfig();
44
- const domains = config.python?.domains || [];
45
- if (domains.length === 0) return [];
46
-
47
- const imports = dataDb.prepare(
48
- `SELECT source_file, target_file FROM ${t('py_imports')}`
49
- ).all() as { source_file: string; target_file: string }[];
50
-
51
- const violations: DomainViolation[] = [];
52
-
53
- for (const imp of imports) {
54
- const srcDomain = classifyPythonFileDomain(imp.source_file);
55
- const tgtDomain = classifyPythonFileDomain(imp.target_file);
56
-
57
- if (srcDomain === tgtDomain || srcDomain === 'Unknown' || tgtDomain === 'Unknown') continue;
58
-
59
- const srcConfig = domains.find(d => d.name === srcDomain);
60
- if (srcConfig && !srcConfig.allowed_imports_from.includes(tgtDomain)) {
61
- violations.push({
62
- sourceFile: imp.source_file,
63
- sourceDomain: srcDomain,
64
- targetFile: imp.target_file,
65
- targetDomain: tgtDomain,
66
- });
67
- }
68
- }
69
-
70
- return violations;
71
- }
72
-
73
- /**
74
- * Get all Python files in a specific domain.
75
- */
76
- export function getPythonFilesInDomain(dataDb: Database.Database, domainName: string): string[] {
77
- const allFiles = dataDb.prepare(
78
- `SELECT DISTINCT source_file as f FROM ${t('py_imports')} UNION SELECT DISTINCT target_file as f FROM ${t('py_imports')}`
79
- ).all() as { f: string }[];
80
-
81
- return allFiles
82
- .map(row => row.f)
83
- .filter(f => classifyPythonFileDomain(f) === domainName);
84
- }
@@ -1,96 +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 { classifyPythonFileDomain } from './domain-enforcer.ts';
6
- import { t } from '../lib/sql-table-names.ts';
7
-
8
- export interface PythonImpactReport {
9
- file: string;
10
- domain: string;
11
- importedBy: string[];
12
- routes: { method: string; path: string; functionName: string }[];
13
- models: { className: string; tableName: string | null }[];
14
- frontendCallers: string[];
15
- domainCrossings: { file: string; domain: string }[];
16
- }
17
-
18
- /**
19
- * Full impact analysis for a Python file.
20
- * Cross-references import graph, routes, models, and frontend coupling.
21
- */
22
- export function analyzePythonImpact(dataDb: Database.Database, file: string): PythonImpactReport {
23
- const domain = classifyPythonFileDomain(file);
24
-
25
- // 1. Who imports this file (direct + transitive)
26
- const importedBy = collectTransitiveImporters(dataDb, file, 5);
27
-
28
- // 2. Routes defined in this file
29
- const routes = dataDb.prepare(
30
- `SELECT method, path, function_name FROM ${t('py_routes')} WHERE file = ?`
31
- ).all(file) as { method: string; path: string; function_name: string }[];
32
-
33
- // 3. Models defined in this file
34
- const models = dataDb.prepare(
35
- `SELECT class_name, table_name FROM ${t('py_models')} WHERE file = ?`
36
- ).all(file) as { class_name: string; table_name: string | null }[];
37
-
38
- // 4. Frontend callers (via routes in this file)
39
- const routeIds = dataDb.prepare(`SELECT id FROM ${t('py_routes')} WHERE file = ?`).all(file) as { id: number }[];
40
- const frontendCallers: string[] = [];
41
- if (routeIds.length > 0) {
42
- const placeholders = routeIds.map(() => '?').join(',');
43
- const callers = dataDb.prepare(
44
- `SELECT DISTINCT frontend_file FROM ${t('py_route_callers')} WHERE route_id IN (${placeholders})`
45
- ).all(...routeIds.map(r => r.id)) as { frontend_file: string }[];
46
- frontendCallers.push(...callers.map(c => c.frontend_file));
47
- }
48
-
49
- // 5. Domain crossings
50
- const imports = dataDb.prepare(
51
- `SELECT target_file FROM ${t('py_imports')} WHERE source_file = ?`
52
- ).all(file) as { target_file: string }[];
53
-
54
- const domainCrossings = imports
55
- .map(imp => ({ file: imp.target_file, domain: classifyPythonFileDomain(imp.target_file) }))
56
- .filter(imp => imp.domain !== domain && imp.domain !== 'Unknown');
57
-
58
- return {
59
- file,
60
- domain,
61
- importedBy,
62
- routes: routes.map(r => ({ method: r.method, path: r.path, functionName: r.function_name })),
63
- models: models.map(m => ({ className: m.class_name, tableName: m.table_name })),
64
- frontendCallers,
65
- domainCrossings,
66
- };
67
- }
68
-
69
- function collectTransitiveImporters(dataDb: Database.Database, file: string, maxDepth: number): string[] {
70
- const visited = new Set<string>();
71
- const queue = [file];
72
- let depth = 0;
73
-
74
- const importerStmt = dataDb.prepare(
75
- `SELECT source_file FROM ${t('py_imports')} WHERE target_file = ?`
76
- );
77
-
78
- while (queue.length > 0 && depth < maxDepth) {
79
- const batch = [...queue];
80
- queue.length = 0;
81
- for (const f of batch) {
82
- if (visited.has(f)) continue;
83
- visited.add(f);
84
- const importers = importerStmt.all(f) as { source_file: string }[];
85
- for (const imp of importers) {
86
- if (!visited.has(imp.source_file)) {
87
- queue.push(imp.source_file);
88
- }
89
- }
90
- }
91
- depth++;
92
- }
93
-
94
- visited.delete(file); // Don't include the file itself
95
- return [...visited];
96
- }