@massu/core 1.5.8 → 1.6.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.
@@ -1,279 +1,4 @@
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: Rails AST adapter.
6
- *
7
- * Second Phase 7 framework after go-chi; first to consume the new `ruby`
8
- * Tree-sitter grammar entry from GRAMMAR_MANIFEST (commit fbb8aa9). Together
9
- * with the @massu/adapter-rails workspace stub created in Stage 2 P1-004
10
- * (commit ebf2983), this completes the per-framework deliverable pattern
11
- * established by Phase 7 commits 1–3:
12
- * 1. packages/core/templates/rails/massu.config.yaml (variant template)
13
- * 2. packages/core/src/detect/adapters/rails.ts (this file — AST adapter)
14
- * 3. Adversarial fixtures (inline in the test file via mkdirSync+writeFileSync)
15
- * 4. packages/core/src/__tests__/rails.test.ts (golden-output test)
16
- *
17
- * Rails uses a DSL-heavy `config/routes.rb` (per Rails routing guide:
18
- * https://guides.rubyonrails.org/routing.html). The adapter walks the
19
- * routes.rb DSL invocations directly rather than scanning controller
20
- * directories, mirroring the go-chi approach against router.go files.
21
- *
22
- * Extracts:
23
- * - route_method: most-common explicit HTTP verb (`get`, `post`, `put`,
24
- * `patch`, `delete`, `head`, `options`) used at TOP LEVEL with a
25
- * string-literal path argument. Mirrors python-fastapi/python-flask/
26
- * go-chi route_method semantics. Distinct from `resources :users`
27
- * RESTful blocks — those imply all seven verbs in one declaration and
28
- * should NOT pin the project to one verb. The string-literal anchor
29
- * also excludes member/collection block calls like `get :preview`
30
- * (where the arg is a symbol, not a string).
31
- * - api_namespace: first segment of the first `namespace :foo do …` or
32
- * `namespace 'foo' do …` block, normalized to a leading-slash path
33
- * (mirrors python-fastapi/flask's blueprint_url_prefix and go-chi's
34
- * mount_prefix_base). Per Rails routing guide §3:
35
- * https://guides.rubyonrails.org/routing.html#controller-namespaces-and-routing
36
- * - root_controller: controller name from `root 'pages#home'` or
37
- * `root to: 'pages#home'`. The `Foo#bar` syntax canonically denotes
38
- * `FooController#bar`. Useful for scaffold-router/scaffold-page
39
- * templates that need to know the project's home route convention.
40
- *
41
- * Confidence rules (mirror go-chi):
42
- * - 'high' if exactly ONE distinct route_method seen (clear convention).
43
- * - 'low' if multiple distinct route_methods seen (mixed convention).
44
- * - 'medium' if api_namespace or root_controller found but no route_method.
45
- * - 'none' if no Rails DSL signals at all (regex fallback takes over).
46
- *
47
- * Does NOT use regex on file content — only Tree-sitter S-expression queries
48
- * compiled via query-helpers.ts. Regex would be the regex-fallback path.
49
- */
50
-
51
- import { Parser } from 'web-tree-sitter';
52
- import type { CodebaseAdapter, AdapterResult, DetectionSignals, Provenance, SourceFile } from './types.ts';
53
- import { runQuery, InvalidQueryError } from './query-helpers.ts';
54
- import { loadGrammar } from './tree-sitter-loader.ts';
55
- import { isParsableSource, MAX_AST_FILE_BYTES } from './parse-guard.ts';
56
-
57
- // ============================================================
58
- // Tree-sitter S-expression queries (Ruby grammar)
59
- // ============================================================
60
-
61
- /**
62
- * HTTP method route registration with a STRING-LITERAL first argument:
63
- * `get '/health', to: 'health#show'`, `post "/login", to: 'sessions#create'`.
64
- *
65
- * Anchored (`.`) on the first argument so that we ONLY match string-literal
66
- * paths — symbol arguments (`get :preview` inside a member block) are
67
- * deliberately excluded because those are nested action declarations, NOT
68
- * top-level routes.
69
- *
70
- * tree-sitter-ruby (v0.20.1, pinned by tree-sitter-wasms@0.1.13) emits
71
- * `(call method: (identifier) arguments: (argument_list ...))` for the
72
- * parens-less DSL form `get '/x', ...` AND for the parens-ful `get('/x', ...)`
73
- * form. Verified by AST probe (2026-05-07): there is NO separate `method_call`
74
- * node in this grammar — historical web sources mentioning `method_call`
75
- * refer to a different/older grammar fork.
76
- */
77
- const ROUTE_METHOD_QUERY = `
78
- (call
79
- method: (identifier) @method (#match? @method "^(get|post|put|patch|delete|options|head)$")
80
- arguments: (argument_list
81
- .
82
- (string) @route_path))
83
- `;
84
-
85
- /**
86
- * Namespace block: `namespace :api do ... end` or `namespace 'api' do ... end`.
87
- * Captures the first symbol-or-string argument so we can normalize to a
88
- * leading-slash path.
89
- *
90
- * Rails accepts both `namespace :api` (symbol — canonical) and the rarer
91
- * `namespace 'api'` (string). Both forms produce a `namespace` identifier
92
- * call; the first arg differs only in node type.
93
- */
94
- const NAMESPACE_QUERY = `
95
- (call
96
- method: (identifier) @_method (#eq? @_method "namespace")
97
- arguments: (argument_list
98
- .
99
- [
100
- (simple_symbol) @namespace_symbol
101
- (string) @namespace_string
102
- ]))
103
- `;
104
-
105
- /**
106
- * Root route: `root 'pages#home'`, `root "pages#home"`, or
107
- * `root to: 'pages#home'`. We capture the string literal in either the
108
- * positional or `to:` keyword position; the controller is whatever
109
- * precedes the `#`.
110
- *
111
- * Rails routing guide §2.6:
112
- * https://guides.rubyonrails.org/routing.html#using-root
113
- */
114
- const ROOT_ROUTE_QUERY = `
115
- (call
116
- method: (identifier) @_method (#eq? @_method "root")
117
- arguments: (argument_list
118
- .
119
- (string) @root_target))
120
-
121
- (call
122
- method: (identifier) @_method (#eq? @_method "root")
123
- arguments: (argument_list
124
- (pair
125
- key: (hash_key_symbol) @_key (#eq? @_key "to")
126
- value: (string) @root_target)))
127
- `;
128
-
129
- // ============================================================
130
- // Adapter
131
- // ============================================================
132
-
133
- export const railsAdapter: CodebaseAdapter = {
134
- id: 'rails',
135
- languages: ['ruby'],
136
-
137
- matches(signals: DetectionSignals): boolean {
138
- // Cheap signal-only check. No file IO. The canonical Rails declaration
139
- // is `gem 'rails'` in Gemfile (per Rails install guide:
140
- // https://guides.rubyonrails.org/getting_started.html). The strict
141
- // `gem ['"]rails['"]` regex deliberately rejects `gem 'rails-api'`,
142
- // `gem 'rails_admin'`, etc. — those are Rails-adjacent gems that may
143
- // appear in non-Rails Ruby projects.
144
- if (!signals.gemfile) return false;
145
- return /^\s*gem\s+['"]rails['"]/im.test(signals.gemfile);
146
- },
147
-
148
- async introspect(files: SourceFile[], _rootDir: string): Promise<AdapterResult> {
149
- if (files.length === 0) {
150
- return { conventions: {}, provenance: [], confidence: 'none' };
151
- }
152
-
153
- let language;
154
- try {
155
- language = await loadGrammar('ruby');
156
- } catch (e) {
157
- // Grammar unavailable → adapter returns 'none' so regex fallback takes over.
158
- return { conventions: {}, provenance: [], confidence: 'none' };
159
- }
160
-
161
- const parser = new Parser();
162
- parser.setLanguage(language);
163
-
164
- const routeMethods = new Map<string, { line: number; file: string }>();
165
- const namespaces = new Map<string, { line: number; file: string }>();
166
- const rootControllers = new Map<string, { line: number; file: string }>();
167
-
168
- try {
169
- for (const file of files) {
170
- // Phase 3.5 defense-in-depth size + depth gate at adapter tier.
171
- const skip = isParsableSource(file.content, file.size);
172
- if (skip) {
173
- process.stderr.write(
174
- `[massu/ast] WARN: rails skipping ${file.path}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES}. (Phase 3.5 mitigation)\n`,
175
- );
176
- continue;
177
- }
178
- try {
179
- for (const hit of runQuery(parser, file.content, ROUTE_METHOD_QUERY, 'rails-route-method', file.path)) {
180
- const method = hit.captures.method;
181
- if (method && !routeMethods.has(method)) {
182
- routeMethods.set(method, { line: hit.line, file: file.path });
183
- }
184
- }
185
- for (const hit of runQuery(parser, file.content, NAMESPACE_QUERY, 'rails-namespace', file.path)) {
186
- const symbolRaw = hit.captures.namespace_symbol;
187
- const stringRaw = hit.captures.namespace_string;
188
- const name = symbolRaw
189
- ? symbolRaw.replace(/^:/, '')
190
- : stringRaw
191
- ? stringRaw.replace(/^['"]/, '').replace(/['"]$/, '')
192
- : null;
193
- if (!name) continue;
194
- const path = '/' + name;
195
- if (!namespaces.has(path)) {
196
- namespaces.set(path, { line: hit.line, file: file.path });
197
- }
198
- }
199
- for (const hit of runQuery(parser, file.content, ROOT_ROUTE_QUERY, 'rails-root', file.path)) {
200
- const raw = hit.captures.root_target;
201
- if (!raw) continue;
202
- const literal = raw.replace(/^['"]/, '').replace(/['"]$/, '');
203
- const controller = extractRootController(literal);
204
- if (controller && !rootControllers.has(controller)) {
205
- rootControllers.set(controller, { line: hit.line, file: file.path });
206
- }
207
- }
208
- } catch (e) {
209
- if (e instanceof InvalidQueryError) {
210
- // Compile-time failure of OUR query is a developer bug — surface it.
211
- throw e;
212
- }
213
- // Per-file parse error: skip + keep going.
214
- continue;
215
- }
216
- }
217
- } finally {
218
- try { parser.delete(); } catch { /* ignore */ }
219
- }
220
-
221
- const conventions: Record<string, unknown> = {};
222
- const provenance: Provenance[] = [];
223
-
224
- if (routeMethods.size === 1) {
225
- const [name, { line, file }] = routeMethods.entries().next().value as [string, { line: number; file: string }];
226
- conventions.route_method = name;
227
- provenance.push({ field: 'route_method', sourceFile: file, line, query: 'rails-route-method' });
228
- } else if (routeMethods.size >= 2) {
229
- // Mixed convention — emit first-seen for visibility.
230
- const [name, { line, file }] = routeMethods.entries().next().value as [string, { line: number; file: string }];
231
- conventions.route_method = name;
232
- provenance.push({ field: 'route_method', sourceFile: file, line, query: 'rails-route-method' });
233
- }
234
-
235
- if (namespaces.size >= 1) {
236
- const [path, { line, file }] = namespaces.entries().next().value as [string, { line: number; file: string }];
237
- conventions.api_namespace = path;
238
- provenance.push({ field: 'api_namespace', sourceFile: file, line, query: 'rails-namespace' });
239
- }
240
-
241
- if (rootControllers.size >= 1) {
242
- const [name, { line, file }] = rootControllers.entries().next().value as [string, { line: number; file: string }];
243
- conventions.root_controller = name;
244
- provenance.push({ field: 'root_controller', sourceFile: file, line, query: 'rails-root' });
245
- }
246
-
247
- let confidence: AdapterResult['confidence'];
248
- if (Object.keys(conventions).length === 0) {
249
- confidence = 'none';
250
- } else if (routeMethods.size === 1) {
251
- confidence = 'high';
252
- } else if (routeMethods.size >= 2) {
253
- confidence = 'low';
254
- } else {
255
- confidence = 'medium';
256
- }
257
-
258
- return { conventions, provenance, confidence };
259
- },
260
- };
261
-
262
- // ============================================================
263
- // Helpers
264
- // ============================================================
265
-
266
- /**
267
- * Extract the controller name from a Rails `controller#action` string.
268
- * `'pages#home'` → `'pages'`; `'admin/dashboard#index'` → `'admin/dashboard'`.
269
- * Returns null for malformed input (no `#` separator).
270
- *
271
- * Per Rails routing guide §2.6: the string before `#` is the controller
272
- * (with optional namespace prefix), the part after is the action.
273
- */
274
- function extractRootController(target: string): string | null {
275
- const idx = target.indexOf('#');
276
- if (idx <= 0) return null;
277
- const controller = target.slice(0, idx).trim();
278
- return controller || null;
279
- }
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';
@@ -1,284 +1,4 @@
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: Spring (Spring Boot / Spring MVC) AST adapter.
6
- *
7
- * Sixth and final Phase 7 framework after go-chi + Flask + Rails + Phoenix
8
- * + ASP.NET. First to consume the `java` Tree-sitter grammar entry from
9
- * GRAMMAR_MANIFEST (commit fbb8aa9). All four queries verified against
10
- * actual tree-sitter-java AST shape via probe (R-011) BEFORE writing the
11
- * adapter — same discipline as phoenix + aspnet.
12
- *
13
- * Spring uses annotation-based routing per the Spring MVC reference
14
- * (https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller.html):
15
- * - `@RestController` / `@Controller` at the class level
16
- * - `@RequestMapping("/api/users")` at class level for path prefix
17
- * - `@GetMapping("/{id}")` / `@PostMapping` / etc. on methods for verb +
18
- * optional sub-path
19
- *
20
- * Extracts:
21
- * - route_method: most-common HTTP verb captured from
22
- * `@<Verb>Mapping` annotations. Normalized: `GetMapping` → `Get`,
23
- * `PostMapping` → `Post`, etc. Mirrors aspnet's MapGet/HttpGet
24
- * normalization for downstream consumer consistency.
25
- * - route_prefix_base: first segment of the first class-level
26
- * `@RequestMapping("/api/...")` template, normalized to a leading-
27
- * slash path. Mirrors aspnet route_prefix_base / phoenix
28
- * scope_prefix_base.
29
- * - controller_class: name of the first class annotated with
30
- * `@RestController` or `@Controller`. Mirrors aspnet controller_class
31
- * / phoenix router_module.
32
- *
33
- * Confidence rules (mirror phoenix / rails / aspnet):
34
- * - 'high' if exactly ONE distinct route_method seen.
35
- * - 'low' if multiple distinct route_methods seen.
36
- * - 'medium' if route_prefix_base or controller_class found but no
37
- * route_method.
38
- * - 'none' if no Spring signals at all.
39
- *
40
- * Tree-sitter-java AST shape (verified via probe 2026-05-07):
41
- * - Annotations come in TWO node-type flavors:
42
- * - `(marker_annotation name: (identifier))` for parameterless
43
- * annotations like `@PostMapping`, `@RestController`.
44
- * - `(annotation name: (identifier) arguments: (annotation_argument_list
45
- * (string_literal) ...))` for parameterized annotations like
46
- * `@GetMapping("/{id}")`, `@RequestMapping("/api/users")`.
47
- * Adapter queries cover BOTH where applicable.
48
- * - Class declarations: `(class_declaration (modifiers (annotation ...) /
49
- * (marker_annotation ...)) name: (identifier) body: (class_body ...))`.
50
- * - String literals are `(string_literal (string_fragment))`; node.text
51
- * returns the quoted source verbatim.
52
- *
53
- * Does NOT use regex on file content — only Tree-sitter S-expression queries
54
- * compiled via query-helpers.ts.
55
- */
56
-
57
- import { Parser } from 'web-tree-sitter';
58
- import type { CodebaseAdapter, AdapterResult, DetectionSignals, Provenance, SourceFile } from './types.ts';
59
- import { runQuery, InvalidQueryError } from './query-helpers.ts';
60
- import { loadGrammar } from './tree-sitter-loader.ts';
61
- import { isParsableSource, MAX_AST_FILE_BYTES } from './parse-guard.ts';
62
-
63
- // ============================================================
64
- // Tree-sitter S-expression queries (Java grammar)
65
- // ============================================================
66
-
67
- /**
68
- * Parameterized HTTP mapping annotations: `@GetMapping("/{id}")`,
69
- * `@PostMapping("/login")`, etc. Captures both the verb (from the
70
- * annotation name) AND the path string for prefix-base extraction.
71
- *
72
- * Per Spring docs:
73
- * https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-requestmapping.html
74
- */
75
- const HTTP_MAPPING_QUERY = `
76
- (annotation
77
- name: (identifier) @method (#match? @method "^(Get|Post|Put|Patch|Delete|Head|Options)Mapping$")
78
- arguments: (annotation_argument_list
79
- (string_literal) @route_path))
80
- `;
81
-
82
- /**
83
- * Parameterless HTTP mapping annotations: `@PostMapping`, `@DeleteMapping`,
84
- * etc. The tree-sitter-java grammar uses a separate `marker_annotation`
85
- * node for annotations without an argument list — distinct from the
86
- * parameterized `annotation` node above.
87
- */
88
- const HTTP_MAPPING_NO_ARGS_QUERY = `
89
- (marker_annotation
90
- name: (identifier) @method (#match? @method "^(Get|Post|Put|Patch|Delete|Head|Options)Mapping$"))
91
- `;
92
-
93
- /**
94
- * Class-level `@RequestMapping("/api/users")`. Captures the path template
95
- * so we can extract its first segment.
96
- */
97
- const REQUEST_MAPPING_QUERY = `
98
- (annotation
99
- name: (identifier) @_name (#eq? @_name "RequestMapping")
100
- arguments: (annotation_argument_list
101
- (string_literal) @route_template))
102
- `;
103
-
104
- /**
105
- * Class declarations annotated with `@RestController` or `@Controller`.
106
- * Both annotation flavors (marker + parameterized) are captured because
107
- * Spring controllers usually use parameterless `@RestController` /
108
- * `@Controller` but some use `@Controller(value = "name")`.
109
- */
110
- const CONTROLLER_CLASS_QUERY = `
111
- (class_declaration
112
- (modifiers
113
- (marker_annotation
114
- name: (identifier) @_anno (#match? @_anno "^(RestController|Controller)$")))
115
- name: (identifier) @class_name)
116
-
117
- (class_declaration
118
- (modifiers
119
- (annotation
120
- name: (identifier) @_anno (#match? @_anno "^(RestController|Controller)$")))
121
- name: (identifier) @class_name)
122
- `;
123
-
124
- // ============================================================
125
- // Adapter
126
- // ============================================================
127
-
128
- export const springAdapter: CodebaseAdapter = {
129
- id: 'spring',
130
- languages: ['java'],
131
-
132
- matches(signals: DetectionSignals): boolean {
133
- // Cheap signal-only check. No file IO. The canonical Spring Boot
134
- // declaration is the `spring-boot-starter-web` artifact (Maven) or
135
- // dependency string (Gradle), per the Spring Boot reference:
136
- // https://docs.spring.io/spring-boot/reference/using/build-systems.html
137
- if (signals.pomXml && /\bspring-boot-starter[\w-]*\b/.test(signals.pomXml)) {
138
- return true;
139
- }
140
- if (signals.gradleBuild && /\bspring-boot-starter[\w-]*\b/.test(signals.gradleBuild)) {
141
- return true;
142
- }
143
- // Fallback: explicit `spring-webmvc` or `org.springframework` references
144
- // catch projects that use Spring without Spring Boot (rare today but
145
- // canonical Spring MVC pre-Boot apps).
146
- if (signals.pomXml && /\borg\.springframework\b/.test(signals.pomXml)) {
147
- return true;
148
- }
149
- if (signals.gradleBuild && /\borg\.springframework\b/.test(signals.gradleBuild)) {
150
- return true;
151
- }
152
- return false;
153
- },
154
-
155
- async introspect(files: SourceFile[], _rootDir: string): Promise<AdapterResult> {
156
- if (files.length === 0) {
157
- return { conventions: {}, provenance: [], confidence: 'none' };
158
- }
159
-
160
- let language;
161
- try {
162
- language = await loadGrammar('java');
163
- } catch (e) {
164
- return { conventions: {}, provenance: [], confidence: 'none' };
165
- }
166
-
167
- const parser = new Parser();
168
- parser.setLanguage(language);
169
-
170
- const routeMethods = new Map<string, { line: number; file: string }>();
171
- const prefixBases = new Map<string, { line: number; file: string }>();
172
- const controllerClasses = new Map<string, { line: number; file: string }>();
173
-
174
- try {
175
- for (const file of files) {
176
- const skip = isParsableSource(file.content, file.size);
177
- if (skip) {
178
- process.stderr.write(
179
- `[massu/ast] WARN: spring skipping ${file.path}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES}. (Phase 3.5 mitigation)\n`,
180
- );
181
- continue;
182
- }
183
- try {
184
- // Parameterized @<Verb>Mapping("/path")
185
- for (const hit of runQuery(parser, file.content, HTTP_MAPPING_QUERY, 'spring-http-mapping', file.path)) {
186
- const methodRaw = hit.captures.method;
187
- if (!methodRaw) continue;
188
- const verb = methodRaw.replace(/Mapping$/, ''); // GetMapping → Get
189
- if (!routeMethods.has(verb)) {
190
- routeMethods.set(verb, { line: hit.line, file: file.path });
191
- }
192
- }
193
- // Parameterless @<Verb>Mapping
194
- for (const hit of runQuery(parser, file.content, HTTP_MAPPING_NO_ARGS_QUERY, 'spring-http-mapping-marker', file.path)) {
195
- const methodRaw = hit.captures.method;
196
- if (!methodRaw) continue;
197
- const verb = methodRaw.replace(/Mapping$/, '');
198
- if (!routeMethods.has(verb)) {
199
- routeMethods.set(verb, { line: hit.line, file: file.path });
200
- }
201
- }
202
- // @RequestMapping("/api/...")
203
- for (const hit of runQuery(parser, file.content, REQUEST_MAPPING_QUERY, 'spring-request-mapping', file.path)) {
204
- const tplRaw = hit.captures.route_template;
205
- if (!tplRaw) continue;
206
- const literal = tplRaw.replace(/^["']/, '').replace(/["']$/, '');
207
- const base = extractPrefixBase(literal);
208
- if (base && !prefixBases.has(base)) {
209
- prefixBases.set(base, { line: hit.line, file: file.path });
210
- }
211
- }
212
- // @RestController / @Controller class
213
- for (const hit of runQuery(parser, file.content, CONTROLLER_CLASS_QUERY, 'spring-controller-class', file.path)) {
214
- const name = hit.captures.class_name;
215
- if (name && !controllerClasses.has(name)) {
216
- controllerClasses.set(name, { line: hit.line, file: file.path });
217
- }
218
- }
219
- } catch (e) {
220
- if (e instanceof InvalidQueryError) {
221
- throw e;
222
- }
223
- continue;
224
- }
225
- }
226
- } finally {
227
- try { parser.delete(); } catch { /* ignore */ }
228
- }
229
-
230
- const conventions: Record<string, unknown> = {};
231
- const provenance: Provenance[] = [];
232
-
233
- if (routeMethods.size === 1) {
234
- const [name, { line, file }] = routeMethods.entries().next().value as [string, { line: number; file: string }];
235
- conventions.route_method = name;
236
- provenance.push({ field: 'route_method', sourceFile: file, line, query: 'spring-http-mapping' });
237
- } else if (routeMethods.size >= 2) {
238
- const [name, { line, file }] = routeMethods.entries().next().value as [string, { line: number; file: string }];
239
- conventions.route_method = name;
240
- provenance.push({ field: 'route_method', sourceFile: file, line, query: 'spring-http-mapping' });
241
- }
242
-
243
- if (prefixBases.size >= 1) {
244
- const [base, { line, file }] = prefixBases.entries().next().value as [string, { line: number; file: string }];
245
- conventions.route_prefix_base = base;
246
- provenance.push({ field: 'route_prefix_base', sourceFile: file, line, query: 'spring-request-mapping' });
247
- }
248
-
249
- if (controllerClasses.size >= 1) {
250
- const [name, { line, file }] = controllerClasses.entries().next().value as [string, { line: number; file: string }];
251
- conventions.controller_class = name;
252
- provenance.push({ field: 'controller_class', sourceFile: file, line, query: 'spring-controller-class' });
253
- }
254
-
255
- let confidence: AdapterResult['confidence'];
256
- if (Object.keys(conventions).length === 0) {
257
- confidence = 'none';
258
- } else if (routeMethods.size === 1) {
259
- confidence = 'high';
260
- } else if (routeMethods.size >= 2) {
261
- confidence = 'low';
262
- } else {
263
- confidence = 'medium';
264
- }
265
-
266
- return { conventions, provenance, confidence };
267
- },
268
- };
269
-
270
- // ============================================================
271
- // Helpers
272
- // ============================================================
273
-
274
- /**
275
- * Extract the first path segment of a Spring `@RequestMapping` template.
276
- * Mirrors aspnet/phoenix/rails/python-flask/go-chi extractors. Returns
277
- * null if input is empty or `/`-only.
278
- */
279
- function extractPrefixBase(prefix: string): string | null {
280
- const stripped = prefix.replace(/^\/+/, '');
281
- const firstSeg = stripped.split('/')[0];
282
- if (!firstSeg) return null;
283
- return '/' + firstSeg;
284
- }
1
+ // Plan 3c Phase 9b P-A-005: re-export shim. Source-of-truth lives at
2
+ // `packages/adapter-spring/src/index.ts` (workspace package). This shim
3
+ // preserves the legacy import path used by codebase-introspector + tests.
4
+ export { springAdapter } from '@massu/adapter-spring';
@@ -1,4 +1,4 @@
1
- // AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-09T20:42:44.314Z.
1
+ // AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-10T21:58:17.622Z.
2
2
  // Source pem: packages/core/security/registry-pubkey.pem
3
3
  // RAW-bytes sha256: 3b6226d036c472e533110d11a7d0cd2773ce1d7d4f1003517d5bd69c5418ed4c
4
4
  // DO NOT EDIT — regenerate via `node scripts/bundle-pubkey.mjs` or