@massu/core 1.5.7 → 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 +17 -5
- 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
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massu/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 72 total), 55+ workflow commands, 11 agents, 20+ patterns",
|
|
6
6
|
"main": "src/server.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/server.ts",
|
|
9
|
-
"./adapter":
|
|
9
|
+
"./adapter": {
|
|
10
|
+
"types": "./dist/adapter.d.ts",
|
|
11
|
+
"import": "./dist/adapter.js",
|
|
12
|
+
"default": "./dist/adapter.js"
|
|
13
|
+
}
|
|
10
14
|
},
|
|
11
15
|
"bin": {
|
|
12
16
|
"massu": "./dist/cli.js"
|
|
@@ -14,14 +18,22 @@
|
|
|
14
18
|
"scripts": {
|
|
15
19
|
"start": "npx tsx src/server.ts",
|
|
16
20
|
"test": "vitest run",
|
|
17
|
-
"build": "tsc --noEmit && npm run build:cli && npm run build:hooks",
|
|
21
|
+
"build": "tsc --noEmit && npm run build:cli && npm run build:hooks && npm run build:adapter-types && npm run build:adapter-subpath && npm run build:bundle-adapters",
|
|
22
|
+
"build:adapter-types": "tsc -p tsconfig.adapter-types.json",
|
|
23
|
+
"build:adapter-subpath": "tsx scripts/bundle-adapters.ts --subpath-only",
|
|
24
|
+
"build:bundle-adapters": "tsx scripts/bundle-adapters.ts",
|
|
18
25
|
"build:cli": "esbuild --bundle --platform=node --format=esm --outfile=dist/cli.js src/cli.ts --external:better-sqlite3 --external:yaml --external:zod --external:chokidar --external:proper-lockfile --external:fsevents --external:web-tree-sitter --external:tweetnacl --external:tar --external:smol-toml --external:vscode-languageserver-protocol --banner:js='#!/usr/bin/env node\nimport{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
|
|
19
|
-
"build:hooks": "esbuild --bundle --platform=node --format=esm --outdir=dist/hooks src/hooks/*.ts --external:better-sqlite3 --external:yaml --external:zod --external:chokidar --external:proper-lockfile --external:fsevents --banner:js='import{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
|
|
26
|
+
"build:hooks": "esbuild --bundle --platform=node --format=esm --outdir=dist/hooks src/hooks/*.ts --external:better-sqlite3 --external:yaml --external:zod --external:chokidar --external:proper-lockfile --external:fsevents --external:web-tree-sitter --external:tweetnacl --external:tar --external:smol-toml --external:vscode-languageserver-protocol --banner:js='import{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
|
|
20
27
|
"prepublishOnly": "bash ../../scripts/prepublish-check.sh && node ../../scripts/bundle-pubkey.mjs && npm run build",
|
|
21
28
|
"bench:watch": "tsx test/perf/watch-benchmark.ts"
|
|
22
29
|
},
|
|
23
30
|
"dependencies": {
|
|
24
31
|
"@clack/prompts": "^0.9.1",
|
|
32
|
+
"@massu/adapter-aspnet": "^1.0.0",
|
|
33
|
+
"@massu/adapter-go-chi": "^1.0.0",
|
|
34
|
+
"@massu/adapter-phoenix": "^1.0.0",
|
|
35
|
+
"@massu/adapter-rails": "^1.0.0",
|
|
36
|
+
"@massu/adapter-spring": "^1.0.0",
|
|
25
37
|
"better-sqlite3": "^12.6.2",
|
|
26
38
|
"chokidar": "^3.6.0",
|
|
27
39
|
"fast-glob": "^3.3.0",
|
|
@@ -70,7 +82,7 @@
|
|
|
70
82
|
"feature-registry"
|
|
71
83
|
],
|
|
72
84
|
"engines": {
|
|
73
|
-
"node": ">=
|
|
85
|
+
"node": ">=20.0.0 <26.0.0"
|
|
74
86
|
},
|
|
75
87
|
"repository": {
|
|
76
88
|
"type": "git",
|
package/src/adapter.ts
CHANGED
|
@@ -74,6 +74,37 @@ export {
|
|
|
74
74
|
|
|
75
75
|
import type { CodebaseAdapter } from './detect/adapters/types.js';
|
|
76
76
|
|
|
77
|
+
// ============================================================
|
|
78
|
+
// Plan 3c Phase 9b P-A-001a: runtime helper re-exports.
|
|
79
|
+
//
|
|
80
|
+
// The 5 first-party framework adapters (rails/phoenix/aspnet/spring/go-chi)
|
|
81
|
+
// migrated to `packages/adapter-<f>/src/index.ts` workspace packages need
|
|
82
|
+
// these helpers from `@massu/core/adapter` rather than reaching into
|
|
83
|
+
// `@massu/core` internals (which would couple the workspace package to
|
|
84
|
+
// every transitive .ts file).
|
|
85
|
+
//
|
|
86
|
+
// CR-46: a single SemVer-stable authoring surface. Every export from this
|
|
87
|
+
// file is part of the public adapter API; breaking changes require a
|
|
88
|
+
// major version bump per `massu-adapter-api-version`.
|
|
89
|
+
// ============================================================
|
|
90
|
+
|
|
91
|
+
// Tree-sitter query helpers (compileQuery is intentionally NOT re-exported —
|
|
92
|
+
// only `runQuery` returns the cooked record shape adapters consume).
|
|
93
|
+
export { runQuery, InvalidQueryError, type RunQueryHit } from './detect/adapters/query-helpers.js';
|
|
94
|
+
|
|
95
|
+
// Grammar acquisition (runtime: downloads + verifies SHA-256 + caches).
|
|
96
|
+
export { loadGrammar } from './detect/adapters/tree-sitter-loader.js';
|
|
97
|
+
|
|
98
|
+
// Parse-time safety guards (size cap + nested-depth check).
|
|
99
|
+
export {
|
|
100
|
+
isParsableSource,
|
|
101
|
+
MAX_AST_FILE_BYTES,
|
|
102
|
+
MAX_AST_PARSE_DEPTH,
|
|
103
|
+
MAX_AST_PARSE_MS,
|
|
104
|
+
type ParseSkip,
|
|
105
|
+
type ParseSkipReason,
|
|
106
|
+
} from './detect/adapters/parse-guard.js';
|
|
107
|
+
|
|
77
108
|
/**
|
|
78
109
|
* Identity factory — narrows the input's type to `CodebaseAdapter` so
|
|
79
110
|
* authors get compile-time checking + IDE autocomplete for missing /
|
|
@@ -1,293 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* Plan 3c — Phase 7: ASP.NET Core AST adapter.
|
|
6
|
-
*
|
|
7
|
-
* Fifth Phase 7 framework after go-chi + Flask + Rails + Phoenix. First to
|
|
8
|
-
* consume the `csharp` Tree-sitter grammar entry from GRAMMAR_MANIFEST
|
|
9
|
-
* (commit fbb8aa9). All queries verified against actual tree-sitter-c-sharp
|
|
10
|
-
* AST shape via probe (R-011) BEFORE writing the adapter.
|
|
11
|
-
*
|
|
12
|
-
* ASP.NET Core supports two routing styles, both first-class per the
|
|
13
|
-
* official routing guide (https://learn.microsoft.com/aspnet/core/fundamentals/routing):
|
|
14
|
-
* 1. **Minimal API** (recommended for new projects since .NET 6):
|
|
15
|
-
* `app.MapGet("/path", handler)`, `app.MapPost(...)`, etc. in
|
|
16
|
-
* Program.cs.
|
|
17
|
-
* 2. **Attribute routing** (MVC controllers): `[HttpGet("{id}")]`,
|
|
18
|
-
* `[HttpPost]`, `[Route("api/[controller]")]` on controller classes
|
|
19
|
-
* and methods.
|
|
20
|
-
*
|
|
21
|
-
* The adapter handles BOTH styles uniformly — extracted route_method
|
|
22
|
-
* normalizes `MapGet`/`HttpGet` → `Get`, `MapPost`/`HttpPost` → `Post`,
|
|
23
|
-
* etc. so downstream consumers don't need to know which style produced
|
|
24
|
-
* the signal.
|
|
25
|
-
*
|
|
26
|
-
* Extracts:
|
|
27
|
-
* - route_method: most-common HTTP verb captured from EITHER `app.Map<Verb>`
|
|
28
|
-
* invocations (minimal API) OR `[Http<Verb>]` attributes (MVC).
|
|
29
|
-
* - route_prefix_base: first path segment from EITHER the first `MapGet`
|
|
30
|
-
* string-literal path arg OR the first class-level `[Route("template")]`
|
|
31
|
-
* attribute. Mirrors phoenix scope_prefix_base / rails api_namespace.
|
|
32
|
-
* - controller_class: name of the first class ending in `Controller`
|
|
33
|
-
* (attribute-routing style only — minimal API has no controllers).
|
|
34
|
-
* Mirrors python-flask app_factory / phoenix router_module.
|
|
35
|
-
*
|
|
36
|
-
* Confidence rules (mirror phoenix / rails / go-chi):
|
|
37
|
-
* - 'high' if exactly ONE distinct route_method seen.
|
|
38
|
-
* - 'low' if multiple distinct route_methods seen.
|
|
39
|
-
* - 'medium' if route_prefix_base or controller_class found but no
|
|
40
|
-
* route_method.
|
|
41
|
-
* - 'none' if no ASP.NET signals at all (regex fallback takes over).
|
|
42
|
-
*
|
|
43
|
-
* Tree-sitter-c-sharp AST shape (verified via probe 2026-05-07):
|
|
44
|
-
* - Method calls: `(invocation_expression function: (member_access_expression
|
|
45
|
-
* name: (identifier)) arguments: (argument_list (argument
|
|
46
|
-
* (string_literal)) ...))`.
|
|
47
|
-
* - Attributes: `(attribute name: (identifier) (attribute_argument_list
|
|
48
|
-
* (attribute_argument (string_literal))?))` — argument list is optional
|
|
49
|
-
* because attributes like `[HttpPost]` have no args.
|
|
50
|
-
* - Class declarations: `(class_declaration name: (identifier)
|
|
51
|
-
* bases: (base_list (identifier)?))` — bases optional.
|
|
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 (C# grammar)
|
|
65
|
-
// ============================================================
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Minimal-API verb mapping: `app.MapGet("/path", handler)`,
|
|
69
|
-
* `app.MapPost(...)`, etc. Anchored on the first argument being a
|
|
70
|
-
* string_literal so we capture the route path together with the verb.
|
|
71
|
-
*
|
|
72
|
-
* The captured @method is the full method name like `MapGet` — the
|
|
73
|
-
* adapter strips the `Map` prefix in post-processing.
|
|
74
|
-
*
|
|
75
|
-
* Per ASP.NET Core minimal API docs:
|
|
76
|
-
* https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis
|
|
77
|
-
*/
|
|
78
|
-
const MAP_VERB_QUERY = `
|
|
79
|
-
(invocation_expression
|
|
80
|
-
function: (member_access_expression
|
|
81
|
-
name: (identifier) @method (#match? @method "^Map(Get|Post|Put|Patch|Delete|Head|Options)$"))
|
|
82
|
-
arguments: (argument_list
|
|
83
|
-
.
|
|
84
|
-
(argument (string_literal) @route_path)))
|
|
85
|
-
`;
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Attribute-routing HTTP verb attributes: `[HttpGet]`, `[HttpGet("{id}")]`,
|
|
89
|
-
* `[HttpPost]`, etc. Captures BOTH the parameterless and parameterized
|
|
90
|
-
* forms — the route path may or may not be present.
|
|
91
|
-
*
|
|
92
|
-
* The captured @attr_name is `HttpGet` etc. — the adapter strips the
|
|
93
|
-
* `Http` prefix in post-processing.
|
|
94
|
-
*
|
|
95
|
-
* Per ASP.NET Core attribute routing docs:
|
|
96
|
-
* https://learn.microsoft.com/aspnet/core/mvc/controllers/routing
|
|
97
|
-
*/
|
|
98
|
-
const HTTP_ATTR_QUERY = `
|
|
99
|
-
(attribute
|
|
100
|
-
name: (identifier) @attr_name (#match? @attr_name "^Http(Get|Post|Put|Patch|Delete|Head|Options)$"))
|
|
101
|
-
`;
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Class-level `[Route("api/[controller]")]` attribute. Captures the
|
|
105
|
-
* route template string so we can extract its first path segment.
|
|
106
|
-
* Tokens like `[controller]` inside the template are kept verbatim —
|
|
107
|
-
* the prefix-base extractor splits on `/` so `api/[controller]` → `/api`.
|
|
108
|
-
*/
|
|
109
|
-
const ROUTE_ATTR_QUERY = `
|
|
110
|
-
(attribute
|
|
111
|
-
name: (identifier) @_attr_name (#eq? @_attr_name "Route")
|
|
112
|
-
(attribute_argument_list
|
|
113
|
-
(attribute_argument (string_literal) @route_template)))
|
|
114
|
-
`;
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Controller class declaration: `class FooController : ControllerBase`.
|
|
118
|
-
* We restrict to class names ending in `Controller` to avoid every class
|
|
119
|
-
* in the project (canonical ASP.NET MVC naming convention).
|
|
120
|
-
*/
|
|
121
|
-
const CONTROLLER_CLASS_QUERY = `
|
|
122
|
-
(class_declaration
|
|
123
|
-
name: (identifier) @class_name (#match? @class_name "Controller$"))
|
|
124
|
-
`;
|
|
125
|
-
|
|
126
|
-
// ============================================================
|
|
127
|
-
// Adapter
|
|
128
|
-
// ============================================================
|
|
129
|
-
|
|
130
|
-
export const aspnetAdapter: CodebaseAdapter = {
|
|
131
|
-
id: 'aspnet',
|
|
132
|
-
languages: ['csharp'],
|
|
133
|
-
|
|
134
|
-
matches(signals: DetectionSignals): boolean {
|
|
135
|
-
// Cheap signal-only check. No file IO. The canonical ASP.NET Core
|
|
136
|
-
// declaration is `<Project Sdk="Microsoft.NET.Sdk.Web">` in the .csproj
|
|
137
|
-
// file (per https://learn.microsoft.com/aspnet/core/fundamentals/target-aspnetcore).
|
|
138
|
-
// Fallback: presence of `Microsoft.AspNetCore.App` framework reference,
|
|
139
|
-
// which appears in older .csproj formats.
|
|
140
|
-
if (!signals.csproj) return false;
|
|
141
|
-
if (/Sdk\s*=\s*["']Microsoft\.NET\.Sdk\.Web["']/i.test(signals.csproj)) return true;
|
|
142
|
-
if (/Microsoft\.AspNetCore\.App/i.test(signals.csproj)) return true;
|
|
143
|
-
return false;
|
|
144
|
-
},
|
|
145
|
-
|
|
146
|
-
async introspect(files: SourceFile[], _rootDir: string): Promise<AdapterResult> {
|
|
147
|
-
if (files.length === 0) {
|
|
148
|
-
return { conventions: {}, provenance: [], confidence: 'none' };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
let language;
|
|
152
|
-
try {
|
|
153
|
-
language = await loadGrammar('csharp');
|
|
154
|
-
} catch (e) {
|
|
155
|
-
// Grammar unavailable → adapter returns 'none' so regex fallback takes over.
|
|
156
|
-
return { conventions: {}, provenance: [], confidence: 'none' };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const parser = new Parser();
|
|
160
|
-
parser.setLanguage(language);
|
|
161
|
-
|
|
162
|
-
const routeMethods = new Map<string, { line: number; file: string }>();
|
|
163
|
-
const prefixBases = new Map<string, { line: number; file: string }>();
|
|
164
|
-
const controllerClasses = new Map<string, { line: number; file: string }>();
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
for (const file of files) {
|
|
168
|
-
const skip = isParsableSource(file.content, file.size);
|
|
169
|
-
if (skip) {
|
|
170
|
-
process.stderr.write(
|
|
171
|
-
`[massu/ast] WARN: aspnet skipping ${file.path}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES}. (Phase 3.5 mitigation)\n`,
|
|
172
|
-
);
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
try {
|
|
176
|
-
// Minimal API: app.MapGet("/path", ...)
|
|
177
|
-
for (const hit of runQuery(parser, file.content, MAP_VERB_QUERY, 'aspnet-map-verb', file.path)) {
|
|
178
|
-
const methodRaw = hit.captures.method;
|
|
179
|
-
if (!methodRaw) continue;
|
|
180
|
-
const verb = methodRaw.replace(/^Map/, ''); // MapGet → Get
|
|
181
|
-
if (!routeMethods.has(verb)) {
|
|
182
|
-
routeMethods.set(verb, { line: hit.line, file: file.path });
|
|
183
|
-
}
|
|
184
|
-
// Also capture the route path for prefix base
|
|
185
|
-
const pathRaw = hit.captures.route_path;
|
|
186
|
-
if (pathRaw) {
|
|
187
|
-
const literal = pathRaw.replace(/^["']/, '').replace(/["']$/, '');
|
|
188
|
-
const base = extractPrefixBase(literal);
|
|
189
|
-
if (base && !prefixBases.has(base)) {
|
|
190
|
-
prefixBases.set(base, { line: hit.line, file: file.path });
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
// Attribute routing: [HttpGet], [HttpPost], etc.
|
|
195
|
-
for (const hit of runQuery(parser, file.content, HTTP_ATTR_QUERY, 'aspnet-http-attr', file.path)) {
|
|
196
|
-
const attrRaw = hit.captures.attr_name;
|
|
197
|
-
if (!attrRaw) continue;
|
|
198
|
-
const verb = attrRaw.replace(/^Http/, ''); // HttpGet → Get
|
|
199
|
-
if (!routeMethods.has(verb)) {
|
|
200
|
-
routeMethods.set(verb, { line: hit.line, file: file.path });
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
// Class-level [Route("api/[controller]")]
|
|
204
|
-
for (const hit of runQuery(parser, file.content, ROUTE_ATTR_QUERY, 'aspnet-route-attr', file.path)) {
|
|
205
|
-
const tplRaw = hit.captures.route_template;
|
|
206
|
-
if (!tplRaw) continue;
|
|
207
|
-
const literal = tplRaw.replace(/^["']/, '').replace(/["']$/, '');
|
|
208
|
-
const base = extractPrefixBase(literal);
|
|
209
|
-
if (base && !prefixBases.has(base)) {
|
|
210
|
-
prefixBases.set(base, { line: hit.line, file: file.path });
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
// Controller class: class FooController : ControllerBase
|
|
214
|
-
for (const hit of runQuery(parser, file.content, CONTROLLER_CLASS_QUERY, 'aspnet-controller-class', file.path)) {
|
|
215
|
-
const name = hit.captures.class_name;
|
|
216
|
-
if (name && !controllerClasses.has(name)) {
|
|
217
|
-
controllerClasses.set(name, { line: hit.line, file: file.path });
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
} catch (e) {
|
|
221
|
-
if (e instanceof InvalidQueryError) {
|
|
222
|
-
throw e;
|
|
223
|
-
}
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
} finally {
|
|
228
|
-
try { parser.delete(); } catch { /* ignore */ }
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const conventions: Record<string, unknown> = {};
|
|
232
|
-
const provenance: Provenance[] = [];
|
|
233
|
-
|
|
234
|
-
if (routeMethods.size === 1) {
|
|
235
|
-
const [name, { line, file }] = routeMethods.entries().next().value as [string, { line: number; file: string }];
|
|
236
|
-
conventions.route_method = name;
|
|
237
|
-
provenance.push({ field: 'route_method', sourceFile: file, line, query: 'aspnet-map-verb' });
|
|
238
|
-
} else if (routeMethods.size >= 2) {
|
|
239
|
-
const [name, { line, file }] = routeMethods.entries().next().value as [string, { line: number; file: string }];
|
|
240
|
-
conventions.route_method = name;
|
|
241
|
-
provenance.push({ field: 'route_method', sourceFile: file, line, query: 'aspnet-map-verb' });
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (prefixBases.size >= 1) {
|
|
245
|
-
const [base, { line, file }] = prefixBases.entries().next().value as [string, { line: number; file: string }];
|
|
246
|
-
conventions.route_prefix_base = base;
|
|
247
|
-
provenance.push({ field: 'route_prefix_base', sourceFile: file, line, query: 'aspnet-route-prefix' });
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (controllerClasses.size >= 1) {
|
|
251
|
-
const [name, { line, file }] = controllerClasses.entries().next().value as [string, { line: number; file: string }];
|
|
252
|
-
conventions.controller_class = name;
|
|
253
|
-
provenance.push({ field: 'controller_class', sourceFile: file, line, query: 'aspnet-controller-class' });
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
let confidence: AdapterResult['confidence'];
|
|
257
|
-
if (Object.keys(conventions).length === 0) {
|
|
258
|
-
confidence = 'none';
|
|
259
|
-
} else if (routeMethods.size === 1) {
|
|
260
|
-
confidence = 'high';
|
|
261
|
-
} else if (routeMethods.size >= 2) {
|
|
262
|
-
confidence = 'low';
|
|
263
|
-
} else {
|
|
264
|
-
confidence = 'medium';
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return { conventions, provenance, confidence };
|
|
268
|
-
},
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
// ============================================================
|
|
272
|
-
// Helpers
|
|
273
|
-
// ============================================================
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Extract the first path segment of an ASP.NET route template. Mirrors
|
|
277
|
-
* phoenix/rails/python-flask/go-chi prefix-base extractors. Returns null
|
|
278
|
-
* if input is empty or `/`-only.
|
|
279
|
-
*
|
|
280
|
-
* Examples (verified against test fixtures):
|
|
281
|
-
* "/health" → "/health"
|
|
282
|
-
* "api/[controller]" → "/api"
|
|
283
|
-
* "/api/v1/users" → "/api"
|
|
284
|
-
* "/" → null
|
|
285
|
-
* "" → null
|
|
286
|
-
*/
|
|
287
|
-
function extractPrefixBase(prefix: string): string | null {
|
|
288
|
-
// ASP.NET route templates may or may not begin with `/`. Normalize.
|
|
289
|
-
const stripped = prefix.replace(/^\/+/, '');
|
|
290
|
-
const firstSeg = stripped.split('/')[0];
|
|
291
|
-
if (!firstSeg) return null;
|
|
292
|
-
return '/' + firstSeg;
|
|
293
|
-
}
|
|
1
|
+
// Plan 3c Phase 9b P-A-005: re-export shim. Source-of-truth lives at
|
|
2
|
+
// `packages/adapter-aspnet/src/index.ts` (workspace package). This shim
|
|
3
|
+
// preserves the legacy import path used by codebase-introspector + tests.
|
|
4
|
+
export { aspnetAdapter } from '@massu/adapter-aspnet';
|
|
@@ -1,261 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* Plan 3c — Phase 7: go-chi AST adapter.
|
|
6
|
-
*
|
|
7
|
-
* First Phase 7 framework that introduces a NEW Tree-sitter grammar
|
|
8
|
-
* (`go`) — added to GRAMMAR_MANIFEST in the preceding Commit 1
|
|
9
|
-
* (plan-3c-phase7-grammar-infra). End-to-end proof that the grammar
|
|
10
|
-
* load + parse + query path works for non-Python/JS/Swift languages.
|
|
11
|
-
*
|
|
12
|
-
* Establishes the per-framework deliverable pattern (4 artifacts) for
|
|
13
|
-
* the remaining registry-verified frameworks (rails / aspnet / spring /
|
|
14
|
-
* phoenix) and the bundled Go adapters (gin / echo / fiber / net-http):
|
|
15
|
-
* 1. packages/core/templates/go-chi/massu.config.yaml (variant template)
|
|
16
|
-
* 2. packages/core/src/detect/adapters/go-chi.ts (this file — AST adapter)
|
|
17
|
-
* 3. Adversarial fixtures (inline in the test file via mkdirSync+writeFileSync)
|
|
18
|
-
* 4. packages/core/src/__tests__/go-chi.test.ts (golden-output test)
|
|
19
|
-
*
|
|
20
|
-
* Extracts:
|
|
21
|
-
* - route_method: most-common HTTP method registered via `r.<Method>(...)`
|
|
22
|
-
* (one of Get/Post/Put/Delete/Patch/Head/Options/Connect/Trace).
|
|
23
|
-
* Useful for scaffold-router templates that need to know which verb
|
|
24
|
-
* style the project uses.
|
|
25
|
-
* - mount_prefix_base: first path segment of `r.Mount("/api/...", ...)`
|
|
26
|
-
* mirroring python-fastapi's APIRouter prefix extraction. The chi
|
|
27
|
-
* `Mount` call is the canonical way to attach a sub-router under
|
|
28
|
-
* a path prefix (per chi docs: https://go-chi.io/#/pages/routing).
|
|
29
|
-
* - middleware_name: first chi middleware registered via
|
|
30
|
-
* `r.Use(middleware.<Name>)` (e.g., "Logger", "Recoverer", "RequestID").
|
|
31
|
-
* Captured from the canonical chi `middleware.*` package qualifier.
|
|
32
|
-
*
|
|
33
|
-
* Confidence rules (mirror python-fastapi/python-flask):
|
|
34
|
-
* - 'high' if exactly ONE distinct route_method seen (clear convention).
|
|
35
|
-
* - 'medium' if mount_prefix_base or middleware_name found but no route_method.
|
|
36
|
-
* - 'low' if multiple distinct route_methods seen (mixed convention).
|
|
37
|
-
* - 'none' if no chi signals at all (regex fallback takes over).
|
|
38
|
-
*
|
|
39
|
-
* Does NOT use regex on file content — only Tree-sitter S-expression queries
|
|
40
|
-
* compiled via query-helpers.ts. Regex would be the regex-fallback path.
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
import { Parser } from 'web-tree-sitter';
|
|
44
|
-
import type { CodebaseAdapter, AdapterResult, DetectionSignals, Provenance, SourceFile } from './types.ts';
|
|
45
|
-
import { runQuery, InvalidQueryError } from './query-helpers.ts';
|
|
46
|
-
import { loadGrammar } from './tree-sitter-loader.ts';
|
|
47
|
-
import { isParsableSource, MAX_AST_FILE_BYTES } from './parse-guard.ts';
|
|
48
|
-
|
|
49
|
-
// ============================================================
|
|
50
|
-
// Tree-sitter S-expression queries (Go grammar)
|
|
51
|
-
// ============================================================
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* HTTP method route registration: matches `r.Get("/path", handler)`,
|
|
55
|
-
* `r.Post(...)`, etc. Anchored on the chi-canonical method names; we
|
|
56
|
-
* deliberately do NOT match arbitrary `r.<Anything>(...)` calls because
|
|
57
|
-
* Go's selector_expression shape is shared by every method call.
|
|
58
|
-
*
|
|
59
|
-
* The Tree-sitter Go grammar represents `r.Get(...)` as:
|
|
60
|
-
* call_expression {
|
|
61
|
-
* function: selector_expression {
|
|
62
|
-
* operand: identifier (e.g. "r")
|
|
63
|
-
* field: field_identifier (e.g. "Get")
|
|
64
|
-
* }
|
|
65
|
-
* arguments: argument_list { ... }
|
|
66
|
-
* }
|
|
67
|
-
*
|
|
68
|
-
* The #match? predicate constrains @method to chi's HTTP verb set.
|
|
69
|
-
*/
|
|
70
|
-
const ROUTE_METHOD_QUERY = `
|
|
71
|
-
(call_expression
|
|
72
|
-
function: (selector_expression
|
|
73
|
-
field: (field_identifier) @method (#match? @method "^(Get|Post|Put|Delete|Patch|Head|Options|Connect|Trace)$"))
|
|
74
|
-
arguments: (argument_list
|
|
75
|
-
.
|
|
76
|
-
(interpreted_string_literal) @route_path))
|
|
77
|
-
`;
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Subrouter mount: `r.Mount("/api/v1", apiRouter)`. Captures the path
|
|
81
|
-
* literal so we can split off the base segment, mirroring python-fastapi's
|
|
82
|
-
* APIRouter prefix extraction.
|
|
83
|
-
*
|
|
84
|
-
* Per chi docs (https://go-chi.io/#/pages/routing#mounting):
|
|
85
|
-
* "Mount attaches another http.Handler along ./pattern/*"
|
|
86
|
-
*/
|
|
87
|
-
const MOUNT_PREFIX_QUERY = `
|
|
88
|
-
(call_expression
|
|
89
|
-
function: (selector_expression
|
|
90
|
-
field: (field_identifier) @_field (#eq? @_field "Mount"))
|
|
91
|
-
arguments: (argument_list
|
|
92
|
-
.
|
|
93
|
-
(interpreted_string_literal) @mount_path))
|
|
94
|
-
`;
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* chi middleware registration: `r.Use(middleware.Logger)`,
|
|
98
|
-
* `r.Use(middleware.Recoverer)`, etc. We require the canonical
|
|
99
|
-
* `middleware.<Name>` qualifier to avoid matching user-defined
|
|
100
|
-
* middleware (which we couldn't classify by name alone).
|
|
101
|
-
*
|
|
102
|
-
* The selector_expression inside the argument captures the package
|
|
103
|
-
* qualifier (`middleware`) and the function name (`Logger`/etc).
|
|
104
|
-
*/
|
|
105
|
-
const MIDDLEWARE_USE_QUERY = `
|
|
106
|
-
(call_expression
|
|
107
|
-
function: (selector_expression
|
|
108
|
-
field: (field_identifier) @_use (#eq? @_use "Use"))
|
|
109
|
-
arguments: (argument_list
|
|
110
|
-
.
|
|
111
|
-
(selector_expression
|
|
112
|
-
operand: (identifier) @_pkg (#eq? @_pkg "middleware")
|
|
113
|
-
field: (field_identifier) @middleware_name)))
|
|
114
|
-
`;
|
|
115
|
-
|
|
116
|
-
// ============================================================
|
|
117
|
-
// Adapter
|
|
118
|
-
// ============================================================
|
|
119
|
-
|
|
120
|
-
export const goChiAdapter: CodebaseAdapter = {
|
|
121
|
-
id: 'go-chi',
|
|
122
|
-
languages: ['go'],
|
|
123
|
-
|
|
124
|
-
matches(signals: DetectionSignals): boolean {
|
|
125
|
-
// Cheap signal-only check. No file IO. Match if:
|
|
126
|
-
// 1. go.mod text mentions github.com/go-chi/chi (canonical require), OR
|
|
127
|
-
// 2. project has cmd/ + internal/ AND go.mod is present (Go layout) — but
|
|
128
|
-
// only when go.mod ALSO mentions chi (avoids matching every Go project).
|
|
129
|
-
if (!signals.goMod) return false;
|
|
130
|
-
if (/github\.com\/go-chi\/chi/i.test(signals.goMod)) return true;
|
|
131
|
-
return false;
|
|
132
|
-
},
|
|
133
|
-
|
|
134
|
-
async introspect(files: SourceFile[], _rootDir: string): Promise<AdapterResult> {
|
|
135
|
-
if (files.length === 0) {
|
|
136
|
-
return { conventions: {}, provenance: [], confidence: 'none' };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
let language;
|
|
140
|
-
try {
|
|
141
|
-
language = await loadGrammar('go');
|
|
142
|
-
} catch (e) {
|
|
143
|
-
// Grammar unavailable → adapter returns 'none' so regex fallback takes over.
|
|
144
|
-
return { conventions: {}, provenance: [], confidence: 'none' };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const parser = new Parser();
|
|
148
|
-
parser.setLanguage(language);
|
|
149
|
-
|
|
150
|
-
const routeMethods = new Map<string, { line: number; file: string }>();
|
|
151
|
-
const mountBases = new Map<string, { line: number; file: string }>();
|
|
152
|
-
const middlewareNames = new Map<string, { line: number; file: string }>();
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
for (const file of files) {
|
|
156
|
-
// Phase 3.5 defense-in-depth size + depth gate at adapter tier.
|
|
157
|
-
const skip = isParsableSource(file.content, file.size);
|
|
158
|
-
if (skip) {
|
|
159
|
-
process.stderr.write(
|
|
160
|
-
`[massu/ast] WARN: go-chi skipping ${file.path}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES}. (Phase 3.5 mitigation)\n`,
|
|
161
|
-
);
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
try {
|
|
165
|
-
for (const hit of runQuery(parser, file.content, ROUTE_METHOD_QUERY, 'chi-route-method', file.path)) {
|
|
166
|
-
const method = hit.captures.method;
|
|
167
|
-
if (method && !routeMethods.has(method)) {
|
|
168
|
-
routeMethods.set(method, { line: hit.line, file: file.path });
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
for (const hit of runQuery(parser, file.content, MOUNT_PREFIX_QUERY, 'chi-mount-prefix', file.path)) {
|
|
172
|
-
const raw = hit.captures.mount_path;
|
|
173
|
-
if (!raw) continue;
|
|
174
|
-
const literal = raw.replace(/^["`]/, '').replace(/["`]$/, '');
|
|
175
|
-
const base = extractPrefixBase(literal);
|
|
176
|
-
if (base && !mountBases.has(base)) {
|
|
177
|
-
mountBases.set(base, { line: hit.line, file: file.path });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
for (const hit of runQuery(parser, file.content, MIDDLEWARE_USE_QUERY, 'chi-middleware-use', file.path)) {
|
|
181
|
-
const name = hit.captures.middleware_name;
|
|
182
|
-
if (name && !middlewareNames.has(name)) {
|
|
183
|
-
middlewareNames.set(name, { line: hit.line, file: file.path });
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
} catch (e) {
|
|
187
|
-
if (e instanceof InvalidQueryError) {
|
|
188
|
-
// Compile-time failure of OUR query is a developer bug — surface it.
|
|
189
|
-
throw e;
|
|
190
|
-
}
|
|
191
|
-
// Per-file parse error: skip + keep going.
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
} finally {
|
|
196
|
-
try { parser.delete(); } catch { /* ignore */ }
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const conventions: Record<string, unknown> = {};
|
|
200
|
-
const provenance: Provenance[] = [];
|
|
201
|
-
|
|
202
|
-
if (routeMethods.size === 1) {
|
|
203
|
-
const [name, { line, file }] = routeMethods.entries().next().value as [string, { line: number; file: string }];
|
|
204
|
-
conventions.route_method = name;
|
|
205
|
-
provenance.push({ field: 'route_method', sourceFile: file, line, query: 'chi-route-method' });
|
|
206
|
-
} else if (routeMethods.size >= 2) {
|
|
207
|
-
// Mixed convention — emit first-seen for visibility.
|
|
208
|
-
const [name, { line, file }] = routeMethods.entries().next().value as [string, { line: number; file: string }];
|
|
209
|
-
conventions.route_method = name;
|
|
210
|
-
provenance.push({ field: 'route_method', sourceFile: file, line, query: 'chi-route-method' });
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (mountBases.size >= 1) {
|
|
214
|
-
const [base, { line, file }] = mountBases.entries().next().value as [string, { line: number; file: string }];
|
|
215
|
-
conventions.mount_prefix_base = base;
|
|
216
|
-
provenance.push({ field: 'mount_prefix_base', sourceFile: file, line, query: 'chi-mount-prefix' });
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (middlewareNames.size >= 1) {
|
|
220
|
-
const [name, { line, file }] = middlewareNames.entries().next().value as [string, { line: number; file: string }];
|
|
221
|
-
conventions.middleware_name = name;
|
|
222
|
-
provenance.push({ field: 'middleware_name', sourceFile: file, line, query: 'chi-middleware-use' });
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
let confidence: AdapterResult['confidence'];
|
|
226
|
-
if (Object.keys(conventions).length === 0) {
|
|
227
|
-
confidence = 'none';
|
|
228
|
-
} else if (routeMethods.size === 1) {
|
|
229
|
-
confidence = 'high';
|
|
230
|
-
} else if (routeMethods.size >= 2) {
|
|
231
|
-
confidence = 'low';
|
|
232
|
-
} else {
|
|
233
|
-
confidence = 'medium';
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return { conventions, provenance, confidence };
|
|
237
|
-
},
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
// ============================================================
|
|
241
|
-
// Helpers
|
|
242
|
-
// ============================================================
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Extract the first path segment of a chi mount path. Mirrors
|
|
246
|
-
* python-fastapi/python-flask's extractPrefixBase. Returns null if input
|
|
247
|
-
* doesn't start with `/`.
|
|
248
|
-
*
|
|
249
|
-
* NOTE: future refactor opportunity once a third adapter needs identical
|
|
250
|
-
* logic — extracting a shared helper module. The Phase 7 Flask commit
|
|
251
|
-
* deliberately copied this from python-fastapi rather than premature
|
|
252
|
-
* extraction (per its self-attest #3); the same reasoning applies here
|
|
253
|
-
* until a fourth consumer appears.
|
|
254
|
-
*/
|
|
255
|
-
function extractPrefixBase(prefix: string): string | null {
|
|
256
|
-
if (!prefix.startsWith('/')) return null;
|
|
257
|
-
const stripped = prefix.replace(/^\/+/, '');
|
|
258
|
-
const firstSeg = stripped.split('/')[0];
|
|
259
|
-
if (!firstSeg) return null;
|
|
260
|
-
return '/' + firstSeg;
|
|
261
|
-
}
|
|
1
|
+
// Plan 3c Phase 9b P-A-005: re-export shim. Source-of-truth lives at
|
|
2
|
+
// `packages/adapter-go-chi/src/index.ts` (workspace package). This shim
|
|
3
|
+
// preserves the legacy import path used by codebase-introspector + tests.
|
|
4
|
+
export { goChiAdapter } from '@massu/adapter-go-chi';
|