Skip to content

OPS-T1 Codemod Runbook — wrap-define-route + type-drift-detect

OPS-T1 Codemod Runbook — wrap-define-route + type-drift-detect

Section titled “OPS-T1 Codemod Runbook — wrap-define-route + type-drift-detect”

Status: Corpus rollout complete — canonical PR #50 (Task ID-50) merged 22/06/2026 wrapped 184 of 195 routes. The runbook is retained as the standing procedure for targeted re-wraps, for newly-added routes, and for the {50.12} placeholder-schema follow-up. Audience: a developer wrapping a single new or re-classified route, or working through the residual z.unknown() placeholders. Pair with: scripts/codemods/README.md (quick reference next to the code), docs/specs/id-16-ast-dataflow-tool/ops-t1-codemod/PRODUCT.md §7 (the canonical verifier workflow this runbook expands), and the ast-dataflow catalogue (.claude/skills/ast-dataflow/SKILL.md) for the type-drift-detect query.


The OPS-T1 corpus rollout has landed on main via canonical PR #50 (Task ID-50). What that PR changed:

  • 184 of 195 routes wrapped in app/api/**/route.ts via the wrap-define-route codemod (Subtasks ID-50.5 through ID-50.9). The 19 cron / naked-no-auth / MCP-transport routes are MANUAL by design and remain unwrapped. app/api/upload/route.ts is deferred per the OPS-T1 PRODUCT.md §12.2 carve-out and is the only TRANSFORM-class route still unwrapped.
  • Generic defineRoute ctx type landed (Subtask ID-50.3, Design B variadic-tuple in lib/api/define-route.ts). A route’s narrow { params: Promise<{ id: string }> } annotation now flows through unchanged, and withRequestContext(defineRoute(...)) composes with no cast — resolving the 118 TS2345 ctx-contravariance errors that previously blocked wrapping any parameterised or +WRC route.
  • Three latent route bugs surfaced and fixed by the strict net during the {50.10} reconciliation: profiles/[id] DELETE and workspaces/[id]/sources/[sourceId] DELETE were mis-bound to entity schemas but actually return { success[, action] } envelopes (now rebound to the correct shapes); procurement/[id]/responses/[rId] GET omitted the schema-required primary_subtopic column (now in the .select() projection + row mapping). No .loose() masking was used during reconciliation (INV-S held).
  • 146 of the 184 wrapped routes carry a z.unknown() placeholder schema with a // TODO(OPS-T1): author ResponseSchema marker — the codemod could not infer a real Zod schema for them. Authoring those schemas, and driving the type-drift-detect baseline toward zero, is tracked as Subtask ID-50.12.
  • type-drift-detect --ci is the standing gate. It runs on every canonical PR and exits non-zero on any new fetcher-only interface absent from the committed .type-drift-baseline.json — the regression guard that holds the rollout’s gains.

Use the §3 nine-step workflow below for any incremental wrap — for example, a newly-added route, a route promoted out of the MANUAL set, or a placeholder-to-real schema swap under {50.12}.


OPS-T1 migrates the app/api/**/route.ts corpus onto the defineRoute(...) helper, which validates each route’s response payload against a Zod ResponseSchema. This runbook is the end-to-end procedure for performing that migration safely: from the first dry-run, through authoring the missing schemas, applying the rewrite, and proving — via the type-drift-detect verifier — that the migration closed real gaps and introduced no new ones.

The migration is a one-off, hand-run activity. The codemod is not wired into CI; the verifier is. You run the codemod, review the diff, update the baseline, and the type-drift-detect --ci gate guards the result on every future PR.

Two CLIs are involved:

CLIRoleMutates source?
scripts/codemods/wrap-define-route.tsThe codemodYes, under --apply
bun run ast-dataflow type-drift-detectThe verifier (gate)No — read-only contract surface

