@massu/core 1.10.6 → 1.10.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +363 -334
- package/dist/hooks/post-edit-context.js +6 -1
- package/dist/hooks/pre-delete-check.js +12 -7
- package/package.json +1 -1
- package/src/db.ts +63 -62
- package/src/domains.ts +2 -1
- package/src/hooks/pre-delete-check.ts +7 -5
- package/src/import-resolver.ts +3 -2
- package/src/knowledge-tools.ts +3 -2
- package/src/lib/sql-table-names.ts +71 -0
- package/src/middleware-tree.ts +6 -5
- package/src/page-deps.ts +9 -8
- package/src/python/coupling-detector.ts +4 -3
- package/src/python/domain-enforcer.ts +3 -2
- package/src/python/impact-analyzer.ts +7 -6
- package/src/python/import-resolver.ts +3 -2
- package/src/python/migration-indexer.ts +5 -4
- package/src/python/model-indexer.ts +5 -4
- package/src/python/route-indexer.ts +4 -3
- package/src/python-tools.ts +30 -29
- package/src/security/license-pubkey.generated.ts +1 -1
- package/src/security/registry-pubkey.generated.ts +1 -1
- package/src/sentinel-db.ts +47 -46
- package/src/sentinel-scanner.ts +3 -2
- package/src/sentinel-tools.ts +2 -1
- package/src/tools.ts +14 -13
- package/src/trpc-index.ts +5 -4
- package/src/validate-features-runner.ts +5 -4
package/src/page-deps.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { resolve } from 'path';
|
|
|
6
6
|
import type Database from 'better-sqlite3';
|
|
7
7
|
import { getConfig, getProjectRoot } from './config.ts';
|
|
8
8
|
import { ensureWithinRoot } from './security-utils.ts';
|
|
9
|
+
import { t } from './lib/sql-table-names.ts';
|
|
9
10
|
|
|
10
11
|
export interface PageChain {
|
|
11
12
|
page: string;
|
|
@@ -61,7 +62,7 @@ function traceImports(
|
|
|
61
62
|
visited.add(startFile);
|
|
62
63
|
|
|
63
64
|
const imports = dataDb.prepare(
|
|
64
|
-
|
|
65
|
+
`SELECT target_file, imported_names FROM ${t('imports')} WHERE source_file = ?`
|
|
65
66
|
).all(startFile) as { target_file: string; imported_names: string }[];
|
|
66
67
|
|
|
67
68
|
for (const imp of imports) {
|
|
@@ -117,7 +118,7 @@ function findTablesFromRouters(routerNames: string[], dataDb: Database.Database)
|
|
|
117
118
|
// Look up router files from the tRPC index
|
|
118
119
|
for (const routerName of routerNames) {
|
|
119
120
|
const procs = dataDb.prepare(
|
|
120
|
-
|
|
121
|
+
`SELECT DISTINCT router_file FROM ${t('trpc_procedures')} WHERE router_name = ?`
|
|
121
122
|
).all(routerName) as { router_file: string }[];
|
|
122
123
|
|
|
123
124
|
for (const proc of procs) {
|
|
@@ -150,7 +151,7 @@ function findTablesFromRouters(routerNames: string[], dataDb: Database.Database)
|
|
|
150
151
|
*/
|
|
151
152
|
export function buildPageDeps(dataDb: Database.Database, codegraphDb: Database.Database): number {
|
|
152
153
|
// Clear existing data
|
|
153
|
-
dataDb.exec(
|
|
154
|
+
dataDb.exec(`DELETE FROM ${t('page_deps')}`);
|
|
154
155
|
|
|
155
156
|
// Find all page.tsx files from CodeGraph
|
|
156
157
|
const pages = codegraphDb.prepare(
|
|
@@ -158,7 +159,7 @@ export function buildPageDeps(dataDb: Database.Database, codegraphDb: Database.D
|
|
|
158
159
|
).all() as { path: string }[];
|
|
159
160
|
|
|
160
161
|
const insertStmt = dataDb.prepare(
|
|
161
|
-
|
|
162
|
+
`INSERT INTO ${t('page_deps')} (page_file, route, portal, components, hooks, routers, tables_touched) VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
162
163
|
);
|
|
163
164
|
|
|
164
165
|
let count = 0;
|
|
@@ -200,7 +201,7 @@ export function buildPageDeps(dataDb: Database.Database, codegraphDb: Database.D
|
|
|
200
201
|
* Get the dependency chain for a specific page.
|
|
201
202
|
*/
|
|
202
203
|
export function getPageChain(dataDb: Database.Database, pageFile: string): PageChain | null {
|
|
203
|
-
const row = dataDb.prepare(
|
|
204
|
+
const row = dataDb.prepare(`SELECT * FROM ${t('page_deps')} WHERE page_file = ?`).get(pageFile) as {
|
|
204
205
|
page_file: string; route: string; portal: string;
|
|
205
206
|
components: string; hooks: string; routers: string; tables_touched: string;
|
|
206
207
|
} | undefined;
|
|
@@ -229,7 +230,7 @@ export function findAffectedPages(dataDb: Database.Database, file: string): Page
|
|
|
229
230
|
// Find all pages that import this file (directly or transitively)
|
|
230
231
|
// First, find who imports this file
|
|
231
232
|
const importers = dataDb.prepare(
|
|
232
|
-
|
|
233
|
+
`SELECT source_file FROM ${t('imports')} WHERE target_file = ?`
|
|
233
234
|
).all(file) as { source_file: string }[];
|
|
234
235
|
|
|
235
236
|
const affectedFiles = new Set<string>([file, ...importers.map(i => i.source_file)]);
|
|
@@ -244,7 +245,7 @@ export function findAffectedPages(dataDb: Database.Database, file: string): Page
|
|
|
244
245
|
const next: string[] = [];
|
|
245
246
|
for (const f of frontier) {
|
|
246
247
|
const upstreamImporters = dataDb.prepare(
|
|
247
|
-
|
|
248
|
+
`SELECT source_file FROM ${t('imports')} WHERE target_file = ?`
|
|
248
249
|
).all(f) as { source_file: string }[];
|
|
249
250
|
|
|
250
251
|
for (const imp of upstreamImporters) {
|
|
@@ -260,7 +261,7 @@ export function findAffectedPages(dataDb: Database.Database, file: string): Page
|
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
// Now find which pages are in the affected set
|
|
263
|
-
const allPages = dataDb.prepare(
|
|
264
|
+
const allPages = dataDb.prepare(`SELECT * FROM ${t('page_deps')}`).all() as {
|
|
264
265
|
page_file: string; route: string; portal: string;
|
|
265
266
|
components: string; hooks: string; routers: string; tables_touched: string;
|
|
266
267
|
}[];
|
|
@@ -5,6 +5,7 @@ import { readFileSync, readdirSync } from 'fs';
|
|
|
5
5
|
import { join, relative } from 'path';
|
|
6
6
|
import type Database from 'better-sqlite3';
|
|
7
7
|
import { getProjectRoot, getConfig } from '../config.ts';
|
|
8
|
+
import { t } from '../lib/sql-table-names.ts';
|
|
8
9
|
|
|
9
10
|
interface CouplingMatch {
|
|
10
11
|
frontendFile: string;
|
|
@@ -23,20 +24,20 @@ export function buildPythonCouplingIndex(dataDb: Database.Database): number {
|
|
|
23
24
|
const srcDir = join(projectRoot, config.paths.source);
|
|
24
25
|
|
|
25
26
|
// Get all routes from DB
|
|
26
|
-
const routes = dataDb.prepare(
|
|
27
|
+
const routes = dataDb.prepare(`SELECT id, method, path FROM ${t('py_routes')}`).all() as {
|
|
27
28
|
id: number; method: string; path: string;
|
|
28
29
|
}[];
|
|
29
30
|
|
|
30
31
|
if (routes.length === 0) return 0;
|
|
31
32
|
|
|
32
33
|
// Clear existing callers
|
|
33
|
-
dataDb.exec(
|
|
34
|
+
dataDb.exec(`DELETE FROM ${t('py_route_callers')}`);
|
|
34
35
|
|
|
35
36
|
// Walk frontend files (TS/TSX/JS/JSX)
|
|
36
37
|
const frontendFiles = walkFrontendFiles(srcDir);
|
|
37
38
|
|
|
38
39
|
const insertStmt = dataDb.prepare(
|
|
39
|
-
|
|
40
|
+
`INSERT INTO ${t('py_route_callers')} (route_id, frontend_file, line, call_pattern) VALUES (?, ?, ?, ?)`
|
|
40
41
|
);
|
|
41
42
|
|
|
42
43
|
let count = 0;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import type Database from 'better-sqlite3';
|
|
5
5
|
import { getConfig } from '../config.ts';
|
|
6
|
+
import { t } from '../lib/sql-table-names.ts';
|
|
6
7
|
|
|
7
8
|
export interface DomainViolation {
|
|
8
9
|
sourceFile: string;
|
|
@@ -44,7 +45,7 @@ export function findPythonDomainViolations(dataDb: Database.Database): DomainVio
|
|
|
44
45
|
if (domains.length === 0) return [];
|
|
45
46
|
|
|
46
47
|
const imports = dataDb.prepare(
|
|
47
|
-
|
|
48
|
+
`SELECT source_file, target_file FROM ${t('py_imports')}`
|
|
48
49
|
).all() as { source_file: string; target_file: string }[];
|
|
49
50
|
|
|
50
51
|
const violations: DomainViolation[] = [];
|
|
@@ -74,7 +75,7 @@ export function findPythonDomainViolations(dataDb: Database.Database): DomainVio
|
|
|
74
75
|
*/
|
|
75
76
|
export function getPythonFilesInDomain(dataDb: Database.Database, domainName: string): string[] {
|
|
76
77
|
const allFiles = dataDb.prepare(
|
|
77
|
-
|
|
78
|
+
`SELECT DISTINCT source_file as f FROM ${t('py_imports')} UNION SELECT DISTINCT target_file as f FROM ${t('py_imports')}`
|
|
78
79
|
).all() as { f: string }[];
|
|
79
80
|
|
|
80
81
|
return allFiles
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import type Database from 'better-sqlite3';
|
|
5
5
|
import { classifyPythonFileDomain } from './domain-enforcer.ts';
|
|
6
|
+
import { t } from '../lib/sql-table-names.ts';
|
|
6
7
|
|
|
7
8
|
export interface PythonImpactReport {
|
|
8
9
|
file: string;
|
|
@@ -26,28 +27,28 @@ export function analyzePythonImpact(dataDb: Database.Database, file: string): Py
|
|
|
26
27
|
|
|
27
28
|
// 2. Routes defined in this file
|
|
28
29
|
const routes = dataDb.prepare(
|
|
29
|
-
|
|
30
|
+
`SELECT method, path, function_name FROM ${t('py_routes')} WHERE file = ?`
|
|
30
31
|
).all(file) as { method: string; path: string; function_name: string }[];
|
|
31
32
|
|
|
32
33
|
// 3. Models defined in this file
|
|
33
34
|
const models = dataDb.prepare(
|
|
34
|
-
|
|
35
|
+
`SELECT class_name, table_name FROM ${t('py_models')} WHERE file = ?`
|
|
35
36
|
).all(file) as { class_name: string; table_name: string | null }[];
|
|
36
37
|
|
|
37
38
|
// 4. Frontend callers (via routes in this file)
|
|
38
|
-
const routeIds = dataDb.prepare(
|
|
39
|
+
const routeIds = dataDb.prepare(`SELECT id FROM ${t('py_routes')} WHERE file = ?`).all(file) as { id: number }[];
|
|
39
40
|
const frontendCallers: string[] = [];
|
|
40
41
|
if (routeIds.length > 0) {
|
|
41
42
|
const placeholders = routeIds.map(() => '?').join(',');
|
|
42
43
|
const callers = dataDb.prepare(
|
|
43
|
-
`SELECT DISTINCT frontend_file FROM
|
|
44
|
+
`SELECT DISTINCT frontend_file FROM ${t('py_route_callers')} WHERE route_id IN (${placeholders})`
|
|
44
45
|
).all(...routeIds.map(r => r.id)) as { frontend_file: string }[];
|
|
45
46
|
frontendCallers.push(...callers.map(c => c.frontend_file));
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
// 5. Domain crossings
|
|
49
50
|
const imports = dataDb.prepare(
|
|
50
|
-
|
|
51
|
+
`SELECT target_file FROM ${t('py_imports')} WHERE source_file = ?`
|
|
51
52
|
).all(file) as { target_file: string }[];
|
|
52
53
|
|
|
53
54
|
const domainCrossings = imports
|
|
@@ -71,7 +72,7 @@ function collectTransitiveImporters(dataDb: Database.Database, file: string, max
|
|
|
71
72
|
let depth = 0;
|
|
72
73
|
|
|
73
74
|
const importerStmt = dataDb.prepare(
|
|
74
|
-
|
|
75
|
+
`SELECT source_file FROM ${t('py_imports')} WHERE target_file = ?`
|
|
75
76
|
);
|
|
76
77
|
|
|
77
78
|
while (queue.length > 0 && depth < maxDepth) {
|
|
@@ -6,6 +6,7 @@ import { resolve, join, relative, dirname } from 'path';
|
|
|
6
6
|
import type Database from 'better-sqlite3';
|
|
7
7
|
import { parsePythonImports } from './import-parser.ts';
|
|
8
8
|
import { getProjectRoot } from '../config.ts';
|
|
9
|
+
import { t } from '../lib/sql-table-names.ts';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Resolve a Python module path to a file path.
|
|
@@ -82,10 +83,10 @@ export function buildPythonImportIndex(dataDb: Database.Database, pythonRoot: st
|
|
|
82
83
|
const absRoot = resolve(projectRoot, pythonRoot);
|
|
83
84
|
|
|
84
85
|
// Clear existing Python import edges
|
|
85
|
-
dataDb.exec(
|
|
86
|
+
dataDb.exec(`DELETE FROM ${t('py_imports')}`);
|
|
86
87
|
|
|
87
88
|
const insertStmt = dataDb.prepare(
|
|
88
|
-
|
|
89
|
+
`INSERT INTO ${t('py_imports')} (source_file, target_file, import_type, imported_names, line) VALUES (?, ?, ?, ?, ?)`
|
|
89
90
|
);
|
|
90
91
|
|
|
91
92
|
const files = walkPythonFiles(absRoot, excludeDirs);
|
|
@@ -6,11 +6,12 @@ import { join, relative } from 'path';
|
|
|
6
6
|
import type Database from 'better-sqlite3';
|
|
7
7
|
import { parseAlembicMigration } from './migration-parser.ts';
|
|
8
8
|
import { getProjectRoot } from '../config.ts';
|
|
9
|
+
import { t } from '../lib/sql-table-names.ts';
|
|
9
10
|
|
|
10
11
|
export function buildPythonMigrationIndex(dataDb: Database.Database, alembicDir: string): number {
|
|
11
12
|
const projectRoot = getProjectRoot();
|
|
12
13
|
const absDir = join(projectRoot, alembicDir);
|
|
13
|
-
dataDb.exec(
|
|
14
|
+
dataDb.exec(`DELETE FROM ${t('py_migrations')}`);
|
|
14
15
|
|
|
15
16
|
// Look for version files in versions/ subdirectory
|
|
16
17
|
const versionsDir = join(absDir, 'versions');
|
|
@@ -28,7 +29,7 @@ export function buildPythonMigrationIndex(dataDb: Database.Database, alembicDir:
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
const insertStmt = dataDb.prepare(
|
|
31
|
-
|
|
32
|
+
`INSERT INTO ${t('py_migrations')} (revision, down_revision, file, description, operations, is_head) VALUES (?, ?, ?, ?, ?, ?)`
|
|
32
33
|
);
|
|
33
34
|
|
|
34
35
|
let count = 0;
|
|
@@ -80,11 +81,11 @@ export interface DriftReport {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
export function detectMigrationDrift(dataDb: Database.Database): DriftReport {
|
|
83
|
-
const models = dataDb.prepare(
|
|
84
|
+
const models = dataDb.prepare(`SELECT class_name, table_name, columns FROM ${t('py_models')} WHERE table_name IS NOT NULL`).all() as {
|
|
84
85
|
class_name: string; table_name: string; columns: string;
|
|
85
86
|
}[];
|
|
86
87
|
|
|
87
|
-
const migrations = dataDb.prepare(
|
|
88
|
+
const migrations = dataDb.prepare(`SELECT operations FROM ${t('py_migrations')}`).all() as { operations: string }[];
|
|
88
89
|
|
|
89
90
|
// Collect all tables and columns mentioned in migrations
|
|
90
91
|
const migratedTables = new Set<string>();
|
|
@@ -6,6 +6,7 @@ import { join, relative } from 'path';
|
|
|
6
6
|
import type Database from 'better-sqlite3';
|
|
7
7
|
import { parsePythonModels } from './model-parser.ts';
|
|
8
8
|
import { getProjectRoot } from '../config.ts';
|
|
9
|
+
import { t } from '../lib/sql-table-names.ts';
|
|
9
10
|
|
|
10
11
|
function walkPyFiles(dir: string, excludeDirs: string[]): string[] {
|
|
11
12
|
const files: string[] = [];
|
|
@@ -26,14 +27,14 @@ function walkPyFiles(dir: string, excludeDirs: string[]): string[] {
|
|
|
26
27
|
export function buildPythonModelIndex(dataDb: Database.Database, pythonRoot: string, excludeDirs: string[] = ['__pycache__', '.venv', 'venv', '.mypy_cache', '.pytest_cache']): number {
|
|
27
28
|
const projectRoot = getProjectRoot();
|
|
28
29
|
const absRoot = join(projectRoot, pythonRoot);
|
|
29
|
-
dataDb.exec(
|
|
30
|
-
dataDb.exec(
|
|
30
|
+
dataDb.exec(`DELETE FROM ${t('py_models')}`);
|
|
31
|
+
dataDb.exec(`DELETE FROM ${t('py_fk_edges')}`);
|
|
31
32
|
|
|
32
33
|
const insertModel = dataDb.prepare(
|
|
33
|
-
|
|
34
|
+
`INSERT INTO ${t('py_models')} (class_name, table_name, file, line, columns, relationships, foreign_keys) VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
34
35
|
);
|
|
35
36
|
const insertFk = dataDb.prepare(
|
|
36
|
-
|
|
37
|
+
`INSERT INTO ${t('py_fk_edges')} (source_table, source_column, target_table, target_column) VALUES (?, ?, ?, ?)`
|
|
37
38
|
);
|
|
38
39
|
|
|
39
40
|
const files = walkPyFiles(absRoot, excludeDirs);
|
|
@@ -6,6 +6,7 @@ import { join, relative } from 'path';
|
|
|
6
6
|
import type Database from 'better-sqlite3';
|
|
7
7
|
import { parsePythonRoutes } from './route-parser.ts';
|
|
8
8
|
import { getProjectRoot } from '../config.ts';
|
|
9
|
+
import { t } from '../lib/sql-table-names.ts';
|
|
9
10
|
|
|
10
11
|
function walkPyFiles(dir: string, excludeDirs: string[]): string[] {
|
|
11
12
|
const files: string[] = [];
|
|
@@ -26,11 +27,11 @@ function walkPyFiles(dir: string, excludeDirs: string[]): string[] {
|
|
|
26
27
|
export function buildPythonRouteIndex(dataDb: Database.Database, pythonRoot: string, excludeDirs: string[] = ['__pycache__', '.venv', 'venv', '.mypy_cache', '.pytest_cache']): number {
|
|
27
28
|
const projectRoot = getProjectRoot();
|
|
28
29
|
const absRoot = join(projectRoot, pythonRoot);
|
|
29
|
-
dataDb.exec(
|
|
30
|
-
dataDb.exec(
|
|
30
|
+
dataDb.exec(`DELETE FROM ${t('py_routes')}`);
|
|
31
|
+
dataDb.exec(`DELETE FROM ${t('py_route_callers')}`);
|
|
31
32
|
|
|
32
33
|
const insertStmt = dataDb.prepare(
|
|
33
|
-
|
|
34
|
+
`INSERT INTO ${t('py_routes')} (file, method, path, function_name, dependencies, request_model, response_model, is_authenticated, line) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
34
35
|
);
|
|
35
36
|
|
|
36
37
|
const files = walkPyFiles(absRoot, excludeDirs);
|
package/src/python-tools.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type Database from 'better-sqlite3';
|
|
|
5
5
|
import type { ToolDefinition, ToolResult } from './tools.ts';
|
|
6
6
|
import { getConfig } from './config.ts';
|
|
7
7
|
import { classifyPythonFileDomain } from './python/domain-enforcer.ts';
|
|
8
|
+
import { t } from './lib/sql-table-names.ts';
|
|
8
9
|
|
|
9
10
|
/** Get the configured tool prefix */
|
|
10
11
|
function p(name: string): string {
|
|
@@ -195,7 +196,7 @@ function handlePyImports(args: Record<string, unknown>, dataDb: Database.Databas
|
|
|
195
196
|
if (depth > maxDepth || visited.has(f)) return;
|
|
196
197
|
visited.add(f);
|
|
197
198
|
const imports = dataDb.prepare(
|
|
198
|
-
|
|
199
|
+
`SELECT target_file, imported_names FROM ${t('py_imports')} WHERE source_file = ?`
|
|
199
200
|
).all(f) as { target_file: string; imported_names: string }[];
|
|
200
201
|
for (const imp of imports) {
|
|
201
202
|
const indent = ' '.repeat(depth);
|
|
@@ -207,7 +208,7 @@ function handlePyImports(args: Record<string, unknown>, dataDb: Database.Databas
|
|
|
207
208
|
traverse(file, 0);
|
|
208
209
|
} else {
|
|
209
210
|
const imports = dataDb.prepare(
|
|
210
|
-
|
|
211
|
+
`SELECT target_file, import_type, imported_names, line FROM ${t('py_imports')} WHERE source_file = ? ORDER BY line`
|
|
211
212
|
).all(file) as { target_file: string; import_type: string; imported_names: string; line: number }[];
|
|
212
213
|
lines.push(`## Imports from ${file} (${imports.length} edges)`);
|
|
213
214
|
for (const imp of imports) {
|
|
@@ -217,7 +218,7 @@ function handlePyImports(args: Record<string, unknown>, dataDb: Database.Databas
|
|
|
217
218
|
}
|
|
218
219
|
} else if (importedBy) {
|
|
219
220
|
const importers = dataDb.prepare(
|
|
220
|
-
|
|
221
|
+
`SELECT source_file, imported_names, line FROM ${t('py_imports')} WHERE target_file = ? ORDER BY source_file`
|
|
221
222
|
).all(importedBy) as { source_file: string; imported_names: string; line: number }[];
|
|
222
223
|
lines.push(`## Files importing ${importedBy} (${importers.length} edges)`);
|
|
223
224
|
for (const imp of importers) {
|
|
@@ -225,8 +226,8 @@ function handlePyImports(args: Record<string, unknown>, dataDb: Database.Databas
|
|
|
225
226
|
lines.push(`- ${imp.source_file}:${imp.line}${names.length > 0 ? ' (' + names.join(', ') + ')' : ''}`);
|
|
226
227
|
}
|
|
227
228
|
} else {
|
|
228
|
-
const total = (dataDb.prepare(
|
|
229
|
-
const files = (dataDb.prepare(
|
|
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;
|
|
230
231
|
lines.push('## Python Import Index Summary');
|
|
231
232
|
lines.push(`- Total import edges: ${total}`);
|
|
232
233
|
lines.push(`- Files with imports: ${files}`);
|
|
@@ -241,7 +242,7 @@ function handlePyImports(args: Record<string, unknown>, dataDb: Database.Databas
|
|
|
241
242
|
function handlePyRoutes(args: Record<string, unknown>, dataDb: Database.Database): ToolResult {
|
|
242
243
|
const lines: string[] = [];
|
|
243
244
|
const limit = Math.min(Math.max((args.limit as number) || 500, 1), 5000);
|
|
244
|
-
let query =
|
|
245
|
+
let query = `SELECT * FROM ${t('py_routes')} WHERE 1=1`;
|
|
245
246
|
const params: unknown[] = [];
|
|
246
247
|
|
|
247
248
|
if (args.method) { query += ' AND method = ?'; params.push((args.method as string).toUpperCase()); }
|
|
@@ -258,7 +259,7 @@ function handlePyRoutes(args: Record<string, unknown>, dataDb: Database.Database
|
|
|
258
259
|
}[];
|
|
259
260
|
|
|
260
261
|
if (args.uncoupled) {
|
|
261
|
-
const callerCountStmt = dataDb.prepare(
|
|
262
|
+
const callerCountStmt = dataDb.prepare(`SELECT COUNT(*) as cnt FROM ${t('py_route_callers')} WHERE route_id = ?`);
|
|
262
263
|
const uncoupledRoutes = routes.filter(r => {
|
|
263
264
|
const callers = (callerCountStmt.get(r.id) as { cnt: number }).cnt;
|
|
264
265
|
return callers === 0;
|
|
@@ -286,8 +287,8 @@ function handlePyCoupling(args: Record<string, unknown>, dataDb: Database.Databa
|
|
|
286
287
|
let query = `
|
|
287
288
|
SELECT rc.frontend_file, rc.call_pattern, rc.line as caller_line,
|
|
288
289
|
r.method, r.path, r.function_name, r.file as backend_file
|
|
289
|
-
FROM
|
|
290
|
-
JOIN
|
|
290
|
+
FROM ${t('py_route_callers')} rc
|
|
291
|
+
JOIN ${t('py_routes')} r ON rc.route_id = r.id
|
|
291
292
|
WHERE 1=1
|
|
292
293
|
`;
|
|
293
294
|
const params: unknown[] = [];
|
|
@@ -319,7 +320,7 @@ function handlePyModels(args: Record<string, unknown>, dataDb: Database.Database
|
|
|
319
320
|
const lines: string[] = [];
|
|
320
321
|
|
|
321
322
|
if (args.fk_graph) {
|
|
322
|
-
const edges = dataDb.prepare(
|
|
323
|
+
const edges = dataDb.prepare(`SELECT * FROM ${t('py_fk_edges')} ORDER BY source_table`).all() as {
|
|
323
324
|
source_table: string; source_column: string; target_table: string; target_column: string;
|
|
324
325
|
}[];
|
|
325
326
|
lines.push(`## Foreign Key Graph (${edges.length} edges)`);
|
|
@@ -329,7 +330,7 @@ function handlePyModels(args: Record<string, unknown>, dataDb: Database.Database
|
|
|
329
330
|
} else if (args.model || args.table) {
|
|
330
331
|
const search = (args.model || args.table) as string;
|
|
331
332
|
const model = dataDb.prepare(
|
|
332
|
-
|
|
333
|
+
`SELECT * FROM ${t('py_models')} WHERE class_name = ? OR table_name = ?`
|
|
333
334
|
).get(search, search) as {
|
|
334
335
|
class_name: string; table_name: string; file: string; line: number;
|
|
335
336
|
columns: string; relationships: string; foreign_keys: string;
|
|
@@ -365,7 +366,7 @@ function handlePyModels(args: Record<string, unknown>, dataDb: Database.Database
|
|
|
365
366
|
}
|
|
366
367
|
} else {
|
|
367
368
|
const limit = Math.min(Math.max((args.limit as number) || 500, 1), 5000);
|
|
368
|
-
const models = dataDb.prepare(`SELECT class_name, table_name, file, line FROM
|
|
369
|
+
const models = dataDb.prepare(`SELECT class_name, table_name, file, line FROM ${t('py_models')} ORDER BY class_name LIMIT ${limit}`).all() as {
|
|
369
370
|
class_name: string; table_name: string; file: string; line: number;
|
|
370
371
|
}[];
|
|
371
372
|
lines.push(`## Python Models (${models.length})`);
|
|
@@ -382,10 +383,10 @@ function handlePyMigrations(args: Record<string, unknown>, dataDb: Database.Data
|
|
|
382
383
|
|
|
383
384
|
if (args.drift) {
|
|
384
385
|
// Compare models against migration operations
|
|
385
|
-
const models = dataDb.prepare(
|
|
386
|
+
const models = dataDb.prepare(`SELECT class_name, table_name, columns FROM ${t('py_models')}`).all() as {
|
|
386
387
|
class_name: string; table_name: string; columns: string;
|
|
387
388
|
}[];
|
|
388
|
-
const migrations = dataDb.prepare(
|
|
389
|
+
const migrations = dataDb.prepare(`SELECT revision, operations FROM ${t('py_migrations')}`).all() as {
|
|
389
390
|
revision: string; operations: string;
|
|
390
391
|
}[];
|
|
391
392
|
|
|
@@ -409,7 +410,7 @@ function handlePyMigrations(args: Record<string, unknown>, dataDb: Database.Data
|
|
|
409
410
|
}
|
|
410
411
|
} else if (args.chain) {
|
|
411
412
|
const limit = Math.min(Math.max((args.limit as number) || 500, 1), 5000);
|
|
412
|
-
const migrations = dataDb.prepare(`SELECT * FROM
|
|
413
|
+
const migrations = dataDb.prepare(`SELECT * FROM ${t('py_migrations')} ORDER BY id LIMIT ${limit}`).all() as {
|
|
413
414
|
revision: string; down_revision: string | null; file: string; description: string | null; is_head: number;
|
|
414
415
|
}[];
|
|
415
416
|
lines.push(`## Migration Chain (${migrations.length} revisions)`);
|
|
@@ -418,7 +419,7 @@ function handlePyMigrations(args: Record<string, unknown>, dataDb: Database.Data
|
|
|
418
419
|
lines.push(`- ${m.revision}${head} ← ${m.down_revision || 'BASE'}: ${m.description || '(no description)'} (${m.file})`);
|
|
419
420
|
}
|
|
420
421
|
} else if (args.revision) {
|
|
421
|
-
const m = dataDb.prepare(
|
|
422
|
+
const m = dataDb.prepare(`SELECT * FROM ${t('py_migrations')} WHERE revision = ?`).get(args.revision) as {
|
|
422
423
|
revision: string; down_revision: string | null; file: string; description: string | null;
|
|
423
424
|
operations: string; is_head: number;
|
|
424
425
|
} | undefined;
|
|
@@ -435,8 +436,8 @@ function handlePyMigrations(args: Record<string, unknown>, dataDb: Database.Data
|
|
|
435
436
|
}
|
|
436
437
|
}
|
|
437
438
|
} else {
|
|
438
|
-
const total = (dataDb.prepare(
|
|
439
|
-
const head = dataDb.prepare(
|
|
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;
|
|
440
441
|
lines.push('## Migration Summary');
|
|
441
442
|
lines.push(`- Total revisions: ${total}`);
|
|
442
443
|
lines.push(`- Current head: ${head?.revision || 'N/A'}`);
|
|
@@ -460,7 +461,7 @@ function handlePyDomains(args: Record<string, unknown>, dataDb: Database.Databas
|
|
|
460
461
|
lines.push(`Allowed imports from: ${domainConfig.allowed_imports_from.join(', ') || 'any'}`);
|
|
461
462
|
}
|
|
462
463
|
} else if (args.crossings) {
|
|
463
|
-
const imports = dataDb.prepare(
|
|
464
|
+
const imports = dataDb.prepare(`SELECT source_file, target_file FROM ${t('py_imports')}`).all() as {
|
|
464
465
|
source_file: string; target_file: string;
|
|
465
466
|
}[];
|
|
466
467
|
const violations: string[] = [];
|
|
@@ -481,7 +482,7 @@ function handlePyDomains(args: Record<string, unknown>, dataDb: Database.Databas
|
|
|
481
482
|
if (violations.length > 50) lines.push(`... and ${violations.length - 50} more`);
|
|
482
483
|
} else if (args.domain) {
|
|
483
484
|
const domainName = args.domain as string;
|
|
484
|
-
const allFiles = dataDb.prepare(
|
|
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 }[];
|
|
485
486
|
const filesInDomain = allFiles.filter(f => classifyPythonFileDomain(f.source_file) === domainName);
|
|
486
487
|
lines.push(`## Domain: ${domainName} (${filesInDomain.length} files)`);
|
|
487
488
|
for (const f of filesInDomain) {
|
|
@@ -503,7 +504,7 @@ function handlePyImpact(args: Record<string, unknown>, dataDb: Database.Database
|
|
|
503
504
|
|
|
504
505
|
// 1. Who imports this file
|
|
505
506
|
const importedBy = dataDb.prepare(
|
|
506
|
-
|
|
507
|
+
`SELECT source_file FROM ${t('py_imports')} WHERE target_file = ?`
|
|
507
508
|
).all(file) as { source_file: string }[];
|
|
508
509
|
lines.push(`### Imported By (${importedBy.length} files)`);
|
|
509
510
|
for (const imp of importedBy.slice(0, 20)) {
|
|
@@ -512,7 +513,7 @@ function handlePyImpact(args: Record<string, unknown>, dataDb: Database.Database
|
|
|
512
513
|
|
|
513
514
|
// 2. Routes in this file
|
|
514
515
|
const routes = dataDb.prepare(
|
|
515
|
-
|
|
516
|
+
`SELECT method, path, function_name FROM ${t('py_routes')} WHERE file = ?`
|
|
516
517
|
).all(file) as { method: string; path: string; function_name: string }[];
|
|
517
518
|
if (routes.length > 0) {
|
|
518
519
|
lines.push(`\n### Routes Defined (${routes.length})`);
|
|
@@ -523,7 +524,7 @@ function handlePyImpact(args: Record<string, unknown>, dataDb: Database.Database
|
|
|
523
524
|
|
|
524
525
|
// 3. Models in this file
|
|
525
526
|
const models = dataDb.prepare(
|
|
526
|
-
|
|
527
|
+
`SELECT class_name, table_name FROM ${t('py_models')} WHERE file = ?`
|
|
527
528
|
).all(file) as { class_name: string; table_name: string }[];
|
|
528
529
|
if (models.length > 0) {
|
|
529
530
|
lines.push(`\n### Models Defined (${models.length})`);
|
|
@@ -533,11 +534,11 @@ function handlePyImpact(args: Record<string, unknown>, dataDb: Database.Database
|
|
|
533
534
|
}
|
|
534
535
|
|
|
535
536
|
// 4. Frontend callers (via routes)
|
|
536
|
-
const routeIds = dataDb.prepare(
|
|
537
|
+
const routeIds = dataDb.prepare(`SELECT id FROM ${t('py_routes')} WHERE file = ?`).all(file) as { id: number }[];
|
|
537
538
|
if (routeIds.length > 0) {
|
|
538
539
|
const placeholders = routeIds.map(() => '?').join(',');
|
|
539
540
|
const callers = dataDb.prepare(
|
|
540
|
-
`SELECT DISTINCT frontend_file FROM
|
|
541
|
+
`SELECT DISTINCT frontend_file FROM ${t('py_route_callers')} WHERE route_id IN (${placeholders})`
|
|
541
542
|
).all(...routeIds.map(r => r.id)) as { frontend_file: string }[];
|
|
542
543
|
if (callers.length > 0) {
|
|
543
544
|
lines.push(`\n### Frontend Callers (${callers.length} files)`);
|
|
@@ -566,7 +567,7 @@ function handlePyContext(args: Record<string, unknown>, dataDb: Database.Databas
|
|
|
566
567
|
|
|
567
568
|
// Imports
|
|
568
569
|
const imports = dataDb.prepare(
|
|
569
|
-
|
|
570
|
+
`SELECT target_file, imported_names FROM ${t('py_imports')} WHERE source_file = ? LIMIT 20`
|
|
570
571
|
).all(file) as { target_file: string; imported_names: string }[];
|
|
571
572
|
if (imports.length > 0) {
|
|
572
573
|
lines.push('\n### Imports');
|
|
@@ -578,7 +579,7 @@ function handlePyContext(args: Record<string, unknown>, dataDb: Database.Databas
|
|
|
578
579
|
|
|
579
580
|
// Imported by
|
|
580
581
|
const importedBy = dataDb.prepare(
|
|
581
|
-
|
|
582
|
+
`SELECT source_file FROM ${t('py_imports')} WHERE target_file = ? LIMIT 10`
|
|
582
583
|
).all(file) as { source_file: string }[];
|
|
583
584
|
if (importedBy.length > 0) {
|
|
584
585
|
lines.push('\n### Imported By');
|
|
@@ -588,7 +589,7 @@ function handlePyContext(args: Record<string, unknown>, dataDb: Database.Databas
|
|
|
588
589
|
}
|
|
589
590
|
|
|
590
591
|
// Routes
|
|
591
|
-
const routes = dataDb.prepare(
|
|
592
|
+
const routes = dataDb.prepare(`SELECT method, path, function_name, line FROM ${t('py_routes')} WHERE file = ?`)
|
|
592
593
|
.all(file) as { method: string; path: string; function_name: string; line: number }[];
|
|
593
594
|
if (routes.length > 0) {
|
|
594
595
|
lines.push('\n### Routes');
|
|
@@ -598,7 +599,7 @@ function handlePyContext(args: Record<string, unknown>, dataDb: Database.Databas
|
|
|
598
599
|
}
|
|
599
600
|
|
|
600
601
|
// Models
|
|
601
|
-
const models = dataDb.prepare(
|
|
602
|
+
const models = dataDb.prepare(`SELECT class_name, table_name, line FROM ${t('py_models')} WHERE file = ?`)
|
|
602
603
|
.all(file) as { class_name: string; table_name: string; line: number }[];
|
|
603
604
|
if (models.length > 0) {
|
|
604
605
|
lines.push('\n### Models');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// AUTO-GENERATED by scripts/bundle-license-pubkey.mjs at 2026-05-17T15:
|
|
1
|
+
// AUTO-GENERATED by scripts/bundle-license-pubkey.mjs at 2026-05-17T15:51:32.373Z.
|
|
2
2
|
// Source pem: packages/core/security/license-pubkey.pem
|
|
3
3
|
// RAW-bytes sha256: 18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475
|
|
4
4
|
// DO NOT EDIT — regenerate via `node scripts/bundle-license-pubkey.mjs` or
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-17T15:
|
|
1
|
+
// AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-17T15:51:32.309Z.
|
|
2
2
|
// Source pem: packages/core/security/registry-pubkey.pem
|
|
3
3
|
// RAW-bytes sha256: 3b6226d036c472e533110d11a7d0cd2773ce1d7d4f1003517d5bd69c5418ed4c
|
|
4
4
|
// DO NOT EDIT — regenerate via `node scripts/bundle-pubkey.mjs` or
|