/** * The route-METHOD half of the surface scan (12-23) — what `mcp:coverage` needs * on top of what `rbac:coverage` already ships. * * The WALK is imported from `@12-apps/rbac/coverage` rather than copied — the file * walk (`walkRouteFiles`), the URL mapping (`urlPathOf`) AND the export-head * parser (`exportedNamesOf`) — and that is deliberate: both gates assert a * COMPLETENESS property over the same two surfaces (`app/**` route files and * `*actions.ts` modules), and the origin host's own comment on the shared scanner says * why they must share it — "so the two gates can never disagree about what the * surface is". Two copies would agree on the day they were written and drift * silently after, in the direction of not looking. What is left here is the one * thing that genuinely differs: the GRAMMAR (see {@link exportedMethodsOf}). * * THE SCAN ROOT IS THE WHOLE `app` FOLDER, never `app/api`: a completeness gate * rooted below the surface it claims to cover does not fail when it misses * something, it simply never looks. Three OAuth/JWKS discovery routes shipped * unregistered for exactly as long as the walk was rooted at `app/api`. * * Detection is over SOURCE, with no TS compiler: fast, dependency-free, and it * matches how the framework itself keys routes off file paths plus exported names. */ /** Every method a route file can serve — the scan must see them all. */ declare const HTTP_METHODS: readonly ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]; /** One exported HTTP handler discovered on a route file. */ interface RouteMethod { /** URL path with `[param]` → `{param}` (e.g. `/api/checkout/{id}`). */ urlPath: string; /** The HTTP method exported (GET/POST/…). */ method: string; /** The route file, relative to the web root. */ file: string; } /** * Exported HTTP methods, across every form the app router serves: * `export const GET`, `export function GET`, `export async function GET`, * `export const { GET, POST } = handlers`, `export { handler as GET }`. For brace * lists the exported name is the last identifier of each item (after `as`, or * after `:` for destructuring renames). `export type { … }` never matches — a type * is never a handler. * * The two grammar knobs are the whole difference from `exportedActionsOf`, and both * are load-bearing: a route handler may be a SYNC `export function` (a server * action may not — it must be async), and only the seven HTTP methods count, where * every runtime export of a use-server module is an action. * * The shared walk is a linear hand-parse, not a regex: the `\s+`-joined patterns * this gate first shipped with backtracked polynomially on adversarial input * (CodeQL js/polynomial-redos), and a COMPLETENESS gate must stay O(n) on whatever * source it is pointed at — it is run over files a contributor supplies. */ declare function exportedMethodsOf(source: string): string[]; /** Every exported HTTP handler across all route files under `appDir`. */ declare function collectRouteMethods(appDir: string, webRoot: string): RouteMethod[]; /** * `@12-apps/mcp/coverage` — the MCP route/action coverage gate (12-23), moved out * of the origin host's `apps/web/scripts/mcp/coverage.ts` so a host's own script is a * one-line re-export and the CI workflow that shells out to the consumer's * `mcp:coverage` package script (`12-apps/ci`'s `mcp-contract.yml`) keeps working * unchanged. * * `mcp:check` only proves the REGISTRY matches the committed manifest; nothing * stops a new route file, or a new server action, from shipping outside the * agent-exposable surface. This gate closes both: * * 1. **Route coverage** — every HTTP method exported by a route file must be * registered in the host's MCP registry (or its path listed under `routes` in * the exclusions file), and every registry entry must map back to a real route * file exporting that method. A tool the manifest advertises but no route * serves is a promise an agent cannot cash. * 2. **Action coverage** — every exported server action must be mapped to a * registry operationId in the action map, or listed under `actions` in the * exclusions file with a reason. New actions fail until mapped; stale entries * fail until pruned, so neither file can rot. * * The exclusions file is the ONLY escape hatch, and keeping it a separate, * human-protected file is the point: an agent cannot silently exclude a new * route/action — it has to justify to a human why the capability is not exposed. */ /** One registry entry, as the host's MCP registry describes an endpoint. */ interface McpRegistryEndpoint { method: string; /** URL path in `{param}` form — the same shape the scan produces. */ path: string; operationId: string; } /** The protected exclusions file: every deliberate gate escape hatch. */ interface McpCoverageExclusions { /** Server actions kept off the surface, name → reason. */ actions: Record; /** Route path prefixes kept off the surface, prefix → reason. */ routes: Record; } /** The action map: server action name → registry operationId. */ interface McpActionMap { mapped: Record; } /** * A route served WITHOUT a route file — declared by an adopted package's * wiring manifest and registered wholesale from the assembled aggregate. * `{param}`-form path, the same grammar the filesystem scan yields. */ interface DeclaredRouteMethod { method: string; path: string; } interface McpCoverageOptions { /** The framework routes folder (the WHOLE `app`, never `app/api`). */ appDir: string; /** Root for relative paths in failure messages. Default: `appDir`. */ webRoot?: string; /** The host's registry entries (its `endpoints` array). */ endpoints: readonly McpRegistryEndpoint[]; /** * Routes with no file behind them, declared by wiring manifests. Each * feeds BOTH directions of the route check: it must be registered like * any scanned method, and it lets a registry entry count as served. A * declared method duplicating a scanned file's method is refused — one * URL, one source of truth. */ declaredRoutes?: readonly DeclaredRouteMethod[]; /** Path to the exclusions JSON ({@link McpCoverageExclusions}). */ exclusionsPath: string; /** * Path to the action-map JSON ({@link McpActionMap}). Omit for a host with no * server actions at all — action coverage is then vacuous rather than a crash on * a file that was never written. */ actionMapPath?: string; } interface McpCoverageResult { failures: string[]; routeMethodCount: number; actionCount: number; } /** Run the gate and return every violation (empty = green). */ declare function runMcpCoverage(options: McpCoverageOptions): McpCoverageResult; /** * The CLI face: print the verdict and exit non-zero on violations. A host's * `scripts/mcp/coverage.ts` is then one import + one call, and the CI workflow that * runs `pnpm mcp:coverage` needs no change at all. */ declare function mcpCoverageCli(options: McpCoverageOptions): void; export { type DeclaredRouteMethod, HTTP_METHODS, type McpActionMap, type McpCoverageExclusions, type McpCoverageOptions, type McpCoverageResult, type McpRegistryEndpoint, type RouteMethod, collectRouteMethods, exportedMethodsOf, mcpCoverageCli, runMcpCoverage };