@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,630 +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
- import { classifyPythonFileDomain } from './python/domain-enforcer.ts';
8
- import { t } from './lib/sql-table-names.ts';
9
-
10
- /** Get the configured tool prefix */
11
- function p(name: string): string {
12
- return `${getConfig().toolPrefix}_${name}`;
13
- }
14
-
15
- function stripPrefix(name: string): string {
16
- const pfx = getConfig().toolPrefix + '_';
17
- return name.startsWith(pfx) ? name.slice(pfx.length) : name;
18
- }
19
-
20
- function text(content: string): ToolResult {
21
- return { content: [{ type: 'text', text: content }] };
22
- }
23
-
24
- /** Safely parse a JSON array from SQLite, returning [] on error */
25
- function safeParseArray(json: string): unknown[] {
26
- try { return JSON.parse(json); } catch { return []; }
27
- }
28
-
29
- /**
30
- * Python tool definitions — only included when python.root is configured.
31
- */
32
- export function getPythonToolDefinitions(): ToolDefinition[] {
33
- const config = getConfig();
34
- if (!config.python?.root) return [];
35
-
36
- return [
37
- {
38
- name: p('py_imports'),
39
- description: 'Query the Python import graph. Show imports for a file, reverse lookup (who imports this), or transitive dependencies.',
40
- inputSchema: {
41
- type: 'object',
42
- properties: {
43
- file: { type: 'string', description: 'Python file path (relative to project root) to show imports for' },
44
- imported_by: { type: 'string', description: 'Python file path to find reverse imports (who imports this file)' },
45
- transitive: { type: 'boolean', description: 'If true, traverse import graph transitively' },
46
- depth: { type: 'number', description: 'Max depth for transitive traversal (default: 5)' },
47
- },
48
- required: [],
49
- },
50
- },
51
- {
52
- name: p('py_routes'),
53
- description: 'List and filter FastAPI/Flask routes. Find routes by method, path, file. Show unauthenticated routes or routes with no frontend callers.',
54
- inputSchema: {
55
- type: 'object',
56
- properties: {
57
- method: { type: 'string', description: 'Filter by HTTP method (GET, POST, etc.)' },
58
- path: { type: 'string', description: 'Filter by route path pattern' },
59
- file: { type: 'string', description: 'Filter by source file' },
60
- unauthenticated: { type: 'boolean', description: 'Show only unauthenticated routes' },
61
- uncoupled: { type: 'boolean', description: 'Show only routes with no frontend callers' },
62
- limit: { type: 'number', description: 'Max results to return (default: 500)' },
63
- },
64
- required: [],
65
- },
66
- },
67
- {
68
- name: p('py_coupling'),
69
- description: 'Show frontend-to-backend coupling map. Find which frontend files call which backend routes.',
70
- inputSchema: {
71
- type: 'object',
72
- properties: {
73
- frontend_file: { type: 'string', description: 'Filter by frontend file' },
74
- backend_path: { type: 'string', description: 'Filter by backend route path' },
75
- },
76
- required: [],
77
- },
78
- },
79
- {
80
- name: p('py_models'),
81
- description: 'SQLAlchemy model catalog. View models, FK graph, column details, or verify column references in a file.',
82
- inputSchema: {
83
- type: 'object',
84
- properties: {
85
- model: { type: 'string', description: 'Model class name to inspect' },
86
- table: { type: 'string', description: 'Table name to inspect' },
87
- fk_graph: { type: 'boolean', description: 'Show foreign key relationship graph' },
88
- verify_file: { type: 'string', description: 'File path to verify column references against models' },
89
- limit: { type: 'number', description: 'Max results to return (default: 500)' },
90
- },
91
- required: [],
92
- },
93
- },
94
- {
95
- name: p('py_migrations'),
96
- description: 'Alembic migration information. View migration chain, specific revision details, or detect drift between models and migrations.',
97
- inputSchema: {
98
- type: 'object',
99
- properties: {
100
- revision: { type: 'string', description: 'Specific revision ID to inspect' },
101
- chain: { type: 'boolean', description: 'Show full migration chain' },
102
- drift: { type: 'boolean', description: 'Detect drift between models and migration state' },
103
- limit: { type: 'number', description: 'Max results to return (default: 500)' },
104
- },
105
- required: [],
106
- },
107
- },
108
- {
109
- name: p('py_domains'),
110
- description: 'Python domain boundary information. Classify files, show cross-domain imports, or list files in a domain.',
111
- inputSchema: {
112
- type: 'object',
113
- properties: {
114
- file: { type: 'string', description: 'File to classify into a domain' },
115
- crossings: { type: 'boolean', description: 'Show all cross-domain import violations' },
116
- domain: { type: 'string', description: 'Domain name to list files for' },
117
- },
118
- required: [],
119
- },
120
- },
121
- {
122
- name: p('py_impact'),
123
- description: 'Full impact analysis for a Python file: affected endpoints, models, frontend pages, and domain crossings.',
124
- inputSchema: {
125
- type: 'object',
126
- properties: {
127
- file: { type: 'string', description: 'Python file path to analyze impact for' },
128
- },
129
- required: ['file'],
130
- },
131
- },
132
- {
133
- name: p('py_context'),
134
- description: 'Complete context for a Python file: applicable rules, imports, domain, routes, models, and impact.',
135
- inputSchema: {
136
- type: 'object',
137
- properties: {
138
- file: { type: 'string', description: 'Python file path to get context for' },
139
- },
140
- required: ['file'],
141
- },
142
- },
143
- ];
144
- }
145
-
146
- /** Check if a tool name is a Python tool */
147
- export function isPythonTool(name: string): boolean {
148
- const base = stripPrefix(name);
149
- return base.startsWith('py_');
150
- }
151
-
152
- /** Handle Python tool calls */
153
- export function handlePythonToolCall(
154
- name: string,
155
- args: Record<string, unknown>,
156
- dataDb: Database.Database
157
- ): ToolResult {
158
- const baseName = stripPrefix(name);
159
-
160
- switch (baseName) {
161
- case 'py_imports':
162
- return handlePyImports(args, dataDb);
163
- case 'py_routes':
164
- return handlePyRoutes(args, dataDb);
165
- case 'py_coupling':
166
- return handlePyCoupling(args, dataDb);
167
- case 'py_models':
168
- return handlePyModels(args, dataDb);
169
- case 'py_migrations':
170
- return handlePyMigrations(args, dataDb);
171
- case 'py_domains':
172
- return handlePyDomains(args, dataDb);
173
- case 'py_impact':
174
- return handlePyImpact(args, dataDb);
175
- case 'py_context':
176
- return handlePyContext(args, dataDb);
177
- default:
178
- return text(`Unknown Python tool: ${name}`);
179
- }
180
- }
181
-
182
- // === Tool Handlers ===
183
-
184
- function handlePyImports(args: Record<string, unknown>, dataDb: Database.Database): ToolResult {
185
- const lines: string[] = [];
186
- const file = args.file as string | undefined;
187
- const importedBy = args.imported_by as string | undefined;
188
- const transitive = args.transitive as boolean | undefined;
189
- const maxDepth = Math.min(Math.max((args.depth as number) || 5, 1), 20);
190
-
191
- if (file) {
192
- if (transitive) {
193
- lines.push(`## Transitive imports from ${file} (max depth: ${maxDepth})`);
194
- const visited = new Set<string>();
195
- function traverse(f: string, depth: number): void {
196
- if (depth > maxDepth || visited.has(f)) return;
197
- visited.add(f);
198
- const imports = dataDb.prepare(
199
- `SELECT target_file, imported_names FROM ${t('py_imports')} WHERE source_file = ?`
200
- ).all(f) as { target_file: string; imported_names: string }[];
201
- for (const imp of imports) {
202
- const indent = ' '.repeat(depth);
203
- const names = safeParseArray(imp.imported_names) as string[];
204
- lines.push(`${indent}- ${imp.target_file}${names.length > 0 ? ': ' + names.join(', ') : ''}`);
205
- traverse(imp.target_file, depth + 1);
206
- }
207
- }
208
- traverse(file, 0);
209
- } else {
210
- const imports = dataDb.prepare(
211
- `SELECT target_file, import_type, imported_names, line FROM ${t('py_imports')} WHERE source_file = ? ORDER BY line`
212
- ).all(file) as { target_file: string; import_type: string; imported_names: string; line: number }[];
213
- lines.push(`## Imports from ${file} (${imports.length} edges)`);
214
- for (const imp of imports) {
215
- const names = safeParseArray(imp.imported_names) as string[];
216
- lines.push(`- L${imp.line} [${imp.import_type}] ${imp.target_file}${names.length > 0 ? ': ' + names.join(', ') : ''}`);
217
- }
218
- }
219
- } else if (importedBy) {
220
- const importers = dataDb.prepare(
221
- `SELECT source_file, imported_names, line FROM ${t('py_imports')} WHERE target_file = ? ORDER BY source_file`
222
- ).all(importedBy) as { source_file: string; imported_names: string; line: number }[];
223
- lines.push(`## Files importing ${importedBy} (${importers.length} edges)`);
224
- for (const imp of importers) {
225
- const names = safeParseArray(imp.imported_names) as string[];
226
- lines.push(`- ${imp.source_file}:${imp.line}${names.length > 0 ? ' (' + names.join(', ') + ')' : ''}`);
227
- }
228
- } else {
229
- const total = (dataDb.prepare(`SELECT COUNT(*) as cnt FROM ${t('py_imports')}`).get() as { cnt: number }).cnt;
230
- const files = (dataDb.prepare(`SELECT COUNT(DISTINCT source_file) as cnt FROM ${t('py_imports')}`).get() as { cnt: number }).cnt;
231
- lines.push('## Python Import Index Summary');
232
- lines.push(`- Total import edges: ${total}`);
233
- lines.push(`- Files with imports: ${files}`);
234
- lines.push('');
235
- lines.push('Use { file: "path" } to see imports for a specific file.');
236
- lines.push('Use { imported_by: "path" } for reverse lookup.');
237
- }
238
-
239
- return text(lines.join('\n') || 'No Python import data available. Run sync first.');
240
- }
241
-
242
- function handlePyRoutes(args: Record<string, unknown>, dataDb: Database.Database): ToolResult {
243
- const lines: string[] = [];
244
- const limit = Math.min(Math.max((args.limit as number) || 500, 1), 5000);
245
- let query = `SELECT * FROM ${t('py_routes')} WHERE 1=1`;
246
- const params: unknown[] = [];
247
-
248
- if (args.method) { query += ' AND method = ?'; params.push((args.method as string).toUpperCase()); }
249
- if (args.path) { query += ' AND path LIKE ?'; params.push(`%${args.path as string}%`); }
250
- if (args.file) { query += ' AND file = ?'; params.push(args.file); }
251
- if (args.unauthenticated) { query += ' AND is_authenticated = 0'; }
252
-
253
- query += ` ORDER BY file, line LIMIT ${limit}`;
254
-
255
- const routes = dataDb.prepare(query).all(...params) as {
256
- id: number; file: string; method: string; path: string; function_name: string;
257
- dependencies: string; request_model: string | null; response_model: string | null;
258
- is_authenticated: number; line: number;
259
- }[];
260
-
261
- if (args.uncoupled) {
262
- const callerCountStmt = dataDb.prepare(`SELECT COUNT(*) as cnt FROM ${t('py_route_callers')} WHERE route_id = ?`);
263
- const uncoupledRoutes = routes.filter(r => {
264
- const callers = (callerCountStmt.get(r.id) as { cnt: number }).cnt;
265
- return callers === 0;
266
- });
267
- lines.push(`## Uncoupled Routes (${uncoupledRoutes.length} with no frontend callers)`);
268
- for (const r of uncoupledRoutes) {
269
- lines.push(`- ${r.method} ${r.path} → ${r.function_name} (${r.file}:${r.line})`);
270
- }
271
- } else {
272
- lines.push(`## Python Routes (${routes.length} found)`);
273
- for (const r of routes) {
274
- const auth = r.is_authenticated ? '' : ' [UNAUTH]';
275
- const deps = safeParseArray(r.dependencies) as string[];
276
- lines.push(`- ${r.method} ${r.path} → ${r.function_name} (${r.file}:${r.line})${auth}`);
277
- if (r.response_model) lines.push(` Response: ${r.response_model}`);
278
- if (deps.length > 0) lines.push(` Depends: ${deps.join(', ')}`);
279
- }
280
- }
281
-
282
- return text(lines.join('\n') || 'No Python routes found.');
283
- }
284
-
285
- function handlePyCoupling(args: Record<string, unknown>, dataDb: Database.Database): ToolResult {
286
- const lines: string[] = [];
287
- let query = `
288
- SELECT rc.frontend_file, rc.call_pattern, rc.line as caller_line,
289
- r.method, r.path, r.function_name, r.file as backend_file
290
- FROM ${t('py_route_callers')} rc
291
- JOIN ${t('py_routes')} r ON rc.route_id = r.id
292
- WHERE 1=1
293
- `;
294
- const params: unknown[] = [];
295
-
296
- if (args.frontend_file) { query += ' AND rc.frontend_file = ?'; params.push(args.frontend_file); }
297
- if (args.backend_path) { query += ' AND r.path LIKE ?'; params.push(`%${args.backend_path as string}%`); }
298
-
299
- query += ' ORDER BY rc.frontend_file, rc.line';
300
-
301
- const couplings = dataDb.prepare(query).all(...params) as {
302
- frontend_file: string; call_pattern: string; caller_line: number;
303
- method: string; path: string; function_name: string; backend_file: string;
304
- }[];
305
-
306
- lines.push(`## Frontend ↔ Backend Coupling (${couplings.length} connections)`);
307
- let currentFrontend = '';
308
- for (const c of couplings) {
309
- if (c.frontend_file !== currentFrontend) {
310
- currentFrontend = c.frontend_file;
311
- lines.push(`\n### ${currentFrontend}`);
312
- }
313
- lines.push(`- L${c.caller_line}: ${c.call_pattern} → ${c.method} ${c.path} (${c.backend_file})`);
314
- }
315
-
316
- return text(lines.join('\n') || 'No coupling data found.');
317
- }
318
-
319
- function handlePyModels(args: Record<string, unknown>, dataDb: Database.Database): ToolResult {
320
- const lines: string[] = [];
321
-
322
- if (args.fk_graph) {
323
- const edges = dataDb.prepare(`SELECT * FROM ${t('py_fk_edges')} ORDER BY source_table`).all() as {
324
- source_table: string; source_column: string; target_table: string; target_column: string;
325
- }[];
326
- lines.push(`## Foreign Key Graph (${edges.length} edges)`);
327
- for (const e of edges) {
328
- lines.push(`- ${e.source_table}.${e.source_column} → ${e.target_table}.${e.target_column}`);
329
- }
330
- } else if (args.model || args.table) {
331
- const search = (args.model || args.table) as string;
332
- const model = dataDb.prepare(
333
- `SELECT * FROM ${t('py_models')} WHERE class_name = ? OR table_name = ?`
334
- ).get(search, search) as {
335
- class_name: string; table_name: string; file: string; line: number;
336
- columns: string; relationships: string; foreign_keys: string;
337
- } | undefined;
338
-
339
- if (!model) return text(`Model "${search}" not found.`);
340
-
341
- lines.push(`## ${model.class_name} (table: ${model.table_name || 'N/A'})`);
342
- lines.push(`File: ${model.file}:${model.line}`);
343
-
344
- const cols = safeParseArray(model.columns) as { name: string; type: string; nullable?: boolean; primary_key?: boolean }[];
345
- if (cols.length > 0) {
346
- lines.push('\n### Columns');
347
- for (const col of cols) {
348
- lines.push(`- ${col.name}: ${col.type}${col.nullable ? '?' : ''}${col.primary_key ? ' [PK]' : ''}`);
349
- }
350
- }
351
-
352
- const rels = safeParseArray(model.relationships) as { name: string; target: string; back_populates?: string }[];
353
- if (rels.length > 0) {
354
- lines.push('\n### Relationships');
355
- for (const rel of rels) {
356
- lines.push(`- ${rel.name} → ${rel.target}${rel.back_populates ? ` (back_populates: ${rel.back_populates})` : ''}`);
357
- }
358
- }
359
-
360
- const fks = safeParseArray(model.foreign_keys) as { column: string; target: string }[];
361
- if (fks.length > 0) {
362
- lines.push('\n### Foreign Keys');
363
- for (const fk of fks) {
364
- lines.push(`- ${fk.column} → ${fk.target}`);
365
- }
366
- }
367
- } else {
368
- const limit = Math.min(Math.max((args.limit as number) || 500, 1), 5000);
369
- const models = dataDb.prepare(`SELECT class_name, table_name, file, line FROM ${t('py_models')} ORDER BY class_name LIMIT ${limit}`).all() as {
370
- class_name: string; table_name: string; file: string; line: number;
371
- }[];
372
- lines.push(`## Python Models (${models.length})`);
373
- for (const m of models) {
374
- lines.push(`- ${m.class_name} (${m.table_name || 'no table'}) — ${m.file}:${m.line}`);
375
- }
376
- }
377
-
378
- return text(lines.join('\n') || 'No Python models found.');
379
- }
380
-
381
- function handlePyMigrations(args: Record<string, unknown>, dataDb: Database.Database): ToolResult {
382
- const lines: string[] = [];
383
-
384
- if (args.drift) {
385
- // Compare models against migration operations
386
- const models = dataDb.prepare(`SELECT class_name, table_name, columns FROM ${t('py_models')}`).all() as {
387
- class_name: string; table_name: string; columns: string;
388
- }[];
389
- const migrations = dataDb.prepare(`SELECT revision, operations FROM ${t('py_migrations')}`).all() as {
390
- revision: string; operations: string;
391
- }[];
392
-
393
- const migratedTables = new Set<string>();
394
- for (const m of migrations) {
395
- const ops = safeParseArray(m.operations) as { type: string; table?: string; column?: string }[];
396
- for (const op of ops) {
397
- if (op.table) migratedTables.add(op.table);
398
- }
399
- }
400
-
401
- lines.push('## Migration Drift Report');
402
- const unmigrated = models.filter(m => m.table_name && !migratedTables.has(m.table_name));
403
- if (unmigrated.length > 0) {
404
- lines.push(`\n### Models with no migration (${unmigrated.length})`);
405
- for (const m of unmigrated) {
406
- lines.push(`- ${m.class_name} (table: ${m.table_name})`);
407
- }
408
- } else {
409
- lines.push('All models have corresponding migrations.');
410
- }
411
- } else if (args.chain) {
412
- const limit = Math.min(Math.max((args.limit as number) || 500, 1), 5000);
413
- const migrations = dataDb.prepare(`SELECT * FROM ${t('py_migrations')} ORDER BY id LIMIT ${limit}`).all() as {
414
- revision: string; down_revision: string | null; file: string; description: string | null; is_head: number;
415
- }[];
416
- lines.push(`## Migration Chain (${migrations.length} revisions)`);
417
- for (const m of migrations) {
418
- const head = m.is_head ? ' [HEAD]' : '';
419
- lines.push(`- ${m.revision}${head} ← ${m.down_revision || 'BASE'}: ${m.description || '(no description)'} (${m.file})`);
420
- }
421
- } else if (args.revision) {
422
- const m = dataDb.prepare(`SELECT * FROM ${t('py_migrations')} WHERE revision = ?`).get(args.revision) as {
423
- revision: string; down_revision: string | null; file: string; description: string | null;
424
- operations: string; is_head: number;
425
- } | undefined;
426
- if (!m) return text(`Revision "${args.revision}" not found.`);
427
- lines.push(`## Revision: ${m.revision}`);
428
- lines.push(`Down: ${m.down_revision || 'BASE'}`);
429
- lines.push(`File: ${m.file}`);
430
- lines.push(`Description: ${m.description || 'N/A'}`);
431
- const ops = safeParseArray(m.operations) as { type: string; table?: string; column?: string }[];
432
- if (ops.length > 0) {
433
- lines.push('\n### Operations');
434
- for (const op of ops) {
435
- lines.push(`- ${op.type}: ${op.table || op.column || ''}`);
436
- }
437
- }
438
- } else {
439
- const total = (dataDb.prepare(`SELECT COUNT(*) as cnt FROM ${t('py_migrations')}`).get() as { cnt: number }).cnt;
440
- const head = dataDb.prepare(`SELECT revision FROM ${t('py_migrations')} WHERE is_head = 1`).get() as { revision: string } | undefined;
441
- lines.push('## Migration Summary');
442
- lines.push(`- Total revisions: ${total}`);
443
- lines.push(`- Current head: ${head?.revision || 'N/A'}`);
444
- }
445
-
446
- return text(lines.join('\n') || 'No Python migration data found.');
447
- }
448
-
449
- function handlePyDomains(args: Record<string, unknown>, dataDb: Database.Database): ToolResult {
450
- const lines: string[] = [];
451
- const config = getConfig();
452
- const domains = config.python?.domains || [];
453
-
454
- if (args.file) {
455
- const file = args.file as string;
456
- const domain = classifyPythonFileDomain(file);
457
- lines.push(`## ${file}`);
458
- lines.push(`Domain: ${domain}`);
459
- const domainConfig = domains.find(d => d.name === domain);
460
- if (domainConfig) {
461
- lines.push(`Allowed imports from: ${domainConfig.allowed_imports_from.join(', ') || 'any'}`);
462
- }
463
- } else if (args.crossings) {
464
- const imports = dataDb.prepare(`SELECT source_file, target_file FROM ${t('py_imports')}`).all() as {
465
- source_file: string; target_file: string;
466
- }[];
467
- const violations: string[] = [];
468
- for (const imp of imports) {
469
- const srcDomain = classifyPythonFileDomain(imp.source_file);
470
- const tgtDomain = classifyPythonFileDomain(imp.target_file);
471
- if (srcDomain !== tgtDomain && srcDomain !== 'Unknown' && tgtDomain !== 'Unknown') {
472
- const srcConfig = domains.find(d => d.name === srcDomain);
473
- if (srcConfig && !srcConfig.allowed_imports_from.includes(tgtDomain)) {
474
- violations.push(`${imp.source_file} (${srcDomain}) → ${imp.target_file} (${tgtDomain})`);
475
- }
476
- }
477
- }
478
- lines.push(`## Cross-Domain Violations (${violations.length})`);
479
- for (const v of violations.slice(0, 50)) {
480
- lines.push(`- ${v}`);
481
- }
482
- if (violations.length > 50) lines.push(`... and ${violations.length - 50} more`);
483
- } else if (args.domain) {
484
- const domainName = args.domain as string;
485
- const allFiles = dataDb.prepare(`SELECT DISTINCT source_file FROM ${t('py_imports')} UNION SELECT DISTINCT target_file FROM ${t('py_imports')}`).all() as { source_file: string }[];
486
- const filesInDomain = allFiles.filter(f => classifyPythonFileDomain(f.source_file) === domainName);
487
- lines.push(`## Domain: ${domainName} (${filesInDomain.length} files)`);
488
- for (const f of filesInDomain) {
489
- lines.push(`- ${f.source_file}`);
490
- }
491
- } else {
492
- lines.push('## Python Domain Summary');
493
- for (const d of domains) {
494
- lines.push(`- **${d.name}**: packages ${d.packages.join(', ')}, imports from: ${d.allowed_imports_from.join(', ') || 'any'}`);
495
- }
496
- }
497
-
498
- return text(lines.join('\n') || 'No Python domains configured.');
499
- }
500
-
501
- function handlePyImpact(args: Record<string, unknown>, dataDb: Database.Database): ToolResult {
502
- const file = args.file as string;
503
- const lines: string[] = [`## Impact Analysis: ${file}`, ''];
504
-
505
- // 1. Who imports this file
506
- const importedBy = dataDb.prepare(
507
- `SELECT source_file FROM ${t('py_imports')} WHERE target_file = ?`
508
- ).all(file) as { source_file: string }[];
509
- lines.push(`### Imported By (${importedBy.length} files)`);
510
- for (const imp of importedBy.slice(0, 20)) {
511
- lines.push(`- ${imp.source_file}`);
512
- }
513
-
514
- // 2. Routes in this file
515
- const routes = dataDb.prepare(
516
- `SELECT method, path, function_name FROM ${t('py_routes')} WHERE file = ?`
517
- ).all(file) as { method: string; path: string; function_name: string }[];
518
- if (routes.length > 0) {
519
- lines.push(`\n### Routes Defined (${routes.length})`);
520
- for (const r of routes) {
521
- lines.push(`- ${r.method} ${r.path} → ${r.function_name}`);
522
- }
523
- }
524
-
525
- // 3. Models in this file
526
- const models = dataDb.prepare(
527
- `SELECT class_name, table_name FROM ${t('py_models')} WHERE file = ?`
528
- ).all(file) as { class_name: string; table_name: string }[];
529
- if (models.length > 0) {
530
- lines.push(`\n### Models Defined (${models.length})`);
531
- for (const m of models) {
532
- lines.push(`- ${m.class_name} (${m.table_name || 'no table'})`);
533
- }
534
- }
535
-
536
- // 4. Frontend callers (via routes)
537
- const routeIds = dataDb.prepare(`SELECT id FROM ${t('py_routes')} WHERE file = ?`).all(file) as { id: number }[];
538
- if (routeIds.length > 0) {
539
- const placeholders = routeIds.map(() => '?').join(',');
540
- const callers = dataDb.prepare(
541
- `SELECT DISTINCT frontend_file FROM ${t('py_route_callers')} WHERE route_id IN (${placeholders})`
542
- ).all(...routeIds.map(r => r.id)) as { frontend_file: string }[];
543
- if (callers.length > 0) {
544
- lines.push(`\n### Frontend Callers (${callers.length} files)`);
545
- for (const c of callers) {
546
- lines.push(`- ${c.frontend_file}`);
547
- }
548
- }
549
- }
550
-
551
- // 5. Domain
552
- const config = getConfig();
553
- const domain = classifyPythonFileDomain(file);
554
- lines.push(`\n### Domain: ${domain}`);
555
-
556
- return text(lines.join('\n'));
557
- }
558
-
559
- function handlePyContext(args: Record<string, unknown>, dataDb: Database.Database): ToolResult {
560
- const file = args.file as string;
561
- const lines: string[] = [`## Python Context: ${file}`, ''];
562
- const config = getConfig();
563
-
564
- // Domain
565
- const domain = classifyPythonFileDomain(file);
566
- lines.push(`**Domain**: ${domain}`);
567
-
568
- // Imports
569
- const imports = dataDb.prepare(
570
- `SELECT target_file, imported_names FROM ${t('py_imports')} WHERE source_file = ? LIMIT 20`
571
- ).all(file) as { target_file: string; imported_names: string }[];
572
- if (imports.length > 0) {
573
- lines.push('\n### Imports');
574
- for (const imp of imports) {
575
- const names = safeParseArray(imp.imported_names) as string[];
576
- lines.push(`- ${imp.target_file}${names.length > 0 ? ': ' + names.join(', ') : ''}`);
577
- }
578
- }
579
-
580
- // Imported by
581
- const importedBy = dataDb.prepare(
582
- `SELECT source_file FROM ${t('py_imports')} WHERE target_file = ? LIMIT 10`
583
- ).all(file) as { source_file: string }[];
584
- if (importedBy.length > 0) {
585
- lines.push('\n### Imported By');
586
- for (const imp of importedBy) {
587
- lines.push(`- ${imp.source_file}`);
588
- }
589
- }
590
-
591
- // Routes
592
- const routes = dataDb.prepare(`SELECT method, path, function_name, line FROM ${t('py_routes')} WHERE file = ?`)
593
- .all(file) as { method: string; path: string; function_name: string; line: number }[];
594
- if (routes.length > 0) {
595
- lines.push('\n### Routes');
596
- for (const r of routes) {
597
- lines.push(`- ${r.method} ${r.path} → ${r.function_name} (L${r.line})`);
598
- }
599
- }
600
-
601
- // Models
602
- const models = dataDb.prepare(`SELECT class_name, table_name, line FROM ${t('py_models')} WHERE file = ?`)
603
- .all(file) as { class_name: string; table_name: string; line: number }[];
604
- if (models.length > 0) {
605
- lines.push('\n### Models');
606
- for (const m of models) {
607
- lines.push(`- ${m.class_name} (${m.table_name || 'N/A'}) L${m.line}`);
608
- }
609
- }
610
-
611
- // Applicable rules from config
612
- const rules = config.rules || [];
613
- const matchingRules = rules.filter(r => {
614
- const pattern = r.pattern;
615
- if (pattern === '**') return true;
616
- if (pattern.includes('**/*.py')) return file.endsWith('.py');
617
- return file.includes(pattern.replace(/\*\*/g, ''));
618
- });
619
- if (matchingRules.length > 0) {
620
- lines.push('\n### Applicable Rules');
621
- for (const rule of matchingRules) {
622
- for (const r of rule.rules) {
623
- lines.push(`- ${r}`);
624
- }
625
- }
626
- }
627
-
628
- return text(lines.join('\n'));
629
- }
630
-