@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.
- package/dist/adapter.d.ts +76 -0
- package/dist/adapter.js +431 -0
- package/dist/cli.js +5 -739
- 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 +552 -5211
- package/docs/AUTHORING-ADAPTERS.md +41 -0
- package/docs/SECURITY.md +39 -0
- 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
|
@@ -200,6 +200,47 @@ adapter authors to opt-in to the new shape.
|
|
|
200
200
|
Additive changes (new optional fields on result types, new
|
|
201
201
|
TreeSitterLanguage enum entries) are minor-version compatible.
|
|
202
202
|
|
|
203
|
+
## Manifest sha256 round-trip — what to do when CI fails
|
|
204
|
+
|
|
205
|
+
> Plan 3c Phase 9b P-D-004 runbook excerpt.
|
|
206
|
+
|
|
207
|
+
The `tarball-e2e` CI job runs `adapter-manifest-roundtrip.test.ts` against the
|
|
208
|
+
live registry manifest at `https://registry.massu.ai/adapters/manifest.json`.
|
|
209
|
+
The test rebuilds every workspace adapter's `dist/`, computes the sha256, and
|
|
210
|
+
asserts it matches the manifest's `sha256` entry for that `{package, version}`
|
|
211
|
+
pair.
|
|
212
|
+
|
|
213
|
+
**If the round-trip fails after a workspace adapter source edit**, the
|
|
214
|
+
manifest must be re-signed BEFORE merge. The flow:
|
|
215
|
+
|
|
216
|
+
1. **Verify your edit is intentional.** Run `npm run build` from the repo
|
|
217
|
+
root and inspect `git diff packages/adapter-<f>/dist/`. If the diff is
|
|
218
|
+
non-trivial, the source change is real and needs a manifest re-sign.
|
|
219
|
+
2. **Bump the adapter version** in `packages/adapter-<f>/package.json` (e.g.
|
|
220
|
+
`1.0.0` → `1.0.1` for a bugfix; `1.1.0` for an additive feature). Manifest
|
|
221
|
+
entries are versioned, so re-signing without a version bump would break
|
|
222
|
+
reproducibility for users on the prior version.
|
|
223
|
+
3. **Compute the new sha256** via `node packages/core/scripts/compute-adapter-shasums.mjs`
|
|
224
|
+
(or equivalent) — this writes to `~/.massu/build-shasums.json`.
|
|
225
|
+
4. **Re-sign the manifest.** Run `bash scripts/provision/registry-publish.sh
|
|
226
|
+
path/to/manifest-body.json` — reads the Ed25519 private key from macOS
|
|
227
|
+
Keychain (`massu/registry/signing/private`), produces an envelope, deploys
|
|
228
|
+
to Vercel.
|
|
229
|
+
5. **Re-run the round-trip test locally**: `MASSU_MANIFEST_ROUNDTRIP=1 npm test
|
|
230
|
+
-- adapter-manifest-roundtrip` — should now PASS.
|
|
231
|
+
6. **Commit + open PR**. The CI gate will re-verify against the freshly-deployed
|
|
232
|
+
manifest.
|
|
233
|
+
|
|
234
|
+
If CI fails on a transient registry outage (5xx, DNS, CDN cache miss), the
|
|
235
|
+
test SKIPs cleanly with a console.warn — does NOT fail the job. Re-run the
|
|
236
|
+
job to recover.
|
|
237
|
+
|
|
238
|
+
**Non-monorepo adapter authors** (third-party packages NOT under `packages/adapter-*`):
|
|
239
|
+
the round-trip test SKIPs your package automatically (workspace dir absent in
|
|
240
|
+
the monorepo). Your install-time verification chain runs against the registry
|
|
241
|
+
sha256 directly via `discover.ts:295-360` — that path catches the same drift
|
|
242
|
+
class without requiring the test.
|
|
243
|
+
|
|
203
244
|
## See also
|
|
204
245
|
|
|
205
246
|
- [`SECURITY.md`](./SECURITY.md) — signing model, key rotation, supply-chain risks
|
package/docs/SECURITY.md
CHANGED
|
@@ -240,6 +240,45 @@ per the canonical plan). The maintainer will:
|
|
|
240
240
|
5. Add the affected adapter to the manifest's `unpublished: true` list
|
|
241
241
|
if applicable, so all consumers refuse to load on next refresh.
|
|
242
242
|
|
|
243
|
+
## Migration: 1.5.x → 1.6.0 (workspace adapter publish)
|
|
244
|
+
|
|
245
|
+
> Plan 3c Phase 9b shipped 2026-05-09. See root `CHANGELOG.md` `[1.6.0]`.
|
|
246
|
+
|
|
247
|
+
`1.6.0` is **additive** — end-users on `1.5.x` are unaffected. No
|
|
248
|
+
breaking changes. No config migration. The 5 first-party AST adapters
|
|
249
|
+
(`rails`, `phoenix`, `aspnet`, `spring`, `go-chi`) continue to ship
|
|
250
|
+
CORE-BUNDLED in `@massu/core` itself; zero-config detection still works
|
|
251
|
+
out of the box.
|
|
252
|
+
|
|
253
|
+
What's new for users who want REGISTRY-VERIFIED trust:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
npm install @massu/core@^1.6.0 @massu/adapter-rails@^1.0.0
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
After install, `npx massu adapters list` will show TWO entries for
|
|
260
|
+
`rails`:
|
|
261
|
+
|
|
262
|
+
- `rails` — CORE-BUNDLED (from `@massu/core`'s bundled `dist/detect/adapters/rails.js`).
|
|
263
|
+
- `@massu/adapter-rails` — REGISTRY-VERIFIED (from `node_modules/@massu/adapter-rails/dist/`,
|
|
264
|
+
sha256-cross-checked against the signed manifest at
|
|
265
|
+
`https://registry.massu.ai/adapters/manifest.json`).
|
|
266
|
+
|
|
267
|
+
The two co-exist. Discovery prefers REGISTRY-VERIFIED when present
|
|
268
|
+
(the standalone package opts the user into the more-verified path);
|
|
269
|
+
CORE-BUNDLED remains the fallback. There is no "elevation" — they are
|
|
270
|
+
two distinct trust-class entries.
|
|
271
|
+
|
|
272
|
+
### peerDependency note
|
|
273
|
+
|
|
274
|
+
`@massu/adapter-*@1.0.0` declares `peerDependencies: { "@massu/core": "^1.6.0" }`.
|
|
275
|
+
Users pinning `@massu/core@1.5.x` who install a standalone adapter will
|
|
276
|
+
see an npm peerDep warning (non-fatal). For cleanest UX, upgrade
|
|
277
|
+
`@massu/core` to `^1.6.0` before installing standalone adapters. The
|
|
278
|
+
adapter source is binary-identical between CORE-BUNDLED and
|
|
279
|
+
REGISTRY-VERIFIED — the warning is informational, not a runtime
|
|
280
|
+
incompatibility.
|
|
281
|
+
|
|
243
282
|
## See also
|
|
244
283
|
|
|
245
284
|
- [`AUTHORING-ADAPTERS.md`](./AUTHORING-ADAPTERS.md) — how to write a
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massu/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
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",
|
|
18
|
-
"build:
|
|
19
|
-
"build:
|
|
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",
|
|
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 --external:@massu/adapter-rails --external:@massu/adapter-phoenix --external:@massu/adapter-aspnet --external:@massu/adapter-spring --external:@massu/adapter-go-chi --banner:js='#!/usr/bin/env node\nimport{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 --external:@massu/adapter-rails --external:@massu/adapter-phoenix --external:@massu/adapter-aspnet --external:@massu/adapter-spring --external:@massu/adapter-go-chi --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",
|
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';
|