@massu/core 0.1.1 → 0.4.0
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/commands/_shared-preamble.md +76 -0
- package/commands/massu-audit-deps.md +211 -0
- package/commands/massu-changelog.md +174 -0
- package/commands/massu-cleanup.md +315 -0
- package/commands/massu-commit.md +481 -0
- package/commands/massu-create-plan.md +752 -0
- package/commands/massu-dead-code.md +131 -0
- package/commands/massu-debug.md +484 -0
- package/commands/massu-deploy.md +91 -0
- package/commands/massu-deps.md +374 -0
- package/commands/massu-doc-gen.md +279 -0
- package/commands/massu-docs.md +364 -0
- package/commands/massu-estimate.md +313 -0
- package/commands/massu-golden-path.md +973 -0
- package/commands/massu-guide.md +167 -0
- package/commands/massu-hotfix.md +480 -0
- package/commands/massu-loop-playwright.md +837 -0
- package/commands/massu-loop.md +775 -0
- package/commands/massu-new-feature.md +511 -0
- package/commands/massu-parity.md +214 -0
- package/commands/massu-plan.md +456 -0
- package/commands/massu-push-light.md +207 -0
- package/commands/massu-push.md +434 -0
- package/commands/massu-refactor.md +410 -0
- package/commands/massu-release.md +363 -0
- package/commands/massu-review.md +238 -0
- package/commands/massu-simplify.md +281 -0
- package/commands/massu-status.md +278 -0
- package/commands/massu-tdd.md +201 -0
- package/commands/massu-test.md +516 -0
- package/commands/massu-verify-playwright.md +281 -0
- package/commands/massu-verify.md +667 -0
- package/dist/cli.js +7772 -3140
- package/dist/hooks/cost-tracker.js +103 -40
- package/dist/hooks/post-edit-context.js +74 -8
- package/dist/hooks/post-tool-use.js +268 -106
- package/dist/hooks/pre-compact.js +167 -43
- package/dist/hooks/pre-delete-check.js +159 -42
- package/dist/hooks/quality-event.js +103 -40
- package/dist/hooks/security-gate.js +29 -0
- package/dist/hooks/session-end.js +143 -84
- package/dist/hooks/session-start.js +186 -49
- package/dist/hooks/user-prompt.js +189 -43
- package/package.json +10 -15
- package/src/adr-generator.ts +9 -2
- package/src/analytics.ts +9 -3
- package/src/audit-trail.ts +10 -3
- package/src/backfill-sessions.ts +5 -4
- package/src/cli.ts +6 -0
- package/src/cloud-sync.ts +14 -18
- package/src/commands/doctor.ts +193 -6
- package/src/commands/init.ts +230 -5
- package/src/commands/install-commands.ts +137 -0
- package/src/config.ts +68 -2
- package/src/cost-tracker.ts +11 -6
- package/src/db.ts +115 -2
- package/src/dependency-scorer.ts +9 -2
- package/src/docs-tools.ts +21 -16
- package/src/hooks/post-edit-context.ts +4 -4
- package/src/hooks/post-tool-use.ts +130 -0
- package/src/hooks/pre-compact.ts +23 -1
- package/src/hooks/pre-delete-check.ts +92 -4
- package/src/hooks/security-gate.ts +32 -0
- package/src/hooks/session-end.ts +3 -3
- package/src/hooks/session-start.ts +99 -6
- package/src/hooks/user-prompt.ts +46 -1
- package/src/import-resolver.ts +2 -1
- package/src/knowledge-db.ts +169 -0
- package/src/knowledge-indexer.ts +704 -0
- package/src/knowledge-tools.ts +1413 -0
- package/src/license.ts +482 -0
- package/src/memory-db.ts +1364 -23
- package/src/memory-tools.ts +14 -15
- package/src/observability-tools.ts +13 -2
- package/src/observation-extractor.ts +11 -4
- package/src/page-deps.ts +3 -2
- package/src/prompt-analyzer.ts +9 -2
- package/src/python/coupling-detector.ts +124 -0
- package/src/python/domain-enforcer.ts +83 -0
- package/src/python/impact-analyzer.ts +95 -0
- package/src/python/import-parser.ts +244 -0
- package/src/python/import-resolver.ts +135 -0
- package/src/python/migration-indexer.ts +115 -0
- package/src/python/migration-parser.ts +332 -0
- package/src/python/model-indexer.ts +70 -0
- package/src/python/model-parser.ts +279 -0
- package/src/python/route-indexer.ts +58 -0
- package/src/python/route-parser.ts +317 -0
- package/src/python-tools.ts +629 -0
- package/src/regression-detector.ts +9 -3
- package/src/security-scorer.ts +9 -2
- package/src/sentinel-db.ts +45 -89
- package/src/sentinel-tools.ts +8 -11
- package/src/server.ts +29 -7
- package/src/session-archiver.ts +4 -5
- package/src/team-knowledge.ts +9 -2
- package/src/tools.ts +1032 -44
- package/src/validate-features-runner.ts +0 -1
- package/src/validation-engine.ts +9 -2
- package/README.md +0 -40
- package/dist/server.js +0 -7008
- package/src/__tests__/adr-generator.test.ts +0 -260
- package/src/__tests__/analytics.test.ts +0 -282
- package/src/__tests__/audit-trail.test.ts +0 -382
- package/src/__tests__/backfill-sessions.test.ts +0 -690
- package/src/__tests__/cli.test.ts +0 -290
- package/src/__tests__/cloud-sync.test.ts +0 -261
- package/src/__tests__/config-sections.test.ts +0 -359
- package/src/__tests__/config.test.ts +0 -732
- package/src/__tests__/cost-tracker.test.ts +0 -348
- package/src/__tests__/db.test.ts +0 -177
- package/src/__tests__/dependency-scorer.test.ts +0 -325
- package/src/__tests__/docs-integration.test.ts +0 -178
- package/src/__tests__/docs-tools.test.ts +0 -199
- package/src/__tests__/domains.test.ts +0 -236
- package/src/__tests__/hooks.test.ts +0 -221
- package/src/__tests__/import-resolver.test.ts +0 -95
- package/src/__tests__/integration/path-traversal.test.ts +0 -134
- package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
- package/src/__tests__/integration/tool-registration.test.ts +0 -146
- package/src/__tests__/memory-db.test.ts +0 -404
- package/src/__tests__/memory-enhancements.test.ts +0 -316
- package/src/__tests__/memory-tools.test.ts +0 -199
- package/src/__tests__/middleware-tree.test.ts +0 -177
- package/src/__tests__/observability-tools.test.ts +0 -595
- package/src/__tests__/observability.test.ts +0 -437
- package/src/__tests__/observation-extractor.test.ts +0 -167
- package/src/__tests__/page-deps.test.ts +0 -60
- package/src/__tests__/prompt-analyzer.test.ts +0 -298
- package/src/__tests__/regression-detector.test.ts +0 -295
- package/src/__tests__/rules.test.ts +0 -87
- package/src/__tests__/schema-mapper.test.ts +0 -29
- package/src/__tests__/security-scorer.test.ts +0 -238
- package/src/__tests__/security-utils.test.ts +0 -175
- package/src/__tests__/sentinel-db.test.ts +0 -491
- package/src/__tests__/sentinel-scanner.test.ts +0 -750
- package/src/__tests__/sentinel-tools.test.ts +0 -324
- package/src/__tests__/sentinel-types.test.ts +0 -750
- package/src/__tests__/server.test.ts +0 -452
- package/src/__tests__/session-archiver.test.ts +0 -524
- package/src/__tests__/session-state-generator.test.ts +0 -900
- package/src/__tests__/team-knowledge.test.ts +0 -327
- package/src/__tests__/tools.test.ts +0 -340
- package/src/__tests__/transcript-parser.test.ts +0 -195
- package/src/__tests__/trpc-index.test.ts +0 -25
- package/src/__tests__/validate-features-runner.test.ts +0 -517
- package/src/__tests__/validation-engine.test.ts +0 -300
- package/src/core-tools.ts +0 -685
- package/src/memory-queries.ts +0 -804
- package/src/memory-schema.ts +0 -546
- package/src/tool-helpers.ts +0 -41
package/src/memory-tools.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
3
|
|
|
4
4
|
import type Database from 'better-sqlite3';
|
|
5
|
-
import type { ToolDefinition, ToolResult } from './
|
|
6
|
-
import { p, text } from './tool-helpers.ts';
|
|
5
|
+
import type { ToolDefinition, ToolResult } from './tools.ts';
|
|
7
6
|
import {
|
|
8
7
|
searchObservations,
|
|
9
8
|
getRecentObservations,
|
|
@@ -16,21 +15,15 @@ import {
|
|
|
16
15
|
} from './memory-db.ts';
|
|
17
16
|
import { getConfig } from './config.ts';
|
|
18
17
|
|
|
18
|
+
/** Prefix a base tool name with the configured tool prefix. */
|
|
19
|
+
function p(baseName: string): string {
|
|
20
|
+
return `${getConfig().toolPrefix}_${baseName}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
// ============================================================
|
|
20
24
|
// P4-001 through P4-006: MCP Memory Tools
|
|
21
25
|
// ============================================================
|
|
22
26
|
|
|
23
|
-
const MEMORY_BASE_NAMES = new Set([
|
|
24
|
-
'memory_search', 'memory_timeline', 'memory_detail',
|
|
25
|
-
'memory_sessions', 'memory_failures', 'memory_ingest',
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
export function isMemoryTool(name: string): boolean {
|
|
29
|
-
const pfx = getConfig().toolPrefix + '_';
|
|
30
|
-
const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
|
|
31
|
-
return MEMORY_BASE_NAMES.has(baseName);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
27
|
/**
|
|
35
28
|
* Get all memory tool definitions.
|
|
36
29
|
*/
|
|
@@ -45,7 +38,7 @@ export function getMemoryToolDefinitions(): ToolDefinition[] {
|
|
|
45
38
|
properties: {
|
|
46
39
|
query: { type: 'string', description: 'Search text (FTS5 query syntax supported)' },
|
|
47
40
|
type: { type: 'string', description: 'Filter by observation type (decision, bugfix, feature, failed_attempt, cr_violation, vr_check, etc.)' },
|
|
48
|
-
cr_rule: { type: 'string', description: 'Filter by CR rule (e.g., CR-
|
|
41
|
+
cr_rule: { type: 'string', description: 'Filter by CR rule (e.g., CR-9)' },
|
|
49
42
|
date_from: { type: 'string', description: 'Start date (ISO format)' },
|
|
50
43
|
limit: { type: 'number', description: 'Max results (default: 20)' },
|
|
51
44
|
},
|
|
@@ -122,7 +115,7 @@ export function getMemoryToolDefinitions(): ToolDefinition[] {
|
|
|
122
115
|
title: { type: 'string', description: 'Short description' },
|
|
123
116
|
detail: { type: 'string', description: 'Full context' },
|
|
124
117
|
importance: { type: 'number', description: 'Override importance (1-5, default: auto-assigned)' },
|
|
125
|
-
cr_rule: { type: 'string', description: 'Link to CR rule (e.g., CR-
|
|
118
|
+
cr_rule: { type: 'string', description: 'Link to CR rule (e.g., CR-9)' },
|
|
126
119
|
plan_item: { type: 'string', description: 'Link to plan item (e.g., P2-003)' },
|
|
127
120
|
files: {
|
|
128
121
|
type: 'array',
|
|
@@ -381,7 +374,13 @@ function handleIngest(args: Record<string, unknown>, db: Database.Database): Too
|
|
|
381
374
|
return text(`Observation #${id} recorded successfully.\nType: ${type}\nTitle: ${title}\nImportance: ${importance}\nSession: ${activeSession.session_id.slice(0, 8)}...`);
|
|
382
375
|
}
|
|
383
376
|
|
|
377
|
+
// ============================================================
|
|
378
|
+
// Helpers
|
|
379
|
+
// ============================================================
|
|
384
380
|
|
|
381
|
+
function text(content: string): ToolResult {
|
|
382
|
+
return { content: [{ type: 'text', text: content }] };
|
|
383
|
+
}
|
|
385
384
|
|
|
386
385
|
function safeParseJson(json: string, fallback: unknown): unknown {
|
|
387
386
|
try {
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
3
|
|
|
4
4
|
import type Database from 'better-sqlite3';
|
|
5
|
-
import type { ToolDefinition, ToolResult } from './
|
|
6
|
-
import { p, text } from './tool-helpers.ts';
|
|
5
|
+
import type { ToolDefinition, ToolResult } from './tools.ts';
|
|
7
6
|
import {
|
|
8
7
|
getConversationTurns,
|
|
9
8
|
searchConversationTurns,
|
|
@@ -14,6 +13,11 @@ import {
|
|
|
14
13
|
} from './memory-db.ts';
|
|
15
14
|
import { getConfig } from './config.ts';
|
|
16
15
|
|
|
16
|
+
/** Prefix a base tool name with the configured tool prefix. */
|
|
17
|
+
function p(baseName: string): string {
|
|
18
|
+
return `${getConfig().toolPrefix}_${baseName}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
// ============================================================
|
|
18
22
|
// Observability MCP Tools (P3-001 through P3-004)
|
|
19
23
|
// ============================================================
|
|
@@ -330,3 +334,10 @@ function handleSessionStats(args: Record<string, unknown>, db: Database.Database
|
|
|
330
334
|
return text(lines.join('\n'));
|
|
331
335
|
}
|
|
332
336
|
|
|
337
|
+
// ============================================================
|
|
338
|
+
// Helpers
|
|
339
|
+
// ============================================================
|
|
340
|
+
|
|
341
|
+
function text(content: string): ToolResult {
|
|
342
|
+
return { content: [{ type: 'text', text: content }] };
|
|
343
|
+
}
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
import type { AddObservationOpts } from './memory-db.ts';
|
|
15
15
|
import { assignImportance } from './memory-db.ts';
|
|
16
16
|
import { detectDecisionPatterns } from './adr-generator.ts';
|
|
17
|
-
import { getProjectRoot } from './config.ts';
|
|
17
|
+
import { getProjectRoot, getConfig, getResolvedPaths } from './config.ts';
|
|
18
|
+
import { homedir } from 'os';
|
|
18
19
|
|
|
19
20
|
// ============================================================
|
|
20
21
|
// P2-002: Observation Extractor
|
|
@@ -209,8 +210,10 @@ function classifyToolCall(tc: ParsedToolCall): ExtractedObservation | null {
|
|
|
209
210
|
|
|
210
211
|
case 'Read': {
|
|
211
212
|
const filePath = tc.input.file_path as string ?? 'unknown';
|
|
212
|
-
// Only keep reads of interesting files (plan files,
|
|
213
|
-
|
|
213
|
+
// Only keep reads of interesting files (plan files, knowledge source files, etc.)
|
|
214
|
+
const knowledgeSourceFiles = getConfig().conventions?.knowledgeSourceFiles ?? ['CLAUDE.md', 'MEMORY.md', 'corrections.md'];
|
|
215
|
+
const plansDir = getResolvedPaths().plansDir;
|
|
216
|
+
if (filePath.includes(plansDir) || knowledgeSourceFiles.some(f => filePath.includes(f))) {
|
|
214
217
|
const title = `Read: ${shortenPath(filePath)}`;
|
|
215
218
|
return {
|
|
216
219
|
type: 'discovery',
|
|
@@ -352,7 +355,11 @@ function shortenPath(filePath: string): string {
|
|
|
352
355
|
if (filePath.startsWith(root + '/')) {
|
|
353
356
|
return filePath.slice(root.length + 1);
|
|
354
357
|
}
|
|
355
|
-
|
|
358
|
+
const home = homedir();
|
|
359
|
+
if (filePath.startsWith(home + '/')) {
|
|
360
|
+
return '~/' + filePath.slice(home.length + 1);
|
|
361
|
+
}
|
|
362
|
+
return filePath;
|
|
356
363
|
}
|
|
357
364
|
|
|
358
365
|
/**
|
package/src/page-deps.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { readFileSync, existsSync } from 'fs';
|
|
|
5
5
|
import { resolve } from 'path';
|
|
6
6
|
import type Database from 'better-sqlite3';
|
|
7
7
|
import { getConfig, getProjectRoot } from './config.ts';
|
|
8
|
+
import { ensureWithinRoot } from './security-utils.ts';
|
|
8
9
|
|
|
9
10
|
export interface PageChain {
|
|
10
11
|
page: string;
|
|
@@ -89,7 +90,7 @@ function findRouterCalls(files: string[]): string[] {
|
|
|
89
90
|
const projectRoot = getProjectRoot();
|
|
90
91
|
|
|
91
92
|
for (const file of files) {
|
|
92
|
-
const absPath = resolve(projectRoot, file);
|
|
93
|
+
const absPath = ensureWithinRoot(resolve(projectRoot, file), projectRoot);
|
|
93
94
|
if (!existsSync(absPath)) continue;
|
|
94
95
|
|
|
95
96
|
try {
|
|
@@ -120,7 +121,7 @@ function findTablesFromRouters(routerNames: string[], dataDb: Database.Database)
|
|
|
120
121
|
).all(routerName) as { router_file: string }[];
|
|
121
122
|
|
|
122
123
|
for (const proc of procs) {
|
|
123
|
-
const absPath = resolve(getProjectRoot(), proc.router_file);
|
|
124
|
+
const absPath = ensureWithinRoot(resolve(getProjectRoot(), proc.router_file), getProjectRoot());
|
|
124
125
|
if (!existsSync(absPath)) continue;
|
|
125
126
|
|
|
126
127
|
try {
|
package/src/prompt-analyzer.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
3
|
|
|
4
4
|
import type Database from 'better-sqlite3';
|
|
5
|
-
import type { ToolDefinition, ToolResult } from './
|
|
6
|
-
import { p, text } from './tool-helpers.ts';
|
|
5
|
+
import type { ToolDefinition, ToolResult } from './tools.ts';
|
|
7
6
|
import { createHash } from 'crypto';
|
|
8
7
|
import { getConfig } from './config.ts';
|
|
9
8
|
import { escapeRegex, redactSensitiveContent } from './security-utils.ts';
|
|
@@ -12,6 +11,11 @@ import { escapeRegex, redactSensitiveContent } from './security-utils.ts';
|
|
|
12
11
|
// Prompt Effectiveness Analysis
|
|
13
12
|
// ============================================================
|
|
14
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
|
+
|
|
15
19
|
/** Default success/failure indicators. Can be overridden via config.analytics.prompts */
|
|
16
20
|
const DEFAULT_SUCCESS_INDICATORS = ['committed', 'approved', 'looks good', 'perfect', 'great', 'thanks'];
|
|
17
21
|
const DEFAULT_FAILURE_INDICATORS = ['revert', 'wrong', "that's not", 'undo', 'incorrect'];
|
|
@@ -323,3 +327,6 @@ function handleSuggestions(args: Record<string, unknown>, db: Database.Database)
|
|
|
323
327
|
return text(lines.join('\n'));
|
|
324
328
|
}
|
|
325
329
|
|
|
330
|
+
function text(content: string): ToolResult {
|
|
331
|
+
return { content: [{ type: 'text', text: content }] };
|
|
332
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
|
|
9
|
+
interface CouplingMatch {
|
|
10
|
+
frontendFile: string;
|
|
11
|
+
line: number;
|
|
12
|
+
callPattern: string;
|
|
13
|
+
routeId: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Scan frontend files for API calls matching Python routes.
|
|
18
|
+
* Stores matches in massu_py_route_callers.
|
|
19
|
+
*/
|
|
20
|
+
export function buildPythonCouplingIndex(dataDb: Database.Database): number {
|
|
21
|
+
const projectRoot = getProjectRoot();
|
|
22
|
+
const config = getConfig();
|
|
23
|
+
const srcDir = join(projectRoot, config.paths.source);
|
|
24
|
+
|
|
25
|
+
// Get all routes from DB
|
|
26
|
+
const routes = dataDb.prepare('SELECT id, method, path FROM massu_py_routes').all() as {
|
|
27
|
+
id: number; method: string; path: string;
|
|
28
|
+
}[];
|
|
29
|
+
|
|
30
|
+
if (routes.length === 0) return 0;
|
|
31
|
+
|
|
32
|
+
// Clear existing callers
|
|
33
|
+
dataDb.exec('DELETE FROM massu_py_route_callers');
|
|
34
|
+
|
|
35
|
+
// Walk frontend files (TS/TSX/JS/JSX)
|
|
36
|
+
const frontendFiles = walkFrontendFiles(srcDir);
|
|
37
|
+
|
|
38
|
+
const insertStmt = dataDb.prepare(
|
|
39
|
+
'INSERT INTO massu_py_route_callers (route_id, frontend_file, line, call_pattern) VALUES (?, ?, ?, ?)'
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
let count = 0;
|
|
43
|
+
|
|
44
|
+
// API call patterns to detect
|
|
45
|
+
const apiPatterns = [
|
|
46
|
+
/fetch\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // fetch('/api/...')
|
|
47
|
+
/fetch\s*\(\s*[`'"]([^`'"]*\/api\/[^`'"]*)[`'"]/g, // fetch('http.../api/...')
|
|
48
|
+
/axios\.\w+\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // axios.get('/api/...')
|
|
49
|
+
/\.get\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // client.get('/api/...')
|
|
50
|
+
/\.post\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // client.post('/api/...')
|
|
51
|
+
/\.put\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // client.put('/api/...')
|
|
52
|
+
/\.delete\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // client.delete('/api/...')
|
|
53
|
+
/\.patch\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // client.patch('/api/...')
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
dataDb.transaction(() => {
|
|
57
|
+
for (const absFile of frontendFiles) {
|
|
58
|
+
const relFile = relative(projectRoot, absFile);
|
|
59
|
+
let source: string;
|
|
60
|
+
try { source = readFileSync(absFile, 'utf-8'); } catch { continue; }
|
|
61
|
+
|
|
62
|
+
const lines = source.split('\n');
|
|
63
|
+
for (let i = 0; i < lines.length; i++) {
|
|
64
|
+
const line = lines[i];
|
|
65
|
+
for (const pattern of apiPatterns) {
|
|
66
|
+
pattern.lastIndex = 0;
|
|
67
|
+
let match;
|
|
68
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
69
|
+
const urlPath = match[1];
|
|
70
|
+
// Try to match against routes
|
|
71
|
+
const matchedRoute = findMatchingRoute(urlPath, routes);
|
|
72
|
+
if (matchedRoute) {
|
|
73
|
+
insertStmt.run(matchedRoute.id, relFile, i + 1, match[0].slice(0, 200));
|
|
74
|
+
count++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
|
|
82
|
+
return count;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function walkFrontendFiles(dir: string): string[] {
|
|
86
|
+
const files: string[] = [];
|
|
87
|
+
const exclude = ['node_modules', '.next', 'dist', '.git', '__pycache__', '.venv', 'venv'];
|
|
88
|
+
try {
|
|
89
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
if (entry.isDirectory()) {
|
|
92
|
+
if (exclude.includes(entry.name)) continue;
|
|
93
|
+
files.push(...walkFrontendFiles(join(dir, entry.name)));
|
|
94
|
+
} else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
95
|
+
files.push(join(dir, entry.name));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch { /* dir not readable, skip */ }
|
|
99
|
+
return files;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Match a URL path against route definitions.
|
|
104
|
+
* Handles path parameters: /api/users/{id} matches /api/users/123
|
|
105
|
+
*/
|
|
106
|
+
/**
|
|
107
|
+
* Escape special regex characters in a string.
|
|
108
|
+
*/
|
|
109
|
+
function escapeRegex(s: string): string {
|
|
110
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function findMatchingRoute(urlPath: string, routes: { id: number; method: string; path: string }[]): { id: number } | null {
|
|
114
|
+
for (const route of routes) {
|
|
115
|
+
// Escape regex-special chars in route path, then replace param placeholders
|
|
116
|
+
const escaped = escapeRegex(route.path);
|
|
117
|
+
const pattern = escaped.replace(/\\\{[^}]+\\\}/g, '[^/]+');
|
|
118
|
+
const routeRegex = new RegExp('^' + pattern + '$');
|
|
119
|
+
if (routeRegex.test(urlPath)) {
|
|
120
|
+
return { id: route.id };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
|
|
7
|
+
export interface DomainViolation {
|
|
8
|
+
sourceFile: string;
|
|
9
|
+
sourceDomain: string;
|
|
10
|
+
targetFile: string;
|
|
11
|
+
targetDomain: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Classify a Python file into its domain based on python.domains config.
|
|
16
|
+
*/
|
|
17
|
+
export function classifyPythonFileDomain(file: string): string {
|
|
18
|
+
const config = getConfig();
|
|
19
|
+
const domains = config.python?.domains || [];
|
|
20
|
+
const pythonRoot = config.python?.root || '';
|
|
21
|
+
|
|
22
|
+
let modulePath = file;
|
|
23
|
+
if (pythonRoot && modulePath.startsWith(pythonRoot + '/')) {
|
|
24
|
+
modulePath = modulePath.slice(pythonRoot.length + 1);
|
|
25
|
+
}
|
|
26
|
+
modulePath = modulePath.replace(/\/__init__\.py$/, '').replace(/\.py$/, '').replace(/\//g, '.');
|
|
27
|
+
|
|
28
|
+
for (const domain of domains) {
|
|
29
|
+
for (const pkg of domain.packages) {
|
|
30
|
+
if (modulePath.startsWith(pkg) || modulePath === pkg) {
|
|
31
|
+
return domain.name;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return 'Unknown';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Find all cross-domain import violations in the Python import graph.
|
|
40
|
+
*/
|
|
41
|
+
export function findPythonDomainViolations(dataDb: Database.Database): DomainViolation[] {
|
|
42
|
+
const config = getConfig();
|
|
43
|
+
const domains = config.python?.domains || [];
|
|
44
|
+
if (domains.length === 0) return [];
|
|
45
|
+
|
|
46
|
+
const imports = dataDb.prepare(
|
|
47
|
+
'SELECT source_file, target_file FROM massu_py_imports'
|
|
48
|
+
).all() as { source_file: string; target_file: string }[];
|
|
49
|
+
|
|
50
|
+
const violations: DomainViolation[] = [];
|
|
51
|
+
|
|
52
|
+
for (const imp of imports) {
|
|
53
|
+
const srcDomain = classifyPythonFileDomain(imp.source_file);
|
|
54
|
+
const tgtDomain = classifyPythonFileDomain(imp.target_file);
|
|
55
|
+
|
|
56
|
+
if (srcDomain === tgtDomain || srcDomain === 'Unknown' || tgtDomain === 'Unknown') continue;
|
|
57
|
+
|
|
58
|
+
const srcConfig = domains.find(d => d.name === srcDomain);
|
|
59
|
+
if (srcConfig && !srcConfig.allowed_imports_from.includes(tgtDomain)) {
|
|
60
|
+
violations.push({
|
|
61
|
+
sourceFile: imp.source_file,
|
|
62
|
+
sourceDomain: srcDomain,
|
|
63
|
+
targetFile: imp.target_file,
|
|
64
|
+
targetDomain: tgtDomain,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return violations;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get all Python files in a specific domain.
|
|
74
|
+
*/
|
|
75
|
+
export function getPythonFilesInDomain(dataDb: Database.Database, domainName: string): string[] {
|
|
76
|
+
const allFiles = dataDb.prepare(
|
|
77
|
+
'SELECT DISTINCT source_file as f FROM massu_py_imports UNION SELECT DISTINCT target_file as f FROM massu_py_imports'
|
|
78
|
+
).all() as { f: string }[];
|
|
79
|
+
|
|
80
|
+
return allFiles
|
|
81
|
+
.map(row => row.f)
|
|
82
|
+
.filter(f => classifyPythonFileDomain(f) === domainName);
|
|
83
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
|
|
7
|
+
export interface PythonImpactReport {
|
|
8
|
+
file: string;
|
|
9
|
+
domain: string;
|
|
10
|
+
importedBy: string[];
|
|
11
|
+
routes: { method: string; path: string; functionName: string }[];
|
|
12
|
+
models: { className: string; tableName: string | null }[];
|
|
13
|
+
frontendCallers: string[];
|
|
14
|
+
domainCrossings: { file: string; domain: string }[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Full impact analysis for a Python file.
|
|
19
|
+
* Cross-references import graph, routes, models, and frontend coupling.
|
|
20
|
+
*/
|
|
21
|
+
export function analyzePythonImpact(dataDb: Database.Database, file: string): PythonImpactReport {
|
|
22
|
+
const domain = classifyPythonFileDomain(file);
|
|
23
|
+
|
|
24
|
+
// 1. Who imports this file (direct + transitive)
|
|
25
|
+
const importedBy = collectTransitiveImporters(dataDb, file, 5);
|
|
26
|
+
|
|
27
|
+
// 2. Routes defined in this file
|
|
28
|
+
const routes = dataDb.prepare(
|
|
29
|
+
'SELECT method, path, function_name FROM massu_py_routes WHERE file = ?'
|
|
30
|
+
).all(file) as { method: string; path: string; function_name: string }[];
|
|
31
|
+
|
|
32
|
+
// 3. Models defined in this file
|
|
33
|
+
const models = dataDb.prepare(
|
|
34
|
+
'SELECT class_name, table_name FROM massu_py_models WHERE file = ?'
|
|
35
|
+
).all(file) as { class_name: string; table_name: string | null }[];
|
|
36
|
+
|
|
37
|
+
// 4. Frontend callers (via routes in this file)
|
|
38
|
+
const routeIds = dataDb.prepare('SELECT id FROM massu_py_routes WHERE file = ?').all(file) as { id: number }[];
|
|
39
|
+
const frontendCallers: string[] = [];
|
|
40
|
+
if (routeIds.length > 0) {
|
|
41
|
+
const placeholders = routeIds.map(() => '?').join(',');
|
|
42
|
+
const callers = dataDb.prepare(
|
|
43
|
+
`SELECT DISTINCT frontend_file FROM massu_py_route_callers WHERE route_id IN (${placeholders})`
|
|
44
|
+
).all(...routeIds.map(r => r.id)) as { frontend_file: string }[];
|
|
45
|
+
frontendCallers.push(...callers.map(c => c.frontend_file));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 5. Domain crossings
|
|
49
|
+
const imports = dataDb.prepare(
|
|
50
|
+
'SELECT target_file FROM massu_py_imports WHERE source_file = ?'
|
|
51
|
+
).all(file) as { target_file: string }[];
|
|
52
|
+
|
|
53
|
+
const domainCrossings = imports
|
|
54
|
+
.map(imp => ({ file: imp.target_file, domain: classifyPythonFileDomain(imp.target_file) }))
|
|
55
|
+
.filter(imp => imp.domain !== domain && imp.domain !== 'Unknown');
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
file,
|
|
59
|
+
domain,
|
|
60
|
+
importedBy,
|
|
61
|
+
routes: routes.map(r => ({ method: r.method, path: r.path, functionName: r.function_name })),
|
|
62
|
+
models: models.map(m => ({ className: m.class_name, tableName: m.table_name })),
|
|
63
|
+
frontendCallers,
|
|
64
|
+
domainCrossings,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function collectTransitiveImporters(dataDb: Database.Database, file: string, maxDepth: number): string[] {
|
|
69
|
+
const visited = new Set<string>();
|
|
70
|
+
const queue = [file];
|
|
71
|
+
let depth = 0;
|
|
72
|
+
|
|
73
|
+
const importerStmt = dataDb.prepare(
|
|
74
|
+
'SELECT source_file FROM massu_py_imports WHERE target_file = ?'
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
while (queue.length > 0 && depth < maxDepth) {
|
|
78
|
+
const batch = [...queue];
|
|
79
|
+
queue.length = 0;
|
|
80
|
+
for (const f of batch) {
|
|
81
|
+
if (visited.has(f)) continue;
|
|
82
|
+
visited.add(f);
|
|
83
|
+
const importers = importerStmt.all(f) as { source_file: string }[];
|
|
84
|
+
for (const imp of importers) {
|
|
85
|
+
if (!visited.has(imp.source_file)) {
|
|
86
|
+
queue.push(imp.source_file);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
depth++;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
visited.delete(file); // Don't include the file itself
|
|
94
|
+
return [...visited];
|
|
95
|
+
}
|