@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.
@@ -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.5.8",
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": "./src/adapter.ts"
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: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);'",
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
- // 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: 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';