@massu/core 1.10.7 → 1.12.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/CHANGELOG.md +1303 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +559 -400
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +66 -18
- package/dist/hooks/pre-compact.js +27 -2
- package/dist/hooks/pre-delete-check.js +48 -22
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +24 -0
- package/dist/hooks/session-start.js +8710 -8460
- package/dist/hooks/user-prompt.js +24 -0
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1205
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- package/src/watch/state.ts +0 -178
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Plan 3b — Phase 1: FastAPI AST adapter.
|
|
6
|
-
*
|
|
7
|
-
* Extracts:
|
|
8
|
-
* - auth_dep: name passed to `Depends(...)` in router files
|
|
9
|
-
* - api_prefix_base: first path segment of `APIRouter(prefix="/...")`
|
|
10
|
-
* - test_async_pattern: `@pytest.mark.asyncio` (with or without parens)
|
|
11
|
-
*
|
|
12
|
-
* Confidence rules:
|
|
13
|
-
* - 'high' if the auth dep is found exactly ONCE in routers/ and matches
|
|
14
|
-
* known FastAPI signatures.
|
|
15
|
-
* - 'medium' if found in non-routers/ paths (e.g., a deps.py module).
|
|
16
|
-
* - 'low' if multiple candidate auth deps are found (ambiguous — but still
|
|
17
|
-
* emitted so the user can see what was found).
|
|
18
|
-
* - 'none' if no `Depends(...)` calls found AND no `APIRouter(prefix=)` —
|
|
19
|
-
* adapter doesn't apply, regex fallback takes over.
|
|
20
|
-
*
|
|
21
|
-
* Does NOT use regex on file content — only Tree-sitter S-expression queries
|
|
22
|
-
* compiled via `query-helpers.ts`. Regex would be the regex-fallback path.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { Parser } from 'web-tree-sitter';
|
|
26
|
-
import type { CodebaseAdapter, AdapterResult, DetectionSignals, Provenance, SourceFile } from './types.ts';
|
|
27
|
-
import { runQuery, InvalidQueryError } from './query-helpers.ts';
|
|
28
|
-
import { loadGrammar } from './tree-sitter-loader.ts';
|
|
29
|
-
import { isParsableSource, MAX_AST_FILE_BYTES } from './parse-guard.ts';
|
|
30
|
-
|
|
31
|
-
// ============================================================
|
|
32
|
-
// Tree-sitter S-expression queries
|
|
33
|
-
// ============================================================
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Auth dependency: catches `Depends(get_current_user)`, `Depends(require_tier_or_guardian)`,
|
|
37
|
-
* etc. Anchored on the canonical `Depends` call shape.
|
|
38
|
-
*
|
|
39
|
-
* Per the spec doc §3, predicate constraints (#eq?) keep the query from
|
|
40
|
-
* matching arbitrary `<x>(<y>)` calls.
|
|
41
|
-
*/
|
|
42
|
-
const AUTH_DEP_QUERY = `
|
|
43
|
-
(call
|
|
44
|
-
function: (identifier) @_callee (#eq? @_callee "Depends")
|
|
45
|
-
arguments: (argument_list
|
|
46
|
-
(identifier) @auth_dep))
|
|
47
|
-
`;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* APIRouter prefix: `APIRouter(prefix="/api/orders", ...)`. Captures the
|
|
51
|
-
* string literal so the runner can split off the base segment.
|
|
52
|
-
*/
|
|
53
|
-
const API_PREFIX_QUERY = `
|
|
54
|
-
(call
|
|
55
|
-
function: (identifier) @_callee (#eq? @_callee "APIRouter")
|
|
56
|
-
arguments: (argument_list
|
|
57
|
-
(keyword_argument
|
|
58
|
-
name: (identifier) @_kw (#eq? @_kw "prefix")
|
|
59
|
-
value: (string) @prefix_value)))
|
|
60
|
-
`;
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* `@pytest.mark.asyncio` decorator. Captures the decorator name string for
|
|
64
|
-
* provenance; the value field is fixed as the canonical form.
|
|
65
|
-
*/
|
|
66
|
-
const PYTEST_ASYNCIO_QUERY = `
|
|
67
|
-
(decorator
|
|
68
|
-
(attribute
|
|
69
|
-
object: (attribute
|
|
70
|
-
object: (identifier) @_pkg (#eq? @_pkg "pytest")
|
|
71
|
-
attribute: (identifier) @_mark (#eq? @_mark "mark"))
|
|
72
|
-
attribute: (identifier) @_marker (#eq? @_marker "asyncio"))) @decorator
|
|
73
|
-
`;
|
|
74
|
-
|
|
75
|
-
// ============================================================
|
|
76
|
-
// Adapter
|
|
77
|
-
// ============================================================
|
|
78
|
-
|
|
79
|
-
export const pythonFastApiAdapter: CodebaseAdapter = {
|
|
80
|
-
id: 'python-fastapi',
|
|
81
|
-
languages: ['python'],
|
|
82
|
-
|
|
83
|
-
matches(signals: DetectionSignals): boolean {
|
|
84
|
-
// Cheap signal-only check. No file IO. Match if:
|
|
85
|
-
// 1. pyproject.toml mentions fastapi (raw text contains 'fastapi'), OR
|
|
86
|
-
// 2. project has a routers/ directory (FastAPI convention), OR
|
|
87
|
-
// 3. project has app/ + python files at top level
|
|
88
|
-
const pyToml = signals.pyprojectToml as { __raw?: string } | undefined;
|
|
89
|
-
if (pyToml?.__raw && /\bfastapi\b/i.test(pyToml.__raw)) return true;
|
|
90
|
-
if (signals.presentDirs.has('routers')) return true;
|
|
91
|
-
if (signals.presentDirs.has('app') && signals.presentFiles.has('main.py')) return true;
|
|
92
|
-
return false;
|
|
93
|
-
},
|
|
94
|
-
|
|
95
|
-
async introspect(files: SourceFile[], _rootDir: string): Promise<AdapterResult> {
|
|
96
|
-
if (files.length === 0) {
|
|
97
|
-
return { conventions: {}, provenance: [], confidence: 'none' };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
let language;
|
|
101
|
-
try {
|
|
102
|
-
language = await loadGrammar('python');
|
|
103
|
-
} catch (e) {
|
|
104
|
-
// Grammar unavailable → adapter returns 'none' so regex fallback takes
|
|
105
|
-
// over. The runner's stderr line is emitted at the introspector tier.
|
|
106
|
-
return { conventions: {}, provenance: [], confidence: 'none' };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const parser = new Parser();
|
|
110
|
-
parser.setLanguage(language);
|
|
111
|
-
|
|
112
|
-
// Per-field collection: { value -> { fileLine, queryName } }
|
|
113
|
-
const authDeps = new Map<string, { line: number; file: string }>();
|
|
114
|
-
const prefixBases = new Map<string, { line: number; file: string }>();
|
|
115
|
-
const testAsyncPatterns = new Map<string, { line: number; file: string }>();
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
for (const file of files) {
|
|
119
|
-
// Phase 3.5 fix: defense-in-depth size + depth gate at adapter
|
|
120
|
-
// tier (the runner also gates, but adapters may be invoked
|
|
121
|
-
// directly from tests/CLI).
|
|
122
|
-
const skip = isParsableSource(file.content, file.size);
|
|
123
|
-
if (skip) {
|
|
124
|
-
process.stderr.write(
|
|
125
|
-
`[massu/ast] WARN: python-fastapi skipping ${file.path}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES}. (Phase 3.5 mitigation)\n`,
|
|
126
|
-
);
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
try {
|
|
130
|
-
// Auth dep
|
|
131
|
-
for (const hit of runQuery(parser, file.content, AUTH_DEP_QUERY, 'fastapi-auth-dep', file.path)) {
|
|
132
|
-
const name = hit.captures.auth_dep;
|
|
133
|
-
if (name && !authDeps.has(name)) {
|
|
134
|
-
authDeps.set(name, { line: hit.line, file: file.path });
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
// API prefix
|
|
138
|
-
for (const hit of runQuery(parser, file.content, API_PREFIX_QUERY, 'fastapi-api-prefix', file.path)) {
|
|
139
|
-
const raw = hit.captures.prefix_value;
|
|
140
|
-
if (!raw) continue;
|
|
141
|
-
// Strip enclosing quotes (string node text includes them)
|
|
142
|
-
const literal = raw.replace(/^['"]/, '').replace(/['"]$/, '');
|
|
143
|
-
const base = extractPrefixBase(literal);
|
|
144
|
-
if (base && !prefixBases.has(base)) {
|
|
145
|
-
prefixBases.set(base, { line: hit.line, file: file.path });
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
// pytest.mark.asyncio
|
|
149
|
-
for (const hit of runQuery(parser, file.content, PYTEST_ASYNCIO_QUERY, 'fastapi-pytest-asyncio', file.path)) {
|
|
150
|
-
const pat = '@pytest.mark.asyncio';
|
|
151
|
-
if (!testAsyncPatterns.has(pat)) {
|
|
152
|
-
testAsyncPatterns.set(pat, { line: hit.line, file: file.path });
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
} catch (e) {
|
|
156
|
-
if (e instanceof InvalidQueryError) {
|
|
157
|
-
// Compile-time failure of OUR query is a developer bug — surface it.
|
|
158
|
-
throw e;
|
|
159
|
-
}
|
|
160
|
-
// Per-file parse error: skip this file, keep going. Tree-sitter is
|
|
161
|
-
// error-tolerant so this is rare; usually means we got a binary or
|
|
162
|
-
// a non-Python file mislabeled.
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
} finally {
|
|
167
|
-
try { parser.delete(); } catch { /* ignore */ }
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Build result
|
|
171
|
-
const conventions: Record<string, unknown> = {};
|
|
172
|
-
const provenance: Provenance[] = [];
|
|
173
|
-
|
|
174
|
-
// Auth dep: high if exactly 1, low if >1 (still emit first), none if 0.
|
|
175
|
-
if (authDeps.size === 1) {
|
|
176
|
-
const [name, { line, file }] = authDeps.entries().next().value as [string, { line: number; file: string }];
|
|
177
|
-
conventions.auth_dep = name;
|
|
178
|
-
provenance.push({ field: 'auth_dep', sourceFile: file, line, query: 'fastapi-auth-dep' });
|
|
179
|
-
} else if (authDeps.size >= 2) {
|
|
180
|
-
// Ambiguous — prefer the first-seen (stable order from input file list).
|
|
181
|
-
const [name, { line, file }] = authDeps.entries().next().value as [string, { line: number; file: string }];
|
|
182
|
-
conventions.auth_dep = name;
|
|
183
|
-
provenance.push({ field: 'auth_dep', sourceFile: file, line, query: 'fastapi-auth-dep' });
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (prefixBases.size >= 1) {
|
|
187
|
-
const [base, { line, file }] = prefixBases.entries().next().value as [string, { line: number; file: string }];
|
|
188
|
-
conventions.api_prefix_base = base;
|
|
189
|
-
provenance.push({ field: 'api_prefix_base', sourceFile: file, line, query: 'fastapi-api-prefix' });
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (testAsyncPatterns.size >= 1) {
|
|
193
|
-
const [pat, { line, file }] = testAsyncPatterns.entries().next().value as [string, { line: number; file: string }];
|
|
194
|
-
conventions.test_async_pattern = pat;
|
|
195
|
-
provenance.push({ field: 'test_async_pattern', sourceFile: file, line, query: 'fastapi-pytest-asyncio' });
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
let confidence: AdapterResult['confidence'];
|
|
199
|
-
if (Object.keys(conventions).length === 0) {
|
|
200
|
-
confidence = 'none';
|
|
201
|
-
} else if (authDeps.size === 1 || (authDeps.size === 0 && prefixBases.size > 0)) {
|
|
202
|
-
confidence = 'high';
|
|
203
|
-
} else if (authDeps.size >= 2) {
|
|
204
|
-
confidence = 'low';
|
|
205
|
-
} else {
|
|
206
|
-
confidence = 'medium';
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return { conventions, provenance, confidence };
|
|
210
|
-
},
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
// ============================================================
|
|
214
|
-
// Helpers
|
|
215
|
-
// ============================================================
|
|
216
|
-
|
|
217
|
-
function extractPrefixBase(prefix: string): string | null {
|
|
218
|
-
if (!prefix.startsWith('/')) return null;
|
|
219
|
-
const stripped = prefix.replace(/^\/+/, '');
|
|
220
|
-
const firstSeg = stripped.split('/')[0];
|
|
221
|
-
if (!firstSeg) return null;
|
|
222
|
-
return '/' + firstSeg;
|
|
223
|
-
}
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Plan 3c — Phase 7: Flask AST adapter.
|
|
6
|
-
*
|
|
7
|
-
* First Phase 7 framework after Plan 3b's 4 (FastAPI/Django/tRPC/SwiftUI).
|
|
8
|
-
* Establishes the per-framework deliverable pattern (4 artifacts) all
|
|
9
|
-
* remaining Phase 7 frameworks follow:
|
|
10
|
-
* 1. packages/core/templates/python-flask/massu.config.yaml (variant template)
|
|
11
|
-
* 2. packages/core/src/detect/adapters/python-flask.ts (this file — AST adapter)
|
|
12
|
-
* 3. Adversarial fixtures (inline in the test file via mkdirSync+writeFileSync,
|
|
13
|
-
* following the python-fastapi pattern in ast-adapters-adversarial.test.ts)
|
|
14
|
-
* 4. packages/core/src/__tests__/python-flask.test.ts (golden-output snapshot)
|
|
15
|
-
*
|
|
16
|
-
* Extracts:
|
|
17
|
-
* - auth_decorator: name of the auth-gating decorator (`@login_required`,
|
|
18
|
-
* `@auth_required`, or other Flask-Login-style decorator on a view).
|
|
19
|
-
* - blueprint_url_prefix: first path segment of `Blueprint(name, __name__,
|
|
20
|
-
* url_prefix="/api/...")`, mirroring the python-fastapi APIRouter prefix
|
|
21
|
-
* extraction.
|
|
22
|
-
* - app_factory: name of the Flask app-factory function (`def create_app():`
|
|
23
|
-
* or similar). Useful for scaffold-router templates that need to know
|
|
24
|
-
* where to register a new Blueprint.
|
|
25
|
-
*
|
|
26
|
-
* Confidence rules (mirror python-fastapi):
|
|
27
|
-
* - 'high' if exactly ONE auth_decorator is found (clear single convention).
|
|
28
|
-
* - 'medium' if blueprint_url_prefix or app_factory found but no auth decorator.
|
|
29
|
-
* - 'low' if multiple distinct auth_decorators are found (ambiguous).
|
|
30
|
-
* - 'none' if no Flask signals at all (regex fallback takes over).
|
|
31
|
-
*
|
|
32
|
-
* Does NOT use regex on file content — only Tree-sitter S-expression queries
|
|
33
|
-
* compiled via query-helpers.ts. Regex would be the regex-fallback path.
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
import { Parser } from 'web-tree-sitter';
|
|
37
|
-
import type { CodebaseAdapter, AdapterResult, DetectionSignals, Provenance, SourceFile } from './types.ts';
|
|
38
|
-
import { runQuery, InvalidQueryError } from './query-helpers.ts';
|
|
39
|
-
import { loadGrammar } from './tree-sitter-loader.ts';
|
|
40
|
-
import { isParsableSource, MAX_AST_FILE_BYTES } from './parse-guard.ts';
|
|
41
|
-
|
|
42
|
-
// ============================================================
|
|
43
|
-
// Tree-sitter S-expression queries
|
|
44
|
-
// ============================================================
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Auth decorator: matches `@login_required`, `@auth_required`,
|
|
48
|
-
* `@some_auth_required`, etc. on a view function.
|
|
49
|
-
*
|
|
50
|
-
* The Tree-sitter Python grammar represents `@some_name` decorators as
|
|
51
|
-
* a `decorator` node containing an `identifier` (for bare names) OR an
|
|
52
|
-
* `attribute` (for `@module.name`). We capture both shapes and filter to
|
|
53
|
-
* names ending in `_required` since that's the Flask-Login convention
|
|
54
|
-
* and avoids matching unrelated decorators like `@app.route(...)`.
|
|
55
|
-
*/
|
|
56
|
-
const AUTH_DECORATOR_QUERY = `
|
|
57
|
-
(decorator
|
|
58
|
-
(identifier) @auth_decorator (#match? @auth_decorator "_required$"))
|
|
59
|
-
`;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Blueprint URL prefix: `Blueprint(name, __name__, url_prefix="/api/orders")`.
|
|
63
|
-
* Captures the keyword-arg string literal so the runner can split off the
|
|
64
|
-
* base segment.
|
|
65
|
-
*/
|
|
66
|
-
const BLUEPRINT_URL_PREFIX_QUERY = `
|
|
67
|
-
(call
|
|
68
|
-
function: (identifier) @_callee (#eq? @_callee "Blueprint")
|
|
69
|
-
arguments: (argument_list
|
|
70
|
-
(keyword_argument
|
|
71
|
-
name: (identifier) @_kw (#eq? @_kw "url_prefix")
|
|
72
|
-
value: (string) @url_prefix)))
|
|
73
|
-
`;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* App factory: `def create_app():` (or any function whose name starts with
|
|
77
|
-
* `create_` and contains `Flask`). The factory pattern is canonical in Flask
|
|
78
|
-
* (per Flask docs: https://flask.palletsprojects.com/en/2.3.x/patterns/appfactories/).
|
|
79
|
-
* We capture the function name + assert its body contains `Flask(`.
|
|
80
|
-
*
|
|
81
|
-
* The Tree-sitter Python grammar represents `def name():` as a
|
|
82
|
-
* `function_definition` node with `name: (identifier)` field.
|
|
83
|
-
*/
|
|
84
|
-
const APP_FACTORY_QUERY = `
|
|
85
|
-
(function_definition
|
|
86
|
-
name: (identifier) @factory_name (#match? @factory_name "^create_")
|
|
87
|
-
body: (block
|
|
88
|
-
(expression_statement
|
|
89
|
-
(assignment
|
|
90
|
-
right: (call
|
|
91
|
-
function: (identifier) @_flask_call (#eq? @_flask_call "Flask"))))))
|
|
92
|
-
`;
|
|
93
|
-
|
|
94
|
-
// ============================================================
|
|
95
|
-
// Adapter
|
|
96
|
-
// ============================================================
|
|
97
|
-
|
|
98
|
-
export const pythonFlaskAdapter: CodebaseAdapter = {
|
|
99
|
-
id: 'python-flask',
|
|
100
|
-
languages: ['python'],
|
|
101
|
-
|
|
102
|
-
matches(signals: DetectionSignals): boolean {
|
|
103
|
-
// Cheap signal-only check. No file IO. Match if:
|
|
104
|
-
// 1. pyproject.toml mentions flask (raw text contains 'flask'), OR
|
|
105
|
-
// 2. project has app/ + python files at top level (Flask convention)
|
|
106
|
-
const pyToml = signals.pyprojectToml as { __raw?: string } | undefined;
|
|
107
|
-
if (pyToml?.__raw && /\bflask\b/i.test(pyToml.__raw)) return true;
|
|
108
|
-
if (signals.presentDirs.has('app') && signals.presentFiles.has('app.py')) return true;
|
|
109
|
-
if (signals.presentDirs.has('app') && signals.presentFiles.has('wsgi.py')) return true;
|
|
110
|
-
return false;
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
async introspect(files: SourceFile[], _rootDir: string): Promise<AdapterResult> {
|
|
114
|
-
if (files.length === 0) {
|
|
115
|
-
return { conventions: {}, provenance: [], confidence: 'none' };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
let language;
|
|
119
|
-
try {
|
|
120
|
-
language = await loadGrammar('python');
|
|
121
|
-
} catch (e) {
|
|
122
|
-
// Grammar unavailable → adapter returns 'none' so regex fallback takes over.
|
|
123
|
-
return { conventions: {}, provenance: [], confidence: 'none' };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const parser = new Parser();
|
|
127
|
-
parser.setLanguage(language);
|
|
128
|
-
|
|
129
|
-
const authDecorators = new Map<string, { line: number; file: string }>();
|
|
130
|
-
const urlPrefixes = new Map<string, { line: number; file: string }>();
|
|
131
|
-
const appFactories = new Map<string, { line: number; file: string }>();
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
for (const file of files) {
|
|
135
|
-
// Phase 3.5 defense-in-depth size + depth gate at adapter tier.
|
|
136
|
-
const skip = isParsableSource(file.content, file.size);
|
|
137
|
-
if (skip) {
|
|
138
|
-
process.stderr.write(
|
|
139
|
-
`[massu/ast] WARN: python-flask skipping ${file.path}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES}. (Phase 3.5 mitigation)\n`,
|
|
140
|
-
);
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
try {
|
|
144
|
-
for (const hit of runQuery(parser, file.content, AUTH_DECORATOR_QUERY, 'flask-auth-decorator', file.path)) {
|
|
145
|
-
const name = hit.captures.auth_decorator;
|
|
146
|
-
if (name && !authDecorators.has(name)) {
|
|
147
|
-
authDecorators.set(name, { line: hit.line, file: file.path });
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
for (const hit of runQuery(parser, file.content, BLUEPRINT_URL_PREFIX_QUERY, 'flask-blueprint-url-prefix', file.path)) {
|
|
151
|
-
const raw = hit.captures.url_prefix;
|
|
152
|
-
if (!raw) continue;
|
|
153
|
-
const literal = raw.replace(/^['"]/, '').replace(/['"]$/, '');
|
|
154
|
-
const base = extractPrefixBase(literal);
|
|
155
|
-
if (base && !urlPrefixes.has(base)) {
|
|
156
|
-
urlPrefixes.set(base, { line: hit.line, file: file.path });
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
for (const hit of runQuery(parser, file.content, APP_FACTORY_QUERY, 'flask-app-factory', file.path)) {
|
|
160
|
-
const name = hit.captures.factory_name;
|
|
161
|
-
if (name && !appFactories.has(name)) {
|
|
162
|
-
appFactories.set(name, { line: hit.line, file: file.path });
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
} catch (e) {
|
|
166
|
-
if (e instanceof InvalidQueryError) {
|
|
167
|
-
// Compile-time failure of OUR query is a developer bug — surface it.
|
|
168
|
-
throw e;
|
|
169
|
-
}
|
|
170
|
-
// Per-file parse error: skip + keep going.
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
} finally {
|
|
175
|
-
try { parser.delete(); } catch { /* ignore */ }
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Build result
|
|
179
|
-
const conventions: Record<string, unknown> = {};
|
|
180
|
-
const provenance: Provenance[] = [];
|
|
181
|
-
|
|
182
|
-
if (authDecorators.size === 1) {
|
|
183
|
-
const [name, { line, file }] = authDecorators.entries().next().value as [string, { line: number; file: string }];
|
|
184
|
-
conventions.auth_decorator = name;
|
|
185
|
-
provenance.push({ field: 'auth_decorator', sourceFile: file, line, query: 'flask-auth-decorator' });
|
|
186
|
-
} else if (authDecorators.size >= 2) {
|
|
187
|
-
// Ambiguous — prefer first-seen (stable order from input file list).
|
|
188
|
-
const [name, { line, file }] = authDecorators.entries().next().value as [string, { line: number; file: string }];
|
|
189
|
-
conventions.auth_decorator = name;
|
|
190
|
-
provenance.push({ field: 'auth_decorator', sourceFile: file, line, query: 'flask-auth-decorator' });
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (urlPrefixes.size >= 1) {
|
|
194
|
-
const [base, { line, file }] = urlPrefixes.entries().next().value as [string, { line: number; file: string }];
|
|
195
|
-
conventions.blueprint_url_prefix = base;
|
|
196
|
-
provenance.push({ field: 'blueprint_url_prefix', sourceFile: file, line, query: 'flask-blueprint-url-prefix' });
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (appFactories.size >= 1) {
|
|
200
|
-
const [name, { line, file }] = appFactories.entries().next().value as [string, { line: number; file: string }];
|
|
201
|
-
conventions.app_factory = name;
|
|
202
|
-
provenance.push({ field: 'app_factory', sourceFile: file, line, query: 'flask-app-factory' });
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
let confidence: AdapterResult['confidence'];
|
|
206
|
-
if (Object.keys(conventions).length === 0) {
|
|
207
|
-
confidence = 'none';
|
|
208
|
-
} else if (authDecorators.size === 1) {
|
|
209
|
-
confidence = 'high';
|
|
210
|
-
} else if (authDecorators.size >= 2) {
|
|
211
|
-
confidence = 'low';
|
|
212
|
-
} else {
|
|
213
|
-
confidence = 'medium';
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return { conventions, provenance, confidence };
|
|
217
|
-
},
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
// ============================================================
|
|
221
|
-
// Helpers
|
|
222
|
-
// ============================================================
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Extract the first path segment of a Blueprint url_prefix. Mirrors
|
|
226
|
-
* python-fastapi's extractPrefixBase. Returns null if input doesn't
|
|
227
|
-
* start with `/`.
|
|
228
|
-
*/
|
|
229
|
-
function extractPrefixBase(prefix: string): string | null {
|
|
230
|
-
if (!prefix.startsWith('/')) return null;
|
|
231
|
-
const stripped = prefix.replace(/^\/+/, '');
|
|
232
|
-
const firstSeg = stripped.split('/')[0];
|
|
233
|
-
if (!firstSeg) return null;
|
|
234
|
-
return '/' + firstSeg;
|
|
235
|
-
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Plan 3b — Phase 1: Tree-sitter query wrapper.
|
|
6
|
-
*
|
|
7
|
-
* Adapters consume the helpers in this file — never the raw `web-tree-sitter`
|
|
8
|
-
* API. This keeps the surface area minimal and testable.
|
|
9
|
-
*
|
|
10
|
-
* Design:
|
|
11
|
-
* - `compileQuery` caches compiled `Query` instances per (language, source)
|
|
12
|
-
* tuple. Compiling an S-expression is non-trivial; cache hit-rate is
|
|
13
|
-
* critical when the same query runs across N sampled files.
|
|
14
|
-
* - `runQuery` returns the captures as `{captures, file, line}` records so
|
|
15
|
-
* adapters never need to touch raw `Node` objects.
|
|
16
|
-
* - `InvalidQueryError` is the typed error thrown when an S-expression is
|
|
17
|
-
* malformed; never let a raw `Error` reach the adapter (per audit-iter-5
|
|
18
|
-
* fix HH test (b)).
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { Query, type Language, type Node, type Parser, type QueryMatch } from 'web-tree-sitter';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Thrown when an S-expression query string fails to compile against the
|
|
25
|
-
* supplied grammar. Carries the original message and the offending source
|
|
26
|
-
* so adapter authors can debug.
|
|
27
|
-
*/
|
|
28
|
-
export class InvalidQueryError extends Error {
|
|
29
|
-
public readonly queryName: string;
|
|
30
|
-
public readonly querySource: string;
|
|
31
|
-
public readonly cause?: unknown;
|
|
32
|
-
constructor(queryName: string, querySource: string, cause: unknown) {
|
|
33
|
-
const causeMsg = cause instanceof Error ? cause.message : String(cause);
|
|
34
|
-
super(
|
|
35
|
-
`[query-helpers] Invalid Tree-sitter query "${queryName}": ${causeMsg}\n` +
|
|
36
|
-
`Query source:\n${querySource}`,
|
|
37
|
-
);
|
|
38
|
-
this.name = 'InvalidQueryError';
|
|
39
|
-
this.queryName = queryName;
|
|
40
|
-
this.querySource = querySource;
|
|
41
|
-
this.cause = cause;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ============================================================
|
|
46
|
-
// Query compile cache
|
|
47
|
-
// ============================================================
|
|
48
|
-
|
|
49
|
-
// We key by Language identity (not by name) AND by source string. The Query
|
|
50
|
-
// type from web-tree-sitter is opaque; we store it directly.
|
|
51
|
-
const queryCache = new WeakMap<Language, Map<string, Query>>();
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Compile (and cache) an S-expression query against `language`.
|
|
55
|
-
*
|
|
56
|
-
* Throws `InvalidQueryError` (NOT raw Error) on malformed S-expressions —
|
|
57
|
-
* adapters can catch this without losing the typed boundary.
|
|
58
|
-
*
|
|
59
|
-
* Cache lookup is O(1) on the (Language, source) tuple via WeakMap+Map.
|
|
60
|
-
*/
|
|
61
|
-
export function compileQuery(
|
|
62
|
-
language: Language,
|
|
63
|
-
source: string,
|
|
64
|
-
queryName: string,
|
|
65
|
-
): Query {
|
|
66
|
-
let perLang = queryCache.get(language);
|
|
67
|
-
if (!perLang) {
|
|
68
|
-
perLang = new Map();
|
|
69
|
-
queryCache.set(language, perLang);
|
|
70
|
-
}
|
|
71
|
-
const cached = perLang.get(source);
|
|
72
|
-
if (cached) return cached;
|
|
73
|
-
|
|
74
|
-
let q: Query;
|
|
75
|
-
try {
|
|
76
|
-
q = new Query(language, source);
|
|
77
|
-
} catch (e) {
|
|
78
|
-
throw new InvalidQueryError(queryName, source, e);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
perLang.set(source, q);
|
|
82
|
-
return q;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ============================================================
|
|
86
|
-
// Capture extraction
|
|
87
|
-
// ============================================================
|
|
88
|
-
|
|
89
|
-
export interface RunQueryHit {
|
|
90
|
-
/**
|
|
91
|
-
* Capture name → captured text. If the same capture name appears multiple
|
|
92
|
-
* times in a single match, the LAST occurrence wins (callers usually want
|
|
93
|
-
* the most-specific one).
|
|
94
|
-
*/
|
|
95
|
-
captures: Record<string, string>;
|
|
96
|
-
/** Absolute path to the file being parsed. */
|
|
97
|
-
file: string;
|
|
98
|
-
/** 1-based line number of the FIRST capture in the match. */
|
|
99
|
-
line: number;
|
|
100
|
-
/** Name of the query (used for provenance). */
|
|
101
|
-
queryName: string;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Run a compiled query against a parsed tree. Returns a flat list of hits.
|
|
106
|
-
*
|
|
107
|
-
* Each match becomes one `RunQueryHit`. The `line` is computed from the
|
|
108
|
-
* earliest-starting capture in the match (1-based). Note that this helper is
|
|
109
|
-
* intentionally narrow — it is NOT a general node-walker. Adapters that need
|
|
110
|
-
* tree traversal should compose multiple queries instead.
|
|
111
|
-
*/
|
|
112
|
-
export function runQuery(
|
|
113
|
-
parser: Parser,
|
|
114
|
-
source: string,
|
|
115
|
-
queryText: string,
|
|
116
|
-
queryName: string,
|
|
117
|
-
filePath: string,
|
|
118
|
-
): RunQueryHit[] {
|
|
119
|
-
const language = parser.language;
|
|
120
|
-
if (!language) {
|
|
121
|
-
throw new InvalidQueryError(
|
|
122
|
-
queryName,
|
|
123
|
-
queryText,
|
|
124
|
-
new Error('Parser has no language assigned'),
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
const query = compileQuery(language, queryText, queryName);
|
|
128
|
-
|
|
129
|
-
const tree = parser.parse(source);
|
|
130
|
-
if (!tree) return [];
|
|
131
|
-
|
|
132
|
-
let matches: QueryMatch[];
|
|
133
|
-
try {
|
|
134
|
-
matches = query.matches(tree.rootNode);
|
|
135
|
-
} catch (e) {
|
|
136
|
-
// Match-time errors are unusual (compile-time catches most), but we still
|
|
137
|
-
// wrap to keep the typed-error contract.
|
|
138
|
-
throw new InvalidQueryError(queryName, queryText, e);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const out: RunQueryHit[] = [];
|
|
142
|
-
for (const match of matches) {
|
|
143
|
-
if (!match.captures || match.captures.length === 0) continue;
|
|
144
|
-
const captures: Record<string, string> = {};
|
|
145
|
-
let earliestLine = Number.POSITIVE_INFINITY;
|
|
146
|
-
for (const cap of match.captures) {
|
|
147
|
-
const node: Node = cap.node;
|
|
148
|
-
captures[cap.name] = node.text;
|
|
149
|
-
if (node.startPosition.row + 1 < earliestLine) {
|
|
150
|
-
earliestLine = node.startPosition.row + 1;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
out.push({
|
|
154
|
-
captures,
|
|
155
|
-
file: filePath,
|
|
156
|
-
line: Number.isFinite(earliestLine) ? earliestLine : 1,
|
|
157
|
-
queryName,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Per Tree-sitter docs: trees should be deleted to free WASM memory.
|
|
162
|
-
// Adapters call runQuery once per file so this cleanup is local.
|
|
163
|
-
try {
|
|
164
|
-
tree.delete();
|
|
165
|
-
} catch {
|
|
166
|
-
/* deletion is best-effort — some test mocks don't implement delete */
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return out;
|
|
170
|
-
}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
// Plan 3c Phase 9b P-A-005: re-export shim. Source-of-truth lives at
|
|
2
|
-
// `packages/adapter-rails/src/index.ts` (workspace package). This shim
|
|
3
|
-
// preserves the legacy import path used by codebase-introspector + tests.
|
|
4
|
-
export { railsAdapter } from '@massu/adapter-rails';
|