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 thetype-drift-detectquery.
§0. Current state (22/06/2026)
Section titled “§0. Current state (22/06/2026)”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.tsvia thewrap-define-routecodemod (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.tsis deferred per the OPS-T1 PRODUCT.md §12.2 carve-out and is the only TRANSFORM-class route still unwrapped. - Generic
defineRoutectx type landed (Subtask ID-50.3, Design B variadic-tuple inlib/api/define-route.ts). A route’s narrow{ params: Promise<{ id: string }> }annotation now flows through unchanged, andwithRequestContext(defineRoute(...))composes with no cast — resolving the 118TS2345ctx-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 andworkspaces/[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-requiredprimary_subtopiccolumn (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 ResponseSchemamarker — the codemod could not infer a real Zod schema for them. Authoring those schemas, and driving thetype-drift-detectbaseline toward zero, is tracked as Subtask ID-50.12. type-drift-detect --ciis the standing gate. It runs on every canonical PR and exits non-zero on any newfetcher-onlyinterface 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}.
§1. Purpose
Section titled “§1. Purpose”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:
| CLI | Role | Mutates source? |
|---|---|---|
scripts/codemods/wrap-define-route.ts | The codemod | Yes, under --apply |
bun run ast-dataflow type-drift-detect | The 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-dataflowpackage (public repoai-solution-hub/ast-dataflow), which canonical consumes as a bun git dependency pinned to a SHA per DR-128; thebun run ast-dataflowscript name is preserved (it now resolves to the installed bin). History: shipped in-repo atlib/ast-dataflow/, relocated totools/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).
§2. Prerequisites
Section titled “§2. Prerequisites”-
bun installhas run andnode_modules/ts-morphis present (both CLIs use ts-morph). -
.type-drift-baseline.jsonexists. It is produced by the verifier and consumed by the codemod’s schema-inference step. If it is missing, the codemod aborts with anENOENT. 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).
§3. The nine-step workflow
Section titled “§3. The nine-step workflow”This expands PRODUCT.md §7’s seven steps into the nine discrete commands a developer runs.
Step 1 — Dry-run
Section titled “Step 1 — Dry-run”bun scripts/codemods/wrap-define-route.tsDefault 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.
Step 2 — Review the dry-run report
Section titled “Step 2 — Review the dry-run report”Open docs/generated/codemod-dry-run.md. Confirm:
- The
TRANSFORMset is the routes you expect to migrate mechanically. - The
MANUALset is the cron / MCP-transport / naked-no-auth routes plus any multi-method files you will handle by hand. - Any
TRANSFORMroute whose inferred schema isz.unknown()is flagged — it carries a// TODO(OPS-T1): author ResponseSchemacomment 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.
Step 4 — Apply
Section titled “Step 4 — Apply”bun scripts/codemods/wrap-define-route.ts --applyRewrites 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”bun run ast-dataflow type-drift-detect --prettyRenders 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 thedefineRoute(...)wrap. The wrap alone does not move an interface out of thefetcher-onlybucket — closing the gap is recorded by updating the baseline in step 6, which is exactly what the--cigate measures against.
Step 6 — Update the baseline
Section titled “Step 6 — Update the baseline”# 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.
Step 7 — Test and lint
Section titled “Step 7 — Test and lint”bun run testbun lintUse 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.
Step 8 — CI gate
Section titled “Step 8 — CI gate”bun run ast-dataflow type-drift-detect --ciMust 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).
Step 9 — Raise the migration PR
Section titled “Step 9 — Raise the migration PR”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.
§4. Troubleshooting
Section titled “§4. Troubleshooting”| Symptom | Cause / fix |
|---|---|
Codemod aborts: ENOENT … type-drift-baseline.json | The 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-zero | The 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 bucket | It 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 closed | The 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 wrong | Do not edit type-drift-detect.ts to force a pass. Escalate — it is a read-only contract surface. |
§5. Related references
Section titled “§5. Related references”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, includingtype-drift-detect.__tests__/integration/ops-t1-codemod-verifier.integration.test.ts— the end-to-end test that exercises this workflow against a temporary corpus.