@massu/core 1.5.8 → 1.6.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/dist/adapter.d.ts +76 -0
- package/dist/adapter.js +431 -0
- package/dist/cli.js +818 -356
- package/dist/detect/adapters/.bundle-shasums.json +12 -0
- package/dist/detect/adapters/aspnet.js +577 -0
- package/dist/detect/adapters/go-chi.js +561 -0
- package/dist/detect/adapters/parse-guard.d.ts +69 -0
- package/dist/detect/adapters/parse-guard.js +54 -0
- package/dist/detect/adapters/phoenix.js +556 -0
- package/dist/detect/adapters/query-helpers.d.ts +60 -0
- package/dist/detect/adapters/query-helpers.js +85 -0
- package/dist/detect/adapters/rails.js +567 -0
- package/dist/detect/adapters/spring.js +582 -0
- package/dist/detect/adapters/tree-sitter-loader.d.ts +102 -0
- package/dist/detect/adapters/tree-sitter-loader.js +317 -0
- package/dist/detect/adapters/types.d.ts +151 -0
- package/dist/detect/adapters/types.js +0 -0
- package/dist/hooks/session-start.js +570 -5224
- package/package.json +16 -4
- package/src/adapter.ts +31 -0
- package/src/detect/adapters/aspnet.ts +4 -293
- package/src/detect/adapters/go-chi.ts +4 -261
- package/src/detect/adapters/phoenix.ts +4 -277
- package/src/detect/adapters/rails.ts +4 -279
- package/src/detect/adapters/spring.ts +4 -284
- package/src/security/registry-pubkey.generated.ts +1 -1
|
@@ -1,284 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
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-
|
|
1
|
+
// AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-09T23:01:13.664Z.
|
|
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
|