The verifier (type-drift-detect) is a read-only contract surface. Do not modify it as part of a migration; if its classification looks wrong, escalate rather than editing the gate to make a run pass. The query lives in the standalone @ai-solution-hub/ast-dataflow package (public repo ai-solution-hub/ast-dataflow), which canonical consumes as a bun git dependency pinned to a SHA per DR-128; the bun run ast-dataflow script name is preserved (it now resolves to the installed bin). History: shipped in-repo at lib/ast-dataflow/, relocated to tools/ast-dataflow/ in S393 bl-360, then extracted to its own repo in id-377 {377.5} (canonical PR #177, executing DR-100’s extraction rider).


  • bun install has run and node_modules/ts-morph is present (both CLIs use ts-morph).

  • .type-drift-baseline.json exists. It is produced by the verifier and consumed by the codemod’s schema-inference step. If it is missing, the codemod aborts with an ENOENT. Generate or refresh it first:

    Terminal window
    bun run ast-dataflow type-drift-detect --update-baseline
  • You are on a feature branch (never migrate directly on main).


This expands PRODUCT.md §7’s seven steps into the nine discrete commands a developer runs.

Terminal window
bun scripts/codemods/wrap-define-route.ts

Default mode. No route.ts is touched. The codemod enumerates the corpus, classifies each route, and writes two artefacts into docs/generated/:

  • codemod-dry-run.md — the diff preview, a verdict tally (TRANSFORM / NEEDS_REVIEW / MANUAL / SKIPPED), and the shape distribution.
  • codemod-needs-manual.json — the structured list of MANUAL and NEEDS-REVIEW routes with their reason codes.

Open docs/generated/codemod-dry-run.md. Confirm:

  • The TRANSFORM set is the routes you expect to migrate mechanically.
  • The MANUAL set is the cron / MCP-transport / naked-no-auth routes plus any multi-method files you will handle by hand.
  • Any TRANSFORM route whose inferred schema is z.unknown() is flagged — it carries a // TODO(OPS-T1): author ResponseSchema comment in the preview. These are the routes you must address in step 3.

Step 3 — Author the missing ResponseSchema objects

Section titled “Step 3 — Author the missing ResponseSchema objects”

For every placeholder route (the z.unknown() cases), author the real Zod ResponseSchema so the apply step inserts a concrete schema instead of the placeholder. Co-locate the schema using the project’s existing convention (${InterfaceName}Schema in lib/validation/schemas.ts) so the codemod’s Source A inference can bind it. Re-run the dry-run (step 1) and confirm the placeholder count has dropped before applying.

Terminal window
bun scripts/codemods/wrap-define-route.ts --apply

Rewrites the MECHANISABLE and NEEDS-REVIEW routes on disk, wrapping each handler in defineRoute(ResponseSchema, ...). withRequestContext routes keep withRequestContext as the outermost wrapper (AC-7). MANUAL routes and already-wrapped routes are left byte-identical (idempotency). After writing, the codemod runs bun run format over the modified set.

Step 5 — Human review of the drift picture

Section titled “Step 5 — Human review of the drift picture”
Terminal window
bun run ast-dataflow type-drift-detect --pretty

Renders the current fetcher-only / route-only / enforced / unused classification as Markdown. Read it as a human: the routes you just migrated should be the ones you intend to record as closed in the next step.

Why the wrapped routes still show as fetcher-only: the verifier detects a route annotation via the handler’s return type, not via the defineRoute(...) wrap. The wrap alone does not move an interface out of the fetcher-only bucket — closing the gap is recorded by updating the baseline in step 6, which is exactly what the --ci gate measures against.

Terminal window
# Option A — regenerate from the current state:
bun run ast-dataflow type-drift-detect --update-baseline
# Option B — hand-edit .type-drift-baseline.json to remove
# the closed-gap entries.

.type-drift-baseline.json records the accepted fetcher-only set. Removing an entry asserts “this gap is closed and must never reappear”. The migration PR must include this baseline change; the --ci gate enforces that the recorded count does not grow.

Terminal window
bun run test
bun lint

Use bun run test (Vitest), not bun test (Bun’s built-in runner). The codemod-specific suites live under __tests__/scripts/codemods/; the verifier-integration test runs separately via bun run test:integration.

Terminal window
bun run ast-dataflow type-drift-detect --ci

Must exit 0. A non-zero exit lists, on stderr, the fetcher-only interfaces that are new since the baseline — i.e. gaps you introduced and have not recorded. Resolve each one (annotate the route, or record it in the baseline with justification) and re-run until the gate is clean. This is the same gate CI runs on the PR (PRODUCT.md AC-10).

Include in the PR: the rewritten route.ts files, any new ResponseSchema objects, and the updated .type-drift-baseline.json. CI re-runs the type-drift-detect --ci gate as a blocking check.


SymptomCause / fix
Codemod aborts: ENOENT … type-drift-baseline.jsonThe baseline does not exist. Generate it first: bun run ast-dataflow type-drift-detect --update-baseline (see §2).
--apply warns the format pass exited non-zeroThe codemod still saved the files; bun run format failed for an unrelated reason. Run bun run format manually over the modified routes and proceed.
A route you expected to transform is in the MANUAL bucketIt matched a MANUAL discriminator (cron path, MCP transport, no auth wrapper). Migrate it by hand; the codemod intentionally will not rewrite it.
--ci exits 1 listing an interface you thought you closedThe interface is still fetcher-only and not in the baseline. Either annotate the route’s return type, or record the entry in the baseline.
Verifier classification looks wrongDo not edit type-drift-detect.ts to force a pass. Escalate — it is a read-only contract surface.

  • scripts/codemods/README.md — quick reference adjacent to the codemod.
  • docs/specs/id-16-ast-dataflow-tool/ops-t1-codemod/PRODUCT.md — behaviour spec (§7 verifier workflow, §8 acceptance criteria).
  • docs/specs/id-16-ast-dataflow-tool/ops-t1-codemod/TECH.md — implementation spec.
  • .claude/skills/ast-dataflow/SKILL.md — the ast-dataflow query catalogue, including type-drift-detect.
  • __tests__/integration/ops-t1-codemod-verifier.integration.test.ts — the end-to-end test that exercises this workflow against a temporary corpus.