@massu/core 0.1.2 → 0.4.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.
- 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 +12521 -0
- package/dist/hooks/cost-tracker.js +80 -5
- package/dist/hooks/post-edit-context.js +72 -6
- package/dist/hooks/post-tool-use.js +234 -57
- package/dist/hooks/pre-compact.js +144 -5
- package/dist/hooks/pre-delete-check.js +141 -11
- package/dist/hooks/quality-event.js +80 -5
- package/dist/hooks/security-gate.js +29 -0
- package/dist/hooks/session-end.js +83 -8
- package/dist/hooks/session-start.js +153 -7
- package/dist/hooks/user-prompt.js +166 -5
- package/package.json +6 -5
- package/src/backfill-sessions.ts +5 -4
- package/src/cli.ts +6 -1
- package/src/commands/doctor.ts +193 -6
- package/src/commands/init.ts +235 -6
- package/src/commands/install-commands.ts +137 -0
- package/src/config.ts +68 -2
- package/src/db.ts +115 -2
- package/src/docs-tools.ts +8 -6
- package/src/hooks/post-edit-context.ts +1 -1
- 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-start.ts +97 -4
- 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 +14 -1
- package/src/observation-extractor.ts +11 -4
- package/src/page-deps.ts +3 -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/sentinel-db.ts +2 -1
- package/src/server.ts +29 -6
- package/src/session-archiver.ts +4 -5
- package/src/tools.ts +283 -31
- package/README.md +0 -40
|
@@ -0,0 +1,58 @@
|
|
|
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 { parsePythonRoutes } from './route-parser.ts';
|
|
8
|
+
import { getProjectRoot } from '../config.ts';
|
|
9
|
+
|
|
10
|
+
function walkPyFiles(dir: string, excludeDirs: string[]): string[] {
|
|
11
|
+
const files: string[] = [];
|
|
12
|
+
try {
|
|
13
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
if (excludeDirs.includes(entry.name)) continue;
|
|
17
|
+
files.push(...walkPyFiles(join(dir, entry.name), excludeDirs));
|
|
18
|
+
} else if (entry.name.endsWith('.py')) {
|
|
19
|
+
files.push(join(dir, entry.name));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
} catch { /* dir not readable, skip */ }
|
|
23
|
+
return files;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildPythonRouteIndex(dataDb: Database.Database, pythonRoot: string, excludeDirs: string[] = ['__pycache__', '.venv', 'venv', '.mypy_cache', '.pytest_cache']): number {
|
|
27
|
+
const projectRoot = getProjectRoot();
|
|
28
|
+
const absRoot = join(projectRoot, pythonRoot);
|
|
29
|
+
dataDb.exec('DELETE FROM massu_py_routes');
|
|
30
|
+
dataDb.exec('DELETE FROM massu_py_route_callers');
|
|
31
|
+
|
|
32
|
+
const insertStmt = dataDb.prepare(
|
|
33
|
+
'INSERT INTO massu_py_routes (file, method, path, function_name, dependencies, request_model, response_model, is_authenticated, line) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const files = walkPyFiles(absRoot, excludeDirs);
|
|
37
|
+
let count = 0;
|
|
38
|
+
|
|
39
|
+
dataDb.transaction(() => {
|
|
40
|
+
for (const absFile of files) {
|
|
41
|
+
const relFile = relative(projectRoot, absFile);
|
|
42
|
+
let source: string;
|
|
43
|
+
try { source = readFileSync(absFile, 'utf-8'); } catch { continue; }
|
|
44
|
+
|
|
45
|
+
const routes = parsePythonRoutes(source);
|
|
46
|
+
for (const route of routes) {
|
|
47
|
+
insertStmt.run(
|
|
48
|
+
relFile, route.method, route.path, route.functionName,
|
|
49
|
+
JSON.stringify(route.dependencies), route.requestModel, route.responseModel,
|
|
50
|
+
route.isAuthenticated ? 1 : 0, route.line
|
|
51
|
+
);
|
|
52
|
+
count++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
})();
|
|
56
|
+
|
|
57
|
+
return count;
|
|
58
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
export interface ParsedRoute {
|
|
5
|
+
method: string;
|
|
6
|
+
path: string;
|
|
7
|
+
functionName: string;
|
|
8
|
+
dependencies: string[];
|
|
9
|
+
requestModel: string | null;
|
|
10
|
+
responseModel: string | null;
|
|
11
|
+
isAuthenticated: boolean;
|
|
12
|
+
line: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const HTTP_METHODS = new Set([
|
|
16
|
+
'get',
|
|
17
|
+
'post',
|
|
18
|
+
'put',
|
|
19
|
+
'delete',
|
|
20
|
+
'patch',
|
|
21
|
+
'options',
|
|
22
|
+
'head',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const BASIC_TYPES = new Set([
|
|
26
|
+
'str',
|
|
27
|
+
'int',
|
|
28
|
+
'float',
|
|
29
|
+
'bool',
|
|
30
|
+
'bytes',
|
|
31
|
+
'list',
|
|
32
|
+
'dict',
|
|
33
|
+
'set',
|
|
34
|
+
'tuple',
|
|
35
|
+
'Any',
|
|
36
|
+
'None',
|
|
37
|
+
'Optional',
|
|
38
|
+
'Request',
|
|
39
|
+
'Response',
|
|
40
|
+
'Query',
|
|
41
|
+
'Path',
|
|
42
|
+
'Header',
|
|
43
|
+
'Cookie',
|
|
44
|
+
'Body',
|
|
45
|
+
'Form',
|
|
46
|
+
'File',
|
|
47
|
+
'UploadFile',
|
|
48
|
+
'BackgroundTasks',
|
|
49
|
+
'HTTPException',
|
|
50
|
+
'Depends',
|
|
51
|
+
'Session',
|
|
52
|
+
'AsyncSession',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Join logical lines that are split across multiple physical lines.
|
|
57
|
+
* Handles backslash continuations and unclosed parentheses/brackets.
|
|
58
|
+
*/
|
|
59
|
+
function joinLogicalLines(source: string): { text: string; startLine: number }[] {
|
|
60
|
+
const physicalLines = source.split('\n');
|
|
61
|
+
const logical: { text: string; startLine: number }[] = [];
|
|
62
|
+
let current = '';
|
|
63
|
+
let startLine = 0;
|
|
64
|
+
let openParens = 0;
|
|
65
|
+
let openBrackets = 0;
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < physicalLines.length; i++) {
|
|
68
|
+
const raw = physicalLines[i];
|
|
69
|
+
if (current === '') {
|
|
70
|
+
startLine = i + 1; // 1-based
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Strip trailing backslash continuation
|
|
74
|
+
const continued = raw.trimEnd().endsWith('\\');
|
|
75
|
+
const line = continued ? raw.trimEnd().slice(0, -1) : raw;
|
|
76
|
+
|
|
77
|
+
current += (current ? ' ' : '') + line;
|
|
78
|
+
|
|
79
|
+
// Count parens/brackets (ignoring those inside strings for simplicity)
|
|
80
|
+
for (const ch of line) {
|
|
81
|
+
if (ch === '(') openParens++;
|
|
82
|
+
else if (ch === ')') openParens = Math.max(0, openParens - 1);
|
|
83
|
+
else if (ch === '[') openBrackets++;
|
|
84
|
+
else if (ch === ']') openBrackets = Math.max(0, openBrackets - 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!continued && openParens === 0 && openBrackets === 0) {
|
|
88
|
+
logical.push({ text: current, startLine });
|
|
89
|
+
current = '';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (current) {
|
|
94
|
+
logical.push({ text: current, startLine });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return logical;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Extract the first quoted string from a text segment.
|
|
102
|
+
*/
|
|
103
|
+
function extractQuotedString(text: string): string | null {
|
|
104
|
+
const m = text.match(/(['"])(.*?)\1/);
|
|
105
|
+
return m ? m[2] : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extract response_model value from a decorator argument string.
|
|
110
|
+
*/
|
|
111
|
+
function extractResponseModel(argStr: string): string | null {
|
|
112
|
+
const m = argStr.match(/response_model\s*=\s*([A-Za-z_][A-Za-z0-9_.\[\]]*)/);
|
|
113
|
+
return m ? m[1] : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Extract methods list from api_route decorator.
|
|
118
|
+
*/
|
|
119
|
+
function extractApiRouteMethods(argStr: string): string[] {
|
|
120
|
+
const m = argStr.match(/methods\s*=\s*\[([^\]]*)\]/);
|
|
121
|
+
if (!m) return [];
|
|
122
|
+
const inner = m[1];
|
|
123
|
+
const methods: string[] = [];
|
|
124
|
+
const re = /['"](\w+)['"]/g;
|
|
125
|
+
let match;
|
|
126
|
+
while ((match = re.exec(inner)) !== null) {
|
|
127
|
+
methods.push(match[1].toUpperCase());
|
|
128
|
+
}
|
|
129
|
+
return methods;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parse Depends() calls from function parameters.
|
|
134
|
+
*/
|
|
135
|
+
function extractDependencies(paramStr: string): string[] {
|
|
136
|
+
const deps: string[] = [];
|
|
137
|
+
const re = /Depends\(\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\)/g;
|
|
138
|
+
let match;
|
|
139
|
+
while ((match = re.exec(paramStr)) !== null) {
|
|
140
|
+
deps.push(match[1]);
|
|
141
|
+
}
|
|
142
|
+
return deps;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Parse function parameters for request body models.
|
|
147
|
+
* A request body parameter is a type-annotated param whose type is not
|
|
148
|
+
* a basic/framework type and not assigned via Depends/Query/Path/etc.
|
|
149
|
+
*/
|
|
150
|
+
function extractRequestModel(paramStr: string): string | null {
|
|
151
|
+
// Split params by comma, respecting nested parens
|
|
152
|
+
const params = splitParams(paramStr);
|
|
153
|
+
|
|
154
|
+
for (const param of params) {
|
|
155
|
+
const trimmed = param.trim();
|
|
156
|
+
// Skip self, cls, *args, **kwargs
|
|
157
|
+
if (/^(self|cls|\*\*?\w*)$/.test(trimmed.split(/[\s:=]/)[0].trim())) continue;
|
|
158
|
+
// Skip if has Depends, Query, Path, Header, Cookie, Form, File, Body assignment
|
|
159
|
+
if (/=\s*(Depends|Query|Path|Header|Cookie|Form|File|Body)\s*\(/.test(trimmed)) continue;
|
|
160
|
+
|
|
161
|
+
// Look for type annotation: name: Type
|
|
162
|
+
const annotationMatch = trimmed.match(/^\s*(\w+)\s*:\s*([A-Za-z_][A-Za-z0-9_.\[\]|]*)/);
|
|
163
|
+
if (annotationMatch) {
|
|
164
|
+
const typeName = annotationMatch[2].replace(/\s/g, '');
|
|
165
|
+
// Strip Optional[], List[], etc wrappers to get base type
|
|
166
|
+
const baseType = typeName.replace(/^(Optional|List|Set|Tuple|Dict)\[/, '').replace(/\]$/, '').split('[')[0].split('|')[0];
|
|
167
|
+
if (!BASIC_TYPES.has(baseType) && /^[A-Z]/.test(baseType)) {
|
|
168
|
+
return baseType;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Split parameter string by top-level commas (not inside parens/brackets).
|
|
177
|
+
*/
|
|
178
|
+
function splitParams(paramStr: string): string[] {
|
|
179
|
+
const parts: string[] = [];
|
|
180
|
+
let current = '';
|
|
181
|
+
let depth = 0;
|
|
182
|
+
|
|
183
|
+
for (const ch of paramStr) {
|
|
184
|
+
if (ch === '(' || ch === '[') depth++;
|
|
185
|
+
else if (ch === ')' || ch === ']') depth = Math.max(0, depth - 1);
|
|
186
|
+
else if (ch === ',' && depth === 0) {
|
|
187
|
+
parts.push(current);
|
|
188
|
+
current = '';
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
current += ch;
|
|
192
|
+
}
|
|
193
|
+
if (current.trim()) parts.push(current);
|
|
194
|
+
return parts;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Parse FastAPI route decorators and their handler functions from Python source code.
|
|
199
|
+
*/
|
|
200
|
+
export function parsePythonRoutes(source: string): ParsedRoute[] {
|
|
201
|
+
const routes: ParsedRoute[] = [];
|
|
202
|
+
const logicalLines = joinLogicalLines(source);
|
|
203
|
+
|
|
204
|
+
// Collect pending decorators, then match them to function defs
|
|
205
|
+
interface PendingDecorator {
|
|
206
|
+
method: string;
|
|
207
|
+
path: string;
|
|
208
|
+
responseModel: string | null;
|
|
209
|
+
methods: string[]; // for api_route
|
|
210
|
+
line: number;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const pendingDecorators: PendingDecorator[] = [];
|
|
214
|
+
|
|
215
|
+
for (let i = 0; i < logicalLines.length; i++) {
|
|
216
|
+
const { text, startLine } = logicalLines[i];
|
|
217
|
+
const trimmed = text.trim();
|
|
218
|
+
|
|
219
|
+
// Detect route decorator: @variable.method("path" ...) or @variable.api_route(...)
|
|
220
|
+
const decoratorMatch = trimmed.match(
|
|
221
|
+
/^@\s*(\w+)\s*\.\s*(\w+)\s*\((.*)\)\s*$/s,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (decoratorMatch) {
|
|
225
|
+
const methodName = decoratorMatch[2].toLowerCase();
|
|
226
|
+
const argStr = decoratorMatch[3];
|
|
227
|
+
|
|
228
|
+
if (methodName === 'api_route') {
|
|
229
|
+
const path = extractQuotedString(argStr);
|
|
230
|
+
if (path) {
|
|
231
|
+
const methods = extractApiRouteMethods(argStr);
|
|
232
|
+
const responseModel = extractResponseModel(argStr);
|
|
233
|
+
pendingDecorators.push({
|
|
234
|
+
method: 'API_ROUTE',
|
|
235
|
+
path,
|
|
236
|
+
responseModel,
|
|
237
|
+
methods,
|
|
238
|
+
line: startLine,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
} else if (HTTP_METHODS.has(methodName)) {
|
|
242
|
+
const path = extractQuotedString(argStr);
|
|
243
|
+
if (path) {
|
|
244
|
+
const responseModel = extractResponseModel(argStr);
|
|
245
|
+
pendingDecorators.push({
|
|
246
|
+
method: methodName.toUpperCase(),
|
|
247
|
+
path,
|
|
248
|
+
responseModel,
|
|
249
|
+
methods: [],
|
|
250
|
+
line: startLine,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Detect function definition
|
|
258
|
+
const funcMatch = trimmed.match(
|
|
259
|
+
/^(?:async\s+)?def\s+(\w+)\s*\((.*)\)\s*(?:->.*)?:\s*$/s,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
if (funcMatch && pendingDecorators.length > 0) {
|
|
263
|
+
const functionName = funcMatch[1];
|
|
264
|
+
const paramStr = funcMatch[2];
|
|
265
|
+
|
|
266
|
+
const dependencies = extractDependencies(paramStr);
|
|
267
|
+
const requestModel = extractRequestModel(paramStr);
|
|
268
|
+
|
|
269
|
+
const isAuthenticated = dependencies.some((dep) =>
|
|
270
|
+
/auth|user|current_user/i.test(dep),
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
for (const dec of pendingDecorators) {
|
|
274
|
+
if (dec.method === 'API_ROUTE') {
|
|
275
|
+
// Expand api_route into one route per method
|
|
276
|
+
const methods = dec.methods.length > 0 ? dec.methods : ['GET'];
|
|
277
|
+
for (const m of methods) {
|
|
278
|
+
routes.push({
|
|
279
|
+
method: m,
|
|
280
|
+
path: dec.path,
|
|
281
|
+
functionName,
|
|
282
|
+
dependencies,
|
|
283
|
+
requestModel,
|
|
284
|
+
responseModel: dec.responseModel,
|
|
285
|
+
isAuthenticated,
|
|
286
|
+
line: dec.line,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
routes.push({
|
|
291
|
+
method: dec.method,
|
|
292
|
+
path: dec.path,
|
|
293
|
+
functionName,
|
|
294
|
+
dependencies,
|
|
295
|
+
requestModel,
|
|
296
|
+
responseModel: dec.responseModel,
|
|
297
|
+
isAuthenticated,
|
|
298
|
+
line: dec.line,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
pendingDecorators.length = 0;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// If we hit a non-decorator, non-function line, and there are pending decorators
|
|
308
|
+
// that aren't followed by more decorators, clear them (they belong to something else)
|
|
309
|
+
if (!trimmed.startsWith('@') && !trimmed.startsWith('def ') && !trimmed.startsWith('async def ')) {
|
|
310
|
+
if (pendingDecorators.length > 0 && trimmed !== '') {
|
|
311
|
+
pendingDecorators.length = 0;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return routes;
|
|
317
|
+
}
|