@massu/core 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +9431 -5167
- package/dist/hooks/auto-learning-pipeline.js +18 -0
- package/dist/hooks/classify-failure.js +18 -0
- package/dist/hooks/cost-tracker.js +18 -0
- package/dist/hooks/fix-detector.js +18 -0
- package/dist/hooks/incident-pipeline.js +18 -0
- package/dist/hooks/post-edit-context.js +18 -0
- package/dist/hooks/post-tool-use.js +18 -0
- package/dist/hooks/pre-compact.js +18 -0
- package/dist/hooks/pre-delete-check.js +18 -0
- package/dist/hooks/quality-event.js +18 -0
- package/dist/hooks/rule-enforcement-pipeline.js +18 -0
- package/dist/hooks/session-end.js +18 -0
- package/dist/hooks/session-start.js +2952 -2740
- package/dist/hooks/user-prompt.js +18 -0
- package/docs/AUTHORING-ADAPTERS.md +207 -0
- package/docs/SECURITY.md +250 -0
- package/package.json +7 -3
- package/src/adapter.ts +90 -0
- package/src/cli.ts +7 -0
- package/src/commands/adapters.ts +824 -0
- package/src/commands/config-check-drift.ts +1 -0
- package/src/commands/config-refresh.ts +1 -0
- package/src/commands/config-upgrade.ts +1 -0
- package/src/commands/doctor.ts +2 -0
- package/src/commands/init.ts +151 -2
- package/src/config.ts +63 -0
- package/src/detect/adapters/aspnet.ts +293 -0
- package/src/detect/adapters/discover.ts +469 -0
- package/src/detect/adapters/go-chi.ts +261 -0
- package/src/detect/adapters/index.ts +49 -0
- package/src/detect/adapters/phoenix.ts +277 -0
- package/src/detect/adapters/python-flask.ts +235 -0
- package/src/detect/adapters/rails.ts +279 -0
- package/src/detect/adapters/runner.ts +32 -0
- package/src/detect/adapters/spring.ts +284 -0
- package/src/detect/adapters/tree-sitter-loader.ts +50 -0
- package/src/detect/adapters/types.ts +18 -0
- package/src/detect/framework-detector.ts +26 -0
- package/src/detect/manifest-registry.ts +261 -0
- package/src/detect/monorepo-detector.ts +1 -0
- package/src/detect/package-detector.ts +162 -62
- package/src/detect/source-dir-detector.ts +7 -0
- package/src/hooks/post-tool-use.ts +1 -0
- package/src/hooks/session-start.ts +1 -0
- package/src/lib/fileLock.ts +203 -0
- package/src/lib/installLock.ts +31 -144
- package/src/memory-file-ingest.ts +1 -0
- package/src/security/adapter-origin.ts +130 -0
- package/src/security/adapter-verifier.ts +319 -0
- package/src/security/atomic-write.ts +164 -0
- package/src/security/fetcher.ts +200 -0
- package/src/security/install-tracking.ts +319 -0
- package/src/security/local-fingerprint.ts +225 -0
- package/src/security/manifest-cache.ts +333 -0
- package/src/security/manifest-schema.ts +129 -0
- package/src/security/registry-pubkey.generated.ts +35 -0
- package/src/security/telemetry.ts +320 -0
- package/templates/aspnet/massu.config.yaml +61 -0
- package/templates/go-chi/massu.config.yaml +52 -0
- package/templates/phoenix/massu.config.yaml +54 -0
- package/templates/python-flask/massu.config.yaml +51 -0
- package/templates/rails/massu.config.yaml +56 -0
- package/templates/spring/massu.config.yaml +56 -0
|
@@ -0,0 +1,284 @@
|
|
|
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
|
+
}
|
|
@@ -161,6 +161,56 @@ export const GRAMMAR_MANIFEST: Partial<Record<TreeSitterLanguage, ManifestEntry>
|
|
|
161
161
|
sha256: '41c4fdb2249a3aa6d87eed0d383081ff09725c2248b4977043a43825980ffcc7',
|
|
162
162
|
version: '0.1.13',
|
|
163
163
|
},
|
|
164
|
+
// ----------------------------------------------------------------
|
|
165
|
+
// Plan 3c Phase 7 expansion (2026-05-07):
|
|
166
|
+
//
|
|
167
|
+
// Six additional grammars to support the registry-verified framework
|
|
168
|
+
// adapters (go-chi, rails, aspnet, spring, ktor, phoenix) plus the
|
|
169
|
+
// bundled adapters in the same language families (gin/echo/fiber,
|
|
170
|
+
// sinatra, etc.). All entries use the SAME pinned tree-sitter-wasms
|
|
171
|
+
// version (0.1.13) as the v1 four to keep the dependency surface
|
|
172
|
+
// single-source.
|
|
173
|
+
//
|
|
174
|
+
// SHA-256s computed 2026-05-07 via:
|
|
175
|
+
// curl -fsSL <url> | shasum -a 256
|
|
176
|
+
//
|
|
177
|
+
// The unpkg filename for C# uses an underscore (`c_sharp`) while the
|
|
178
|
+
// TreeSitterLanguage identifier uses no separator (`csharp`); the map
|
|
179
|
+
// key is the type identifier, the URL is the storage path — they do
|
|
180
|
+
// NOT need to match, the same as how `python` maps to `tree-sitter-
|
|
181
|
+
// python.wasm`. This is intentional and validated by the manifest
|
|
182
|
+
// shape test in tree-sitter-loader-manifest.test.ts.
|
|
183
|
+
// ----------------------------------------------------------------
|
|
184
|
+
go: {
|
|
185
|
+
url: 'https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-go.wasm',
|
|
186
|
+
sha256: '9963ca89b616eaf04b08a43bc1fb0f07b85395bec313330851f1f1ead2f755b6',
|
|
187
|
+
version: '0.1.13',
|
|
188
|
+
},
|
|
189
|
+
ruby: {
|
|
190
|
+
url: 'https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-ruby.wasm',
|
|
191
|
+
sha256: '93a5022855314cdb45458c7bb026a24a0ebc3a5ff6439e542e881f14dfa13a39',
|
|
192
|
+
version: '0.1.13',
|
|
193
|
+
},
|
|
194
|
+
csharp: {
|
|
195
|
+
url: 'https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-c_sharp.wasm',
|
|
196
|
+
sha256: '6266a7e32d68a3459104d994dc848df15d5672b0ea8e86d327274b694f8e6991',
|
|
197
|
+
version: '0.1.13',
|
|
198
|
+
},
|
|
199
|
+
java: {
|
|
200
|
+
url: 'https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-java.wasm',
|
|
201
|
+
sha256: '637aac4415fb39a211a4f4292d63c66b5ce9c32fa2cd35464af4f681d91b9a1f',
|
|
202
|
+
version: '0.1.13',
|
|
203
|
+
},
|
|
204
|
+
kotlin: {
|
|
205
|
+
url: 'https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-kotlin.wasm',
|
|
206
|
+
sha256: 'b5cb00c8d06ed0f10f1dbe497205b437809d7e87db1f638721a8cfb30e044449',
|
|
207
|
+
version: '0.1.13',
|
|
208
|
+
},
|
|
209
|
+
elixir: {
|
|
210
|
+
url: 'https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-elixir.wasm',
|
|
211
|
+
sha256: '82e91b9759ddca30d8978ebbfa8e347b4451b64c931f9ae62112e6db9b8fac20',
|
|
212
|
+
version: '0.1.13',
|
|
213
|
+
},
|
|
164
214
|
};
|
|
165
215
|
|
|
166
216
|
// ============================================================
|
|
@@ -68,6 +68,24 @@ export interface DetectionSignals {
|
|
|
68
68
|
cargoToml?: Record<string, unknown>;
|
|
69
69
|
/** Raw `go.mod` text — undefined if absent. */
|
|
70
70
|
goMod?: string;
|
|
71
|
+
/** Raw `mix.exs` text — undefined if absent. Elixir/Mix manifest. */
|
|
72
|
+
mixExs?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Raw text of the FIRST `.csproj` file found at the project root —
|
|
75
|
+
* undefined if absent. .NET project file (XML). Adapters that need to
|
|
76
|
+
* inspect more than one csproj (multi-project solutions) can re-scan from
|
|
77
|
+
* `presentFiles`.
|
|
78
|
+
*/
|
|
79
|
+
csproj?: string;
|
|
80
|
+
/** Raw `pom.xml` text — undefined if absent. Maven build manifest. */
|
|
81
|
+
pomXml?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Raw text of `build.gradle` OR `build.gradle.kts` — whichever exists at
|
|
84
|
+
* the project root, with `.kts` (Kotlin DSL) preferred when both are
|
|
85
|
+
* present (Kotlin DSL is the modern default per Gradle 7+ docs).
|
|
86
|
+
* Undefined if neither exists.
|
|
87
|
+
*/
|
|
88
|
+
gradleBuild?: string;
|
|
71
89
|
/** Set of present directory names directly under the project root (one level). */
|
|
72
90
|
presentDirs: Set<string>;
|
|
73
91
|
/** Set of present file basenames directly under the project root (one level). */
|
|
@@ -138,6 +138,13 @@ export const DETECTION_RULES: DetectionRule[] = [
|
|
|
138
138
|
{ language: 'go', kind: 'framework', keyword: 'github.com/labstack/echo', value: 'echo', priority: 10 },
|
|
139
139
|
{ language: 'go', kind: 'framework', keyword: 'github.com/gofiber/fiber', value: 'fiber', priority: 10 },
|
|
140
140
|
{ language: 'go', kind: 'framework', keyword: 'github.com/go-chi/chi', value: 'chi', priority: 9 },
|
|
141
|
+
// chi versioned import paths (Go convention: github.com/<org>/<name>/v<N>).
|
|
142
|
+
// matchRule does exact case-insensitive set lookup, so the unversioned and
|
|
143
|
+
// each major-version path each need their own rule.
|
|
144
|
+
{ language: 'go', kind: 'framework', keyword: 'github.com/go-chi/chi/v2', value: 'chi', priority: 9 },
|
|
145
|
+
{ language: 'go', kind: 'framework', keyword: 'github.com/go-chi/chi/v3', value: 'chi', priority: 9 },
|
|
146
|
+
{ language: 'go', kind: 'framework', keyword: 'github.com/go-chi/chi/v4', value: 'chi', priority: 9 },
|
|
147
|
+
{ language: 'go', kind: 'framework', keyword: 'github.com/go-chi/chi/v5', value: 'chi', priority: 9 },
|
|
141
148
|
{ language: 'go', kind: 'test_framework', keyword: 'github.com/stretchr/testify', value: 'testify', priority: 8 },
|
|
142
149
|
{ language: 'go', kind: 'orm', keyword: 'gorm.io/gorm', value: 'gorm', priority: 10 },
|
|
143
150
|
|
|
@@ -157,6 +164,25 @@ export const DETECTION_RULES: DetectionRule[] = [
|
|
|
157
164
|
{ language: 'ruby', kind: 'framework', keyword: 'sinatra', value: 'sinatra', priority: 9 },
|
|
158
165
|
{ language: 'ruby', kind: 'test_framework', keyword: 'rspec', value: 'rspec', priority: 10 },
|
|
159
166
|
{ language: 'ruby', kind: 'orm', keyword: 'activerecord', value: 'activerecord', priority: 10 },
|
|
167
|
+
// Plan 1.5.1: elixir + csharp framework rules. Closes the CR-39 gap
|
|
168
|
+
// where Phoenix + ASP.NET projects produced `framework.languages.<lang>`
|
|
169
|
+
// entries WITHOUT a `framework:` value, which prevented variant
|
|
170
|
+
// templates from being looked up.
|
|
171
|
+
{ language: 'elixir', kind: 'framework', keyword: 'phoenix', value: 'phoenix', priority: 10 },
|
|
172
|
+
{ language: 'elixir', kind: 'test_framework', keyword: 'ex_unit', value: 'ex-unit', priority: 10 },
|
|
173
|
+
{ language: 'elixir', kind: 'orm', keyword: 'ecto', value: 'ecto', priority: 10 },
|
|
174
|
+
// ASP.NET Core surfaces via several PackageReference names; the canonical
|
|
175
|
+
// ones in modern .NET projects are .App and .Mvc. matchRule does exact
|
|
176
|
+
// (case-insensitive) lookup against the deps set parseCsproj extracts.
|
|
177
|
+
{ language: 'csharp', kind: 'framework', keyword: 'Microsoft.AspNetCore.App', value: 'aspnet-core', priority: 10 },
|
|
178
|
+
{ language: 'csharp', kind: 'framework', keyword: 'Microsoft.AspNetCore.Mvc', value: 'aspnet-core', priority: 10 },
|
|
179
|
+
{ language: 'csharp', kind: 'framework', keyword: 'Microsoft.AspNetCore', value: 'aspnet-core', priority: 9 },
|
|
180
|
+
// SDK-style projects: `<Project Sdk="Microsoft.NET.Sdk.Web">` is the
|
|
181
|
+
// canonical ASP.NET Core declaration in modern .NET. parseCsproj
|
|
182
|
+
// surfaces the Sdk attribute as a dep so this rule can match.
|
|
183
|
+
{ language: 'csharp', kind: 'framework', keyword: 'Microsoft.NET.Sdk.Web', value: 'aspnet-core', priority: 10 },
|
|
184
|
+
{ language: 'csharp', kind: 'test_framework', keyword: 'xunit', value: 'xunit', priority: 10 },
|
|
185
|
+
{ language: 'csharp', kind: 'orm', keyword: 'EntityFrameworkCore', value: 'ef-core', priority: 10 },
|
|
160
186
|
];
|
|
161
187
|
|
|
162
188
|
/**
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Canonical manifest registry — Plan 1.5.1 §3 deliverable.
|
|
6
|
+
*
|
|
7
|
+
* Single source-of-truth for "what manifest files we recognize and how we
|
|
8
|
+
* read them." Both `package-detector.ts` (init's framework-detection layer)
|
|
9
|
+
* and `adapters/runner.ts:buildDetectionSignals` (AST adapter signal layer)
|
|
10
|
+
* consume from THIS registry. Adding a new manifest type = ONE entry; both
|
|
11
|
+
* consumers automatically pick it up.
|
|
12
|
+
*
|
|
13
|
+
* Pre-registry state (1.5.0) had TWO parallel lists that drifted:
|
|
14
|
+
* - `package-detector.ts:MANIFEST_FILES` — 11 entries (legacy)
|
|
15
|
+
* - `runner.ts:buildDetectionSignals` — 9 manifest reads (extended Phase 7)
|
|
16
|
+
* Phoenix + ASP.NET were unreachable via `npx massu init` because their
|
|
17
|
+
* manifest files (mix.exs, *.csproj) were in the runner's list but missing
|
|
18
|
+
* from package-detector's list. CR-39 violation; closed by this registry.
|
|
19
|
+
*
|
|
20
|
+
* Per CR-46 Rule 0 self-attest #3 ("does this add an N+1th alias map?"):
|
|
21
|
+
* this REPLACES the duplicated lists with one canonical map. The drift-
|
|
22
|
+
* prevention test `manifest-registry-drift.test.ts` fails the build if a
|
|
23
|
+
* registry entry is consumed by only one of the two layers.
|
|
24
|
+
*
|
|
25
|
+
* Plan 1.5.1 reference:
|
|
26
|
+
* `~/massu-internal/docs/plans/2026-05-08-1.5.1-init-end-to-end.md`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { PackageManifest, DetectionWarning, SupportedLanguage } from './package-detector.ts';
|
|
30
|
+
import type { DetectionSignals } from './adapters/types.ts';
|
|
31
|
+
import * as parsers from './package-detector.ts';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The pattern by which the registry recognizes a manifest file:
|
|
35
|
+
* - exact filename: `'Gemfile'`, `'mix.exs'`, `'package.json'`
|
|
36
|
+
* - extension-glob: `'*.csproj'` (matches any file ending in `.csproj`)
|
|
37
|
+
* Only these two shapes are supported; arbitrary glob patterns are
|
|
38
|
+
* intentionally rejected to keep matching cheap and predictable.
|
|
39
|
+
*/
|
|
40
|
+
export type ManifestPattern = string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Match a candidate filename against a registry pattern.
|
|
44
|
+
* Returns true if `name` matches `pattern`.
|
|
45
|
+
*/
|
|
46
|
+
export function matchManifestPattern(name: string, pattern: ManifestPattern): boolean {
|
|
47
|
+
if (pattern.startsWith('*')) {
|
|
48
|
+
const suffix = pattern.slice(1);
|
|
49
|
+
if (suffix.includes('*')) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`[manifest-registry] pattern "${pattern}" has more than one wildcard. ` +
|
|
52
|
+
`Only "*.<ext>" extension-globs are supported.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return name.endsWith(suffix);
|
|
56
|
+
}
|
|
57
|
+
return name === pattern;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parser function signature — matches the existing parse* fns in
|
|
62
|
+
* package-detector.ts. Returns null on file-not-found or parse failure
|
|
63
|
+
* (with a warning pushed to `warnings`).
|
|
64
|
+
*/
|
|
65
|
+
export type ManifestParser = (
|
|
66
|
+
path: string,
|
|
67
|
+
directory: string,
|
|
68
|
+
root: string,
|
|
69
|
+
warnings: DetectionWarning[],
|
|
70
|
+
) => PackageManifest | null;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Single registry entry. One per manifest type.
|
|
74
|
+
*/
|
|
75
|
+
export interface ManifestEntry {
|
|
76
|
+
/** Recognition pattern (see `ManifestPattern`). */
|
|
77
|
+
pattern: ManifestPattern;
|
|
78
|
+
/** Canonical manifest-type tag (matches `PackageManifest.manifestType`). */
|
|
79
|
+
manifestType: PackageManifest['manifestType'];
|
|
80
|
+
/** Default language this manifest implies. */
|
|
81
|
+
language: SupportedLanguage;
|
|
82
|
+
/** Runtime family hint. */
|
|
83
|
+
runtime: string;
|
|
84
|
+
/** Function that reads + parses the file into a `PackageManifest`. */
|
|
85
|
+
parse: ManifestParser;
|
|
86
|
+
/**
|
|
87
|
+
* The DetectionSignals key the runner uses to expose this manifest's
|
|
88
|
+
* contents to AST adapters. `null` when this manifest doesn't surface
|
|
89
|
+
* to the AST tier (e.g., requirements.txt is captured via pyprojectToml
|
|
90
|
+
* sibling already; Package.swift has no AST adapter consumer yet).
|
|
91
|
+
*/
|
|
92
|
+
signalKey: keyof DetectionSignals | null;
|
|
93
|
+
/**
|
|
94
|
+
* Shape the runner stores under `signalKey`. Drives whether the runner
|
|
95
|
+
* calls `tryReadString` (string) or `tryReadToml` (toml) or
|
|
96
|
+
* `tryReadJson` (json) when populating signals. Ignored when
|
|
97
|
+
* `signalKey === null`.
|
|
98
|
+
*/
|
|
99
|
+
signalShape: 'string' | 'toml' | 'json';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The canonical registry — the SINGLE source-of-truth for "what manifests
|
|
104
|
+
* we recognize."
|
|
105
|
+
*
|
|
106
|
+
* Lazy initializer: package-detector.ts re-exports the parsers we
|
|
107
|
+
* reference here, so eager top-level evaluation would risk an ESM
|
|
108
|
+
* circular-import undefined-symbol. Resolution: build the registry on
|
|
109
|
+
* first call (after both modules' top-level evaluations have completed).
|
|
110
|
+
*
|
|
111
|
+
* Adding a new manifest type:
|
|
112
|
+
* 1. Author a new `parseXxx()` function in `package-detector.ts`.
|
|
113
|
+
* 2. Add an entry to MANIFEST_REGISTRY referencing it.
|
|
114
|
+
* 3. (If needed) extend `SupportedLanguage` and
|
|
115
|
+
* `PackageManifest.manifestType` unions.
|
|
116
|
+
* 4. Add the new signal field to `DetectionSignals` if the AST adapter
|
|
117
|
+
* pipeline needs to read it.
|
|
118
|
+
* 5. The drift-prevention test will fail until both consumers see the
|
|
119
|
+
* new entry.
|
|
120
|
+
*/
|
|
121
|
+
let _registryCache: ManifestEntry[] | null = null;
|
|
122
|
+
|
|
123
|
+
export function getManifestRegistry(): ManifestEntry[] {
|
|
124
|
+
if (_registryCache !== null) return _registryCache;
|
|
125
|
+
_registryCache = [
|
|
126
|
+
{
|
|
127
|
+
pattern: 'package.json',
|
|
128
|
+
manifestType: 'package.json',
|
|
129
|
+
language: 'typescript',
|
|
130
|
+
runtime: 'node',
|
|
131
|
+
parse: parsers.parsePackageJson,
|
|
132
|
+
signalKey: 'packageJson',
|
|
133
|
+
signalShape: 'json',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
pattern: 'pyproject.toml',
|
|
137
|
+
manifestType: 'pyproject.toml',
|
|
138
|
+
language: 'python',
|
|
139
|
+
runtime: 'python3',
|
|
140
|
+
parse: parsers.parsePyproject,
|
|
141
|
+
signalKey: 'pyprojectToml',
|
|
142
|
+
signalShape: 'toml',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
pattern: 'requirements.txt',
|
|
146
|
+
manifestType: 'requirements.txt',
|
|
147
|
+
language: 'python',
|
|
148
|
+
runtime: 'python3',
|
|
149
|
+
parse: parsers.parseRequirementsTxt,
|
|
150
|
+
// Captured via pyprojectToml sibling already; no separate signal.
|
|
151
|
+
signalKey: null,
|
|
152
|
+
signalShape: 'string',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
pattern: 'Pipfile',
|
|
156
|
+
manifestType: 'Pipfile',
|
|
157
|
+
language: 'python',
|
|
158
|
+
runtime: 'python3',
|
|
159
|
+
parse: parsers.parsePipfile,
|
|
160
|
+
// Captured via pyprojectToml sibling already; no separate signal.
|
|
161
|
+
signalKey: null,
|
|
162
|
+
signalShape: 'string',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
pattern: 'Cargo.toml',
|
|
166
|
+
manifestType: 'Cargo.toml',
|
|
167
|
+
language: 'rust',
|
|
168
|
+
runtime: 'cargo',
|
|
169
|
+
parse: parsers.parseCargoToml,
|
|
170
|
+
signalKey: 'cargoToml',
|
|
171
|
+
signalShape: 'toml',
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
pattern: 'Package.swift',
|
|
175
|
+
manifestType: 'Package.swift',
|
|
176
|
+
language: 'swift',
|
|
177
|
+
runtime: 'xcode',
|
|
178
|
+
parse: parsers.parsePackageSwift,
|
|
179
|
+
// No AST adapter consumer yet (swift-swiftui doesn't need it).
|
|
180
|
+
signalKey: null,
|
|
181
|
+
signalShape: 'string',
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
pattern: 'go.mod',
|
|
185
|
+
manifestType: 'go.mod',
|
|
186
|
+
language: 'go',
|
|
187
|
+
runtime: 'go',
|
|
188
|
+
parse: parsers.parseGoMod,
|
|
189
|
+
signalKey: 'goMod',
|
|
190
|
+
signalShape: 'string',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
pattern: 'pom.xml',
|
|
194
|
+
manifestType: 'pom.xml',
|
|
195
|
+
language: 'java',
|
|
196
|
+
runtime: 'jvm',
|
|
197
|
+
parse: parsers.parsePomXml,
|
|
198
|
+
signalKey: 'pomXml',
|
|
199
|
+
signalShape: 'string',
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
pattern: 'build.gradle',
|
|
203
|
+
manifestType: 'build.gradle',
|
|
204
|
+
language: 'java',
|
|
205
|
+
runtime: 'jvm',
|
|
206
|
+
parse: parsers.parseBuildGradle,
|
|
207
|
+
signalKey: 'gradleBuild',
|
|
208
|
+
signalShape: 'string',
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
pattern: 'build.gradle.kts',
|
|
212
|
+
manifestType: 'build.gradle',
|
|
213
|
+
language: 'java',
|
|
214
|
+
runtime: 'jvm',
|
|
215
|
+
parse: parsers.parseBuildGradle,
|
|
216
|
+
signalKey: 'gradleBuild',
|
|
217
|
+
signalShape: 'string',
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
pattern: 'Gemfile',
|
|
221
|
+
manifestType: 'Gemfile',
|
|
222
|
+
language: 'ruby',
|
|
223
|
+
runtime: 'ruby',
|
|
224
|
+
parse: parsers.parseGemfile,
|
|
225
|
+
signalKey: 'gemfile',
|
|
226
|
+
signalShape: 'string',
|
|
227
|
+
},
|
|
228
|
+
// Plan 1.5.1 — closes CR-39 violation (1.5.0 init failed for Phoenix
|
|
229
|
+
// + ASP.NET fixtures). Both rely on AST adapters that already work
|
|
230
|
+
// in introspect; the gap was solely package-detector unaware of the
|
|
231
|
+
// manifest filenames.
|
|
232
|
+
{
|
|
233
|
+
pattern: 'mix.exs',
|
|
234
|
+
manifestType: 'mix.exs',
|
|
235
|
+
language: 'elixir',
|
|
236
|
+
runtime: 'beam',
|
|
237
|
+
parse: parsers.parseMixExs,
|
|
238
|
+
signalKey: 'mixExs',
|
|
239
|
+
signalShape: 'string',
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
pattern: '*.csproj',
|
|
243
|
+
manifestType: '*.csproj',
|
|
244
|
+
language: 'csharp',
|
|
245
|
+
runtime: 'dotnet',
|
|
246
|
+
parse: parsers.parseCsproj,
|
|
247
|
+
signalKey: 'csproj',
|
|
248
|
+
signalShape: 'string',
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
return _registryCache;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Filename list for direct iteration callers (e.g., the existing
|
|
256
|
+
* package-detector.ts `MANIFEST_FILES` const). Derived from the registry
|
|
257
|
+
* so it stays in lockstep automatically.
|
|
258
|
+
*/
|
|
259
|
+
export function getManifestPatterns(): ManifestPattern[] {
|
|
260
|
+
return getManifestRegistry().map((e) => e.pattern);
|
|
261
|
+
}
|
|
@@ -232,6 +232,7 @@ function detectPnpmWorkspaces(root: string): WorkspacePackage[] | null {
|
|
|
232
232
|
const raw = safeReadText(join(root, 'pnpm-workspace.yaml'));
|
|
233
233
|
if (!raw) return null;
|
|
234
234
|
try {
|
|
235
|
+
// pattern-scanner-allow: yaml-parse — reason: Phase 1 auto-detection parses pnpm-workspace.yaml to discover workspace packages BEFORE any massu config exists. This runs PRE-getConfig in the init flow, so getConfig() is unavailable by definition.
|
|
235
236
|
const parsed = parseYaml(raw) as { packages?: unknown } | null;
|
|
236
237
|
const list = Array.isArray(parsed?.packages)
|
|
237
238
|
? (parsed!.packages as unknown[]).filter((x) => typeof x === 'string') as string[]
|