/** * Cross-projection route + envelope + name contract. * * Three server/client generators independently derive URL layout, response * envelope keys, and validation-schema names — and they drift (the 2026-07-01 * reconciliation audit, "No single source of truth for cross-projection * contracts"). This module is that single source: pure, deterministic helpers * plus a normalized `RouteContract` object that every projection resolves from * the SAME options, so the routes a client calls always exist and the fields it * reads always match what the server returns. * * Everything here is a pure string transform — identical input always yields * identical output (house-style determinism invariant). No framework imports, * so nextjs / react-query / routes / zod / hono / express can all consume it * without a dependency cycle. */ import { type RouteCasing } from './naming.js'; /** * How a dynamic path segment is rendered in a URL *template*: * - `'nextjs'` → `[id]` (App Router file-convention + the URL it serves) * - `'colon'` → `:id` (Express / Hono / most Node routers) * - `'literal'` → `id` (bare name; for callers that interpolate a value) */ export type ParamStyle = 'nextjs' | 'colon' | 'literal'; /** Options that determine an entity's URL/route path segment. */ export interface EntitySegmentOptions { /** Explicit per-entity segment overrides (used verbatim, may contain `/`). */ routeSegments?: Record; /** Casing applied to the entity name when no override is present. Default `'lowercase'`. */ routeCasing?: RouteCasing; /** * Optional IR module name per entity. When set (and no `routeSegments` override), * the sanitized module is prepended: `billing/order`. Explicit overrides win * unchanged so multi-segment `routeSegments` stay authoritative. */ entityModules?: Record; } /** * URL/route path segment for an entity: explicit `routeSegments` override → * else optional `entityModules` prefix + name normalized per `routeCasing` * (default `'lowercase'`, the legacy flattened form `PrepTask` → `preptask`). * * This is the ONE segment derivation. The nextjs generator's `resolveRouteSegment` * and the client both call it, so route pathHints and client fetch URLs cannot * disagree on casing or module nesting. */ export declare function resolveEntitySegment(entityName: string, opts?: EntitySegmentOptions): string; /** * Envelope key holding the array in a list-read response. The historical, naive * derivation `${camelEntity}s` (e.g. `Recipe` → `recipes`, `OrderLine` → * `orderLines`). Kept as the documented default — server read routes and every * client share THIS helper, so the key they write and the key they read are the * same string by construction. Irregular plurals (`Dish` → `dishs`) are wrong by * design here; overrides live per-projection (react-query `readEnvelope`). */ export declare function listEnvelopeKey(entityName: string): string; /** Envelope key holding the object in a detail-read response, e.g. `Recipe` → `recipe`. */ export declare function detailEnvelopeKey(entityName: string): string; /** * Canonical Zod command-params schema export name: `${Entity}${Command}ParamsSchema` * (e.g. `Recipe` + `create` → `RecipeCreateParamsSchema`). Capitalize-first only * (NOT word-splitting) to match hono/express/convex.react import derivation. * Prefer the bundled `zod.schemas` module (`schemas/manifest-schemas.ts`) over * per-command microfiles (`zod.command`), which collide on shared command names. */ export declare function zodParamsSchemaName(entityName: string, commandName: string): string; /** * URL base path for reads/detail, derived from the App Router `appDir` by * stripping the routing-root segments Next.js does not put in the URL: a leading * `src` then a leading `app`. `app/api` → `/api`; `src/app/api` → `/api`; * `app/api/v2` → `/api/v2`; `app` → `''` (root). This is why a client generated * from the same `appDir` as the routes can never target a stale prefix. */ export declare function deriveApiBasePath(appDir: string): string; /** * A single diagnostic in a Manifest response body. Mirrors the `ManifestDiagnostic` * the emitted `manifest-response` companion declares. */ export interface ManifestResponseDiagnostic { kind?: string; code?: string; message?: string; [key: string]: unknown; } /** * The command response body the Next.js dispatcher (and per-command routes) put * on the wire. Verified against the emitted code: success responds * `{ data, events, diagnostics }` and failure responds `{ error, diagnostics }`. * There is deliberately NO `success` field on the wire — callers branch on the * HTTP status. `data` is the command's return value. */ export interface ManifestCommandResponse { data?: T; events?: unknown[]; diagnostics?: ManifestResponseDiagnostic[]; error?: string; } /** * Canonical dispatcher route path (relative to `appDir`). Mirrors * `DISPATCHER_DEFAULTS.path` in the nextjs projection — the one place the * dispatcher's `[entity]/commands/[command]` shape is defined. */ export declare const DEFAULT_DISPATCHER_ROUTE_PATH = "/manifest/[entity]/commands/[command]/route.ts"; /** Options for {@link resolveRouteContract}. All optional; defaults are coherent. */ export interface RouteContractOptions extends EntitySegmentOptions { /** * URL prefix for read/detail paths. Default: derived from `appDir` via * {@link deriveApiBasePath}, so it tracks the route filesystem layout and * cannot desync. Set explicitly only to point a client at a different origin * prefix than where the routes are emitted. */ apiBasePath?: string; /** Dispatcher URL prefix. Default: `${apiBasePath}/manifest`. */ dispatcherBasePath?: string; /** App Router base directory route pathHints are relative to. Default `'app/api'`. */ appDir?: string; /** Dispatcher route path relative to `appDir`. Default {@link DEFAULT_DISPATCHER_ROUTE_PATH}. */ dispatcherRoutePath?: string; } /** * Normalized, framework-agnostic route contract. Every path method is pure and * deterministic. URL methods (`listPath`, `detailPath`, `dispatcherPath`, * `dispatcherInvocationPath`, `concreteCommandPath`) describe what a CLIENT * calls; the `*RoutePathHint` methods describe where the SERVER emits the route * file — the two are derived from the same `appDir`/`apiBasePath` so they agree. */ export interface RouteContract { readonly apiBasePath: string; readonly dispatcherBasePath: string; readonly appDir: string; readonly dispatcherRoutePath: string; /** URL/route segment for an entity (override → casing). */ entitySegment(entityName: string): string; /** `${apiBasePath}/${segment}` — the per-entity read/detail base. */ entityBasePath(entityName: string): string; /** `${entityBasePath}/list`. */ listPath(entityName: string): string; /** `${entityBasePath}/[id]` (param style selects the placeholder). */ detailPath(entityName: string, paramStyle?: ParamStyle): string; /** Deprecated per-command concrete URL: `${entityBasePath}/${command-slug}`. */ concreteCommandPath(entityName: string, commandName: string): string; /** Dispatcher URL *template*: `${dispatcherBasePath}/[entity]/commands/[command]`. */ dispatcherPath(paramStyle?: ParamStyle): string; /** * Concrete dispatcher invocation URL using the RAW entity/command names * (case-sensitive — the dispatcher resolves the command by its exact name): * `${dispatcherBasePath}/Recipe/commands/publishRecipe`. */ dispatcherInvocationPath(entityName: string, commandName: string): string; listRoutePathHint(entityName: string): string; detailRoutePathHint(entityName: string): string; concreteCommandRoutePathHint(entityName: string, commandName: string): string; dispatcherRoutePathHint(): string; listEnvelopeKey(entityName: string): string; detailEnvelopeKey(entityName: string): string; } /** * Resolve a {@link RouteContract} from options. Defaults are chosen so the * client and routes are coherent with zero configuration: `apiBasePath` and * `dispatcherBasePath` are derived from `appDir`, and the entity segment uses * the same override/casing logic the route emitter uses. */ export declare function resolveRouteContract(options?: RouteContractOptions): RouteContract; //# sourceMappingURL=route-contract.d.ts.map