/** * The endpoint manifest (TH-11) — crew's route table as a MACHINE-READABLE build artifact. * * # Why this exists * * routes.ts declares zero fastify schemas (verified: grep `schema:` = 0), no OpenAPI spec exists * anywhere in crew, and `wicked-crew-api-types` is types-only with no endpoint-to-type binding — * so nothing machine-readable said which methods+paths this daemon serves, what a request body is * called, or which status codes a route can answer. Request validation is NOT a vacuum (13 * `.strict()` zod objects guard the bodies), but zod objects are route-internal: a test harness, * a drift check, or an API-test generator had nothing to read. With no fastify schemas declared, * `@fastify/swagger` has nothing to generate from either — the `onRoute` hook IS the cheap path. * * # How it works * * {@link installEndpointManifestHook} registers a fastify `onRoute` hook (added by `createServer` * BEFORE any route registration, so every route the daemon serves is seen) that accumulates one * {@link EndpointEntry} per method+path. Two declaration channels feed the type/status fields: * * 1. `config.manifest` on the route options ({@link ManifestRouteConfig}) — the explicit * channel. Names should reference `wicked-crew-api-types` exports where one exists * (`LaunchRunBody`, `GateDecision`, …) so the manifest binds endpoints to the published * wire contract; a structural spelling (`'{ runId: string }'`) is the honest fallback for * wire shapes the contract has no name for. * 2. `schema.response` keys, when fastify schemas start existing (the longer-term * zod-to-json-schema path) — status codes are folded in automatically, so migrating a route * to real fastify schemas enriches the manifest without touching this module. * * Routes that declare neither still land in the manifest with `requestType`/`responseType` null * and `statusCodes: []` — the manifest records "where declared", never invents. * * # What consumes it * * - `scripts/generate-endpoint-manifest.ts` (npm run manifest:endpoints) writes the committed * `endpoint-manifest.json` at the package root — the build artifact. * - `tests/endpoint-manifest.test.ts` rebuilds the live route table and fails on ANY drift from * the committed file, so an added/removed/renamed endpoint fails CI until the manifest is * regenerated and the diff reviewed. The manifest diff IS the API regression trigger. * - `scripts/generate-api-tests.ts` derives positive + 400/404/409 negative API tests from the * committed manifest (see `tests/generated/`). * * HEAD entries are dropped: fastify v5 auto-exposes a HEAD twin for every GET * (`exposeHeadRoutes`), which would double the GET surface with rows nobody declared. */ import type { FastifyInstance } from 'fastify'; /** The explicit per-route declaration channel — `{ config: { manifest: {...} } }` on a route. */ export interface ManifestRouteConfig { /** Request-body type name — a `wicked-crew-api-types` export where one exists. */ requestType?: string; /** Success-response type name, or a structural spelling when the contract has no name for it. */ responseType?: string; /** Every status code this route answers on purpose (success + named error codes). */ statusCodes?: number[]; } declare module 'fastify' { interface FastifyContextConfig { /** TH-11 — this route's row in the committed endpoint manifest. */ manifest?: ManifestRouteConfig; } interface FastifyInstance { /** Accumulated by `createServer`'s onRoute hook; read after `app.ready()`. */ endpointManifest?: EndpointEntry[]; } } /** One method+path the daemon serves. */ export interface EndpointEntry { method: string; path: string; /** Declared request-body type name, or null — "where declared", never invented. */ requestType: string | null; /** Declared response type name, or null. */ responseType: string | null; /** Declared status codes (config.manifest + fastify schema.response keys), sorted. */ statusCodes: number[]; /** Present (true) only on websocket routes (`/ws`, `/ws/terminals/:id`). */ websocket?: boolean; } /** The committed artifact's shape (endpoint-manifest.json). */ export interface EndpointManifest { version: 1; /** * The `wicked-crew-api-types` version this route table was generated against — the published * wire contract the type names bind to. Evidence manifests that cite endpoints should carry * this (recon R8/R11: api-types drift is live; studio pinned ^0.8.0 against 0.10.0). */ apiTypesVersion: string; /** Sorted by path, then method — a stable order so drift diffs are readable. */ endpoints: EndpointEntry[]; } /** * Register the accumulating `onRoute` hook. MUST run before any route is registered — fastify * only replays `onRoute` for routes added after the hook exists. Returns the live array the hook * appends to; `createServer` exposes it as `app.endpointManifest`. */ export declare function installEndpointManifestHook(app: FastifyInstance): EndpointEntry[]; /** * The published wire contract's version. * * # Prefer the WORKSPACE-LOCAL source of truth (crew#426) * * The workspace-local `packages/crew-api-types/package.json` — found by walking up from THIS * module to the `packages/` dir that holds it — is read FIRST, and is the version this function * stamps whenever it exists. That is deliberately the version of the checkout THIS module lives in: * in a per-run worktree the module is `/packages/crew/...`, so the walk finds the * WORKTREE's own (possibly bumped) `crew-api-types/package.json`, not the parent checkout's. * * Without this, `apiTypesVersion()` resolved the version through `node_modules` (below), and a * per-run worktree is provisioned with `git worktree add` ALONE — no `node_modules` of its own — so * `require.resolve` walked UP to the PARENT clone's `node_modules/wicked-crew-api-types` symlink and * stamped the MAIN checkout's un-bumped version. The generated manifest + api tests then disagreed * with the worktree's bumped `package.json`, and (with the lockfile also stale) CI's `npm ci` broke. * Reading the workspace file directly makes codegen correct-by-construction even when node_modules is * absent. (The lockfile re-sync is the deliver preflight's job — see core/deliver.ts; this fix alone * cannot repair the lockfile.) * * # Fallback: the actually-installed dependency * * NOT `require('wicked-crew-api-types/package.json')`: that package's exports map exposes only `.` * (types-only, no runtime condition), so both the subpath and a bare resolve are refused. Walking * the resolver's candidate directories and reading the file with fs sidesteps the exports map while * still honoring the real resolution order (workspace link included). Retained for any layout where * the workspace source is not reachable from this module (e.g. a flat published install). */ export declare function apiTypesVersion(moduleUrl?: string | URL): string; /** Entries → the committed artifact: stable order, contract version stamped. */ export declare function buildEndpointManifest(entries: EndpointEntry[]): EndpointManifest; //# sourceMappingURL=endpoint-manifest.d.ts.map