@massu/core 1.10.8 → 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.
Files changed (178) hide show
  1. package/CHANGELOG.md +1303 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +517 -397
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +66 -18
  16. package/dist/hooks/pre-compact.js +27 -2
  17. package/dist/hooks/pre-delete-check.js +48 -22
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +24 -0
  23. package/dist/hooks/session-start.js +8710 -8460
  24. package/dist/hooks/user-prompt.js +24 -0
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/framework-supports.ts +0 -85
  110. package/src/lib/gitToplevel.ts +0 -22
  111. package/src/lib/hook-registry.ts +0 -43
  112. package/src/lib/installLock.ts +0 -66
  113. package/src/lib/memory-path.ts +0 -49
  114. package/src/lib/pidLiveness.ts +0 -67
  115. package/src/lib/settings-local.ts +0 -110
  116. package/src/lib/sql-table-names.ts +0 -71
  117. package/src/license.ts +0 -547
  118. package/src/lsp/auto-detect.ts +0 -98
  119. package/src/lsp/client.ts +0 -776
  120. package/src/lsp/enrich.ts +0 -127
  121. package/src/lsp/types.ts +0 -221
  122. package/src/memory-db.ts +0 -1614
  123. package/src/memory-file-ingest.ts +0 -128
  124. package/src/memory-tools.ts +0 -424
  125. package/src/middleware-tree.ts +0 -71
  126. package/src/observability-tools.ts +0 -343
  127. package/src/observation-extractor.ts +0 -418
  128. package/src/page-deps.ts +0 -285
  129. package/src/permissions.ts +0 -422
  130. package/src/prompt-analyzer.ts +0 -332
  131. package/src/python/coupling-detector.ts +0 -125
  132. package/src/python/domain-enforcer.ts +0 -84
  133. package/src/python/impact-analyzer.ts +0 -96
  134. package/src/python/import-parser.ts +0 -244
  135. package/src/python/import-resolver.ts +0 -136
  136. package/src/python/migration-indexer.ts +0 -116
  137. package/src/python/migration-parser.ts +0 -332
  138. package/src/python/model-indexer.ts +0 -71
  139. package/src/python/model-parser.ts +0 -279
  140. package/src/python/route-indexer.ts +0 -59
  141. package/src/python/route-parser.ts +0 -317
  142. package/src/python-tools.ts +0 -630
  143. package/src/regression-detector.ts +0 -319
  144. package/src/rules.ts +0 -57
  145. package/src/schema-mapper.ts +0 -232
  146. package/src/security/adapter-origin.ts +0 -130
  147. package/src/security/adapter-verifier.ts +0 -319
  148. package/src/security/atomic-write.ts +0 -164
  149. package/src/security/fetcher.ts +0 -200
  150. package/src/security/install-tracking.ts +0 -319
  151. package/src/security/license-pubkey.generated.ts +0 -30
  152. package/src/security/license-response-verifier.ts +0 -149
  153. package/src/security/local-fingerprint.ts +0 -225
  154. package/src/security/manifest-cache.ts +0 -333
  155. package/src/security/manifest-schema.ts +0 -129
  156. package/src/security/registry-pubkey.generated.ts +0 -35
  157. package/src/security/telemetry.ts +0 -320
  158. package/src/security-scorer.ts +0 -405
  159. package/src/security-utils.ts +0 -133
  160. package/src/sentinel-db.ts +0 -580
  161. package/src/sentinel-scanner.ts +0 -406
  162. package/src/sentinel-tools.ts +0 -513
  163. package/src/sentinel-types.ts +0 -140
  164. package/src/server-dispatch.ts +0 -225
  165. package/src/server.ts +0 -107
  166. package/src/session-archiver.ts +0 -111
  167. package/src/session-state-generator.ts +0 -174
  168. package/src/team-knowledge.ts +0 -407
  169. package/src/tool-db-needs.ts +0 -232
  170. package/src/tools.ts +0 -1206
  171. package/src/transcript-parser.ts +0 -458
  172. package/src/trpc-index.ts +0 -215
  173. package/src/validate-features-runner.ts +0 -107
  174. package/src/validation-engine.ts +0 -358
  175. package/src/watch/daemon.ts +0 -385
  176. package/src/watch/lockfile-detector.ts +0 -65
  177. package/src/watch/paths.ts +0 -279
  178. package/src/watch/state.ts +0 -178
@@ -1,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';