import type TS from "typescript"; import type { HttpMethod, SchemaSource } from "../formats.js"; import { type StaticExtraction } from "./static-ts.js"; /** * Route-scan input-schema inference (PR 2, 04 §1): a collector seam asked * once per (route, method) at route-scan's single emission point. Collector * order (spec-locked): zod-in-handler first (Task 2, reuses the * oracle-hardened `zodFromExpression` from `static-ts.ts`), then the * TypeScript-checker collector (Task 3, one lazily built `ts.Program` per * scan), with the query collector (Task 4) merging additively into whichever * of those two results comes back — the query collector runs unconditionally * for every (route, method), regardless of which (if any) body collector * answered. Every collector fails closed: no recognizable evidence means * `null`, and route-scan emits exactly what it emits today (path params * only, blank body/query). * * Review carry-over on the query collector (Task 4): this module reports * query evidence whenever it finds it, on ANY method — it does NOT know or * care whether the tool that evidence would attach to is query-bound or * body-bound. That gate lives one layer up, in route-scan.ts's * `mergeRouteInput`, because only route-scan.ts knows a tool's `argsIn`. The * reason the gate has to exist at all: `runtime/registry.ts`'s route * execution (~560-564) sends every non-path argument as `searchParams` for a * query-bound tool (`argsIn: "query"`, GET/DELETE) but as a single JSON body * for a body-bound tool (`argsIn: "body"`, POST/PUT/PATCH) — never split * across both. A query-derived property advertised on a body-bound tool * would be delivered in the JSON body and never reach `searchParams`, so the * handler would never see it: a lie to the calling agent. `mergeRouteInput` * therefore keeps query properties for query-bound tools and drops them * (silently — this is a scope decision, not a recognition failure, so it * carries no `note`) for body-bound ones. */ /** The minimal route facts a collector needs. A structural subset of * route-scan's internal `RouteSource` — this module never imports from * route-scan.ts, so collectors stay one-directional (asked, never asking * back). */ export interface RouteContext { file: string; source: string; urlPath: string; kind: "app" | "pages"; } /** * State shared across every `inferRouteInput` call within one `scanRoutes` * pass — created once per scan by `createRouteScanState`, then threaded * through unchanged call to call. `zodExtraction` is the zod collector's * `StaticExtraction` (module parse cache), built lazily on first need and * reused for every subsequent (route, method) so each route file is parsed at * most once per scan even though it is asked about once per HTTP method; * `undefined` means "not attempted yet", `null` means "the host's TypeScript * compiler could not be resolved" (cached so we do not retry every call). * * `routeFiles` is every route file discovered by this scan (route-scan.ts * hands the full list to `createRouteScanState` up front, before the * per-route loop) — the checker collector's `ts.Program` needs every route * file present as a root from the moment it is built, since the program is * constructed exactly once and never rebuilt mid-scan (04 §1 Task 3). `checkerProgram`/ * `checkerTs` follow the same lazy-build-then-cache shape as `zodExtraction`; * `checkerProgramBuilds` counts how many times the program-construction path * actually ran (0 or 1 per scan — an injectable/inspectable counter the tests * use to assert the program is built at most once, and never when the zod * collector already answered). `warnings` collects scan-level messages (e.g. * "no tsconfig.json found") the checker collector can't attach to a single * tool; route-scan.ts drains this into its own returned `warnings` array. */ export interface RouteScanState { root: string; routeFiles: readonly string[]; zodExtraction?: StaticExtraction | null; checkerProgram?: TS.Program | null; checkerTs?: typeof TS | null; checkerProgramBuilds: number; warnings: string[]; } export declare function createRouteScanState(root: string, routeFiles?: readonly string[]): RouteScanState; /** * One collector's verdict for a route+method's input, additive by design: * `bodySchema` / `queryProperties` are undefined when that collector found * nothing for that half of the tool's args, and `note` carries a fail-closed * reason onto the emitted tool exactly like the tRPC/server-actions * extractors do for partially- or un-recognized shapes (04 §1). */ export interface RouteInputResult { bodySchema?: Record; queryProperties?: Record; /** Which collector produced `bodySchema`. Absent when nothing was * recognized (a permissive fallback, or query scraping alone), which the * caller records as `"unknown"` — a scraped query string is evidence of * use, not a declaration of the argument list. */ source?: Extract; note?: string; } /** * Ask every collector (spec-locked order) for `route`'s `method` input. * Returns `null` when nothing is recognized, so route-scan falls back to * today's exact path-params-only emission (fail-closed, byte-identical). * The zod collector runs first (a validator is stronger evidence than a * bare type) and short-circuits: the checker collector below is never even * asked when zod already answered, so its `ts.Program` is never built for a * handler zod already covers. The query collector (Task 4) then runs * unconditionally — win or lose for the body half — and merges additively * into whatever the body collectors returned (or answers on its own when * they found nothing): `queryProperties` from the query collector always * wins over any (empty, in practice) `queryProperties` a body collector * might have set, since only the query collector ever populates that field. */ export declare function inferRouteInput(route: RouteContext, method: HttpMethod, state: RouteScanState): Promise;