Skip to content

OPS-T1 codemod — wrap-define-route — Implementation PLAN

OPS-T1 codemod — wrap-define-route — Implementation PLAN

Section titled “OPS-T1 codemod — wrap-define-route — Implementation PLAN”

Status: AMENDED-S265 (Option-4 re-scope, Liam-ratified S262). The S11/S62C decomposition (Subtasks 32.5–32.16) is retained as HISTORICAL / SUPERSEDED- mechanics (§4 — the wrapping / classification / inference / binding machinery it built is SOUND and is REUSED); the Option-4 amendment adds §0 (the continuous real-corpus probe mandate, the single most important S262 process fix) and three new implementation Subtasks ({32.25}/{32.26}/{32.27}, §4a) that re-scope the deliverable from a compile-time codemod to a runtime strict-validation model. What changed: the defineRoute contract (payload-returning → pass-through), schema strictness (permissive .loose() → INV-S strict), and the AC framing (§8 reinterpreted). What did NOT change: the codemod mechanics (classify → infer → wrap → emit). Source: ops-t1-codemod/PRODUCT.md (AMENDED-S265 — §8 INV-PT / INV-FP / INV-S + the reframed AC-1..AC-10), ops-t1-codemod/TECH.md (AMENDED-S265 — §2.4a pass-through contract, §2.4a.2 INV-FP failure policy, §3.1a schema- strictness rules, §11 continuous-probe mandate, §12 empirical verification), ops-t1-codemod/route-shape-inventory.md; docs/reference/test-philosophy.md (criteria 1–6 for testStrategy lines). Sibling: ops-t1-codemod/PRODUCT.md, ops-t1-codemod/TECH.md.


0. Continuous real-corpus probe mandate (the governing S262 process fix)

Section titled “0. Continuous real-corpus probe mandate (the governing S262 process fix)”

This is the single most important process fix from the S262 saga, and the process change that would have caught defects B1–B4 on the first slice that introduced each, rather than at the end. PRODUCT §8 and TECH §11 both make this binding; this §0 is the PLAN-side restatement that PRODUCT §8’s “see PLAN §0” cross-reference resolves to. Mirrors TECH §11 verbatim in intent.

The failure it fixes. In S262 the real-corpus acceptance gate — apply the codemod against the live app/api corpus, then run the route unit suite — ran LAST, as a final gate (the original Subtask 32.16). Because it ran last, all four defects surfaced only at the end:

  • B1withRequestContext substring false-positive (the codemod matched withRequestContextBare by substring; AC-7 now mandates an EXACT-callee match).
  • B2 — Source-A inference produced 0/195 real schema binds on disk (the ${interface}Schema lookup silently fell through to z.unknown()).
  • B3 — rewritten routes were missing their z / schema imports → 130+ routes threw ReferenceError at module load (ESLint is blind to no-undef on TS, so the lint gate did NOT catch it — AC-9 now pairs the lint-delta with a tsc/no-undef gate).
  • B4 — the payload-returning defineRoute contract double-wrapped the ~178 routes that return NextResponse inline → 1676/2075 route tests failed (the contract is now pass-through, INV-PT).

The mandate (binding on {32.25}/{32.26}/{32.27}). The real-corpus probe runs from the FIRST implementation slice onward, NOT as a final gate:

  1. The probe. Apply the codemod --apply against a temp copy of the working tree (git archive HEAD | tar -x into an mkdtempSync dir with node_modules symlinked — the working tree is NEVER mutated), then run the route unit suite (vitest run __tests__/api) against the migrated copy, under the pass-through wrapper (INV-PT) so inline NextResponse error returns pass through unchanged.
  2. Run it per slice, continuously. Every implementation Subtask runs the probe as it lands — {32.25} to prove the pass-through redesign does not regress the route suite (the B4 reinterpretation); {32.26} to surface any pre-existing real drift the tightened schemas now reject, under the INV-FP loud-in-test policy (S262 OQ-1); {32.27} as the green-on-all-AC closing assertion. Each Subtask’s details (in task-list.json) repeats this mandate.
  3. It is the AC-8 oracle. AC-8 (PRODUCT §8.1) is, definitionally, this probe’s route-unit-suite pass under the pass-through wrapper. Do not treat AC-8 as a separate, after-the-fact check.

Reframing note for §§1–8 below. Sections 1–8 describe the SUPERSEDED S11/ S62C decomposition (Subtasks 32.5–32.16). They are preserved as the historical record of the SOUND mechanics now reused by the Option-4 Subtasks. The LIVE decomposition is §4a (Subtasks {32.25}/{32.26}/{32.27}) and the LIVE traceability matrix is §5a. Read §4a + §5a as authoritative; read §§1–8 as context for what was already built.

1. Decomposition overview — SUPERSEDED (S11/S62C historical record)

Section titled “1. Decomposition overview — SUPERSEDED (S11/S62C historical record)”

SUPERSEDED-S265. This section and §§2–8 describe the original S11/S62C decomposition (Subtasks 32.5–32.16). Subtasks 32.5–32.16 SHIPPED (per the S262 id32 final report) and built the codemod mechanics — classify, infer, wrap, emit — which are SOUND and REUSED under Option-4. The Option-4 re-scope did NOT discard this work; it changed the defineRoute contract (payload-returning → pass-through, §4a {32.25}), schema strictness (permissive → INV-S strict, §4a {32.26}), and the AC framing (§4a {32.27}). The LIVE decomposition is §4a; the LIVE traceability matrix is §5a. Treat the records below as history, not as the dispatch plan.

ID-32 was originally decomposed into 12 implementation Subtasks (32.5..32.16) plus the 4 already-recorded spec Subtasks (32.1 RESEARCH ✓, 32.2 PRODUCT ✓, 32.3 TECH ✓, 32.4 PLAN — this document; the Option-4 amendment is Subtask 32.23, which authored §0 + §4a + §5a here).

Decomposition principles applied:

  • One mechanism per Subtask. Each Subtask owns a single load-bearing mechanism (scaffold, classifier, inference Source A, rewrite, etc.) so that Checker review is bounded and rollback is per-Subtask.
  • Single-method vs multi-method split. Subtask 32.10 covers the 137 MECHANISABLE single-method routes; Subtask 32.11 covers the 40 NEEDS-REVIEW multi-method routes. Separated because multi-method logic is structurally distinct (per-method schema inference, per-method wrap loop, per-method codemod-needs-manual.json entries).
  • Verifier kept end-to-end. Subtask 32.15 wires type-drift-detect as the post-migration gate per PRODUCT §7; not folded into 32.16’s acceptance probe because the verifier integration is a discrete contract surface (CLI args, exit codes, baseline update procedure).
  • Acceptance gate as its own Subtask. Subtask 32.16 runs AC-1..AC-10 end-to-end against the full 193-route corpus. Separated so AC failure can be attributed to a specific upstream Subtask without entangling the diagnosis with implementation work.
  • All Subtasks sibling-only. No Subtask in 32.5..32.16 references any external Task — the Task-level dep on ID-31 is set at the Task record.

Subtask count: 12 implementation + 4 spec = 16 total within Task 32. Well within the 25-Subtask soft ceiling (§3.4 / A7).


32.5 (scaffold + defineRoute wrapper)
├──> 32.6 (shape classifier)
│ │
│ └──> 32.7 (test fixture corpus, 14 fixtures)
│ │
│ ├──> 32.8 (ResponseSchema inference Source A)
│ │ │
│ │ ├──> 32.9 (ResponseSchema inference Source B — optional)
│ │ │
│ │ ├──> 32.10 (handler rewrite — single-method)
│ │ │ │
│ │ │ └──> 32.13 (idempotency check)
│ │ │ │
│ │ │ └──> 32.14 (apply mode + format pass)
│ │ │ │
│ │ │ ├──> 32.15 (verifier integration)
│ │ │ │ │
│ │ │ │ └──> 32.16 (AC-1..AC-10 gate)
│ │ │ │
│ │ │
│ │ └──> 32.11 (handler rewrite — multi-method)
│ │ │
│ │ └──> (joins into 32.13 above)
│ │
│ └──> 32.12 (CLI + output artefact emitters)
│ │
│ └──> (joins into 32.14 above)

Parallelisable slices (within Task 32):

  • 32.8 + 32.9 + 32.12 — once 32.7 lands, inference Source A, inference Source B, and the CLI/emitter Subtasks can be assigned to parallel Executors. None of them depend on each other.
  • 32.10 + 32.11 — once 32.8 lands, single-method and multi-method rewrites can run in parallel (different files, different shape buckets).

Serial-only choke points:

  • 32.5 → 32.6: classifier needs the defineRoute import target to test against.
  • 32.7 → 32.10/32.11: rewrite logic is exercised through the fixture set.
  • 32.14 → 32.15: verifier integration runs against post-apply state.
  • 32.15 → 32.16: acceptance gate runs after verifier confirms no regression.

IDRiskLikelihoodImpactMitigation
R-1defineRoute() wrapper authoring (32.5) blocks every downstream Subtask. PRODUCT §9 declares it OOS for the codemod, but TECH.md and ACs require it to exist before any fixture test can run.HighHighSubtask 32.5 folds the wrapper authoring into the scaffold Subtask as a hard prereq. OQ-1 escalates to Liam: fold-in vs separate task. If Liam prefers separate, sub-orchestrator splits as a new Task ID-32a pre-32.5.
R-2ResponseSchema name-convention lookup (Source A) fails for interfaces lacking a co-located Schema constant. TECH §3.A trade-off explicitly flags this.MediumMediumSubtask 32.8’s testStrategy includes a fixture (with-schema-in-baseline.ts) for the happy path and a with-baseline-but-no-schema-constant.ts fixture for the placeholder fall-back. AC-5 only requires Source-A routes with a discoverable schema constant to receive a real schema.
R-3withRequestContext double-wrapping order regression. TECH §8.1 spells out that the outer wrapper order is request-context-propagation-sensitive.MediumHighSubtask 32.10’s testStrategy includes the auth-plain-with-wrc.ts fixture; AC-7 explicitly asserts the wrap order. Checker for 32.10 must verify against TECH §8.1 verbatim.
R-4bun run test regression after apply (AC-8) caused by ts-morph print-order changes or import-ordering drift not caught by 32.14’s format pass.MediumMediumSubtask 32.14 runs bun run format against all modified files before save; Subtask 32.16’s AC-8 gate runs the full Vitest suite as the regression check. If new failures surface, Checker triages whether the codemod or pre-existing tests are responsible.
R-5type-drift-detect --ci (AC-10) fails post-apply because the baseline was not updated to reflect closed gaps.LowMediumSubtask 32.15’s testStrategy is the verifier-integration walk-through (run —pretty, edit baseline, run —ci, confirm exit 0). Subtask 32.16 includes an explicit baseline-diff check as part of the AC-10 gate.

Test-invocation convention (applies to every Subtask 32.5..32.16 below that invokes the test suite): Run tests via bun run test (Vitest) — NOT bun test (Bun’s built-in runner produces a Vitest config mismatch). Per TECH §8.6 / CLAUDE.md “Gotchas — Testing”. Applies uniformly to every testStrategy in this section without per-Subtask repetition.

Subtask 32.5 — Scaffold codemod + author defineRoute wrapper

Section titled “Subtask 32.5 — Scaffold codemod + author defineRoute wrapper”

Scope: Create scripts/codemods/wrap-define-route.ts skeleton (CLI argv parse stub, ts-morph Project init, route enumeration with --scope filter, exit-code 0/1 contract). Author lib/api/define-route.ts — the typed wrapper function that downstream Subtasks rewrite handlers to call. The wrapper takes a Zod ResponseSchema and an async handler; returns a Next.js-compatible route export that validates the handler’s return payload at runtime against the schema (or, at minimum, type-checks the handler’s return shape at compile time).

File ownership:

  • scripts/codemods/wrap-define-route.ts — new file (skeleton only; no rewrite logic yet)
  • lib/api/define-route.ts — new file
  • __tests__/lib/api/define-route.test.ts — new test file (wrapper unit tests, separate from codemod fixture suite)
  • package.json — only if a new script alias is needed (e.g. "codemod:wrap-define-route")

Dependencies: none (root Subtask).

Acceptance criteria (PRODUCT.md mapping):

  • AC-1 (dry-run completes without exception) — scaffold must at minimum enumerate the 193 routes and exit 0.

testStrategy: Vitest fixture __tests__/lib/api/define-route.test.ts asserts that defineRoute(SchemaX, handler) returns a callable route export which (a) invokes the handler with the (request, ctx) tuple, (b) validates the handler’s return value against SchemaX, (c) returns a 500 envelope when validation fails. Codemod-side: __tests__/scripts/codemods/wrap-define-route.test.ts asserts the CLI invocation with --help prints usage and exits 0, and with no args returns exit 0 against a temp fixture directory.


Scope: Implement the classifyRoute(sf: SourceFile): RouteShape function per TECH §2.3. Classifier inspects imports, exported HTTP-method declarations, path-parameterisation ([ in path), body usage (request.json() / parseBody(), and withRequestContext presence. Returns one of the 10 primary shape codes plus the +WRC sub-variant flag.

File ownership:

  • scripts/codemods/wrap-define-route.ts — add classifyRoute() function and getExportedMethods() helper.
  • scripts/codemods/types.ts — new file for RouteShape discriminated union and NeedsManualReason type.

Dependencies: [5]

Acceptance criteria (PRODUCT.md mapping):

  • AC-2 (apply rewrites exactly 137 MECHANISABLE) — classifier must correctly bucket routes; tested via fixture suite in 32.7.
  • AC-4 (codemod-needs-manual.json lists 16 MANUAL + NEEDS-REVIEW) — classifier determines which routes flow into the report.

testStrategy: Vitest unit test classifyRoute() against synthetic ts-morph SourceFile objects built from each of the 10 shape templates; asserts the returned shape code matches expected per fixture. Each test title reads as a behaviour line: “classifies a single-method auth-wrapped handler with no params as AUTH_PLAIN”, “classifies a cron-secret route as CRON”, etc. Per test-philosophy §1, assertions are on the returned shape value (public return), never on internal classifier helpers.


Subtask 32.7 — Test fixture corpus (14 fixtures)

Section titled “Subtask 32.7 — Test fixture corpus (14 fixtures)”

Scope: Author the 14 fixture files per TECH §4 at __tests__/scripts/codemods/fixtures/wrap-define-route/. Each fixture is a self-contained route.ts-shaped TypeScript file representing one shape variant (plus the three inference / idempotency special cases). Each fixture ships with a matching expected-output snapshot file for the post-rewrite state (for MECHANISABLE / NEEDS-REVIEW) or a reason-code expectation for MANUAL.

File ownership:

  • __tests__/scripts/codemods/fixtures/wrap-define-route/auth-plain.ts
  • __tests__/scripts/codemods/fixtures/wrap-define-route/auth-plain-with-wrc.ts
  • __tests__/scripts/codemods/fixtures/wrap-define-route/param-body.ts
  • __tests__/scripts/codemods/fixtures/wrap-define-route/body-validated.ts
  • __tests__/scripts/codemods/fixtures/wrap-define-route/param-only.ts
  • __tests__/scripts/codemods/fixtures/wrap-define-route/multi-param-body.ts
  • __tests__/scripts/codemods/fixtures/wrap-define-route/multi-body.ts
  • __tests__/scripts/codemods/fixtures/wrap-define-route/multi-param.ts
  • __tests__/scripts/codemods/fixtures/wrap-define-route/cron.ts
  • __tests__/scripts/codemods/fixtures/wrap-define-route/naked-no-auth.ts
  • __tests__/scripts/codemods/fixtures/wrap-define-route/mcp.ts
  • __tests__/scripts/codemods/fixtures/wrap-define-route/already-wrapped.ts
  • __tests__/scripts/codemods/fixtures/wrap-define-route/with-schema-in-baseline.ts
  • __tests__/scripts/codemods/fixtures/wrap-define-route/with-return-type-annotation.ts
  • __tests__/scripts/codemods/wrap-define-route.test.ts — initial test harness importing the fixtures and asserting shape classification per fixture.

Dependencies: [6]

Acceptance criteria (PRODUCT.md mapping):

  • AC-1..AC-10 (downstream Subtasks exercise the full AC matrix through these fixtures).

testStrategy: Vitest fixture harness loads each fixture into a temporary ts-morph Project, runs classifyRoute(), and asserts the returned shape. Test titles read as product behaviour: “AUTH_PLAIN fixture classifies as AUTH_PLAIN”, “auth-plain-with-wrc fixture classifies as AUTH_PLAIN+WRC”, etc. Per test-philosophy §6, fixture content is generated by a factory helper (createRouteFixture({ shape, methods, withRequestContext })) where possible, not hand-rolled per file.


Subtask 32.8 — ResponseSchema inference Source A (type-drift-baseline.json)

Section titled “Subtask 32.8 — ResponseSchema inference Source A (type-drift-baseline.json)”

Scope: Implement Source A inference per TECH §3.A. Read docs/generated/type-drift-baseline.json; for each baseline entry, map the interface name to the route file via the R-WP17 heuristic URL matcher (read the existing logic in lib/ast-dataflow/queries/type-drift-detect.ts); look up ${interfaceName}Schema or ${interfaceName}ZodSchema in lib/validation/schemas.ts; return the schema identifier or fall back to z.unknown() placeholder with a NEEDS-SCHEMA reason code.

File ownership:

  • scripts/codemods/wrap-define-route.ts — add inferSchema(sf, method, project) function with Source A logic.
  • scripts/codemods/inference-source-a.ts — extracted helper module if the baseline-loading + interface-to-route mapping exceeds 50 LOC.
  • __tests__/scripts/codemods/fixtures/wrap-define-route/with-baseline-but-no-schema-constant.ts — new fall-back-path fixture authored alongside Source A inference logic. Represents a route that IS in type-drift-baseline.json but whose interface has NO co-located ${interfaceName}Schema constant in lib/validation/schemas.ts; exercises the z.unknown() + NEEDS-SCHEMA fall-back branch. Counterpart to with-schema-in-baseline.ts (the happy path) authored by Subtask 32.7. Note: 32.7’s fixture set is 14; this raises the total fixture count to 15 once 32.8 lands.

Dependencies: [7]

Acceptance criteria (PRODUCT.md mapping):

  • AC-5 (R-WP17 37-interface baseline routes get real schema from type-drift-baseline.json, not z.unknown()).
  • AC-6 (routes with z.unknown() placeholder get the TODO comment).

testStrategy: Vitest test against fixture with-schema-in-baseline.ts asserts that inferSchema() returns IntelligenceWorkspaceListResponseSchema (the synthetic schema constant declared in the test’s mocked lib/validation/schemas.ts). A second fixture with-baseline-but-no-schema-constant.ts asserts the function returns z.unknown() + NEEDS-SCHEMA reason code. Titles read as behaviour: “inserts real schema when baseline interface has a co-located Schema constant”, “falls back to z.unknown() placeholder when the schema constant is missing”.


Subtask 32.9 — ResponseSchema inference Source B (return-type annotation) — OPTIONAL

Section titled “Subtask 32.9 — ResponseSchema inference Source B (return-type annotation) — OPTIONAL”

Scope: Implement Source B inference per TECH §3.B. If the handler carries Promise<NextResponse<X>> return-type annotation, extract X and resolve via the same name-convention lookup as Source A. If no annotation present, return null (signalling fall-back to Source A). Per TECH §3 recommended ranking, this Subtask is time-boxed: implement only if Subtask 32.8 landed and there is bandwidth in the implementation session. Source C is explicitly out of scope for this PLAN cycle.

File ownership:

  • scripts/codemods/wrap-define-route.ts — add extractNextResponseTypeArg() and extend inferSchema() to chain Source B → Source A (canonical runtime precedence: Source B’s explicit return-type annotation is authoritative; Source A is the heuristic fall-through when no annotation is present — see TECH §3 “Canonical chain order”).
  • scripts/codemods/inference-source-b.ts — extracted helper if needed.

Dependencies: [8]

Acceptance criteria (PRODUCT.md mapping):

  • (AC-5 is the Source-A contract; Source B is nice-to-have and not AC-gated per TECH §3 recommended ranking.)

testStrategy: Vitest test against fixture with-return-type-annotation.ts asserts that inferSchema() extracts the inner type and returns the matching schema identifier when one exists. A negative test asserts Source B returns null when no annotation is present, so Source A’s result is used. If this Subtask is descoped at dispatch time, the fixture is left in place and the test is marked it.skip() with a TODO referencing this PLAN.


Subtask 32.10 — Handler rewrite — single-method shapes

Section titled “Subtask 32.10 — Handler rewrite — single-method shapes”

Scope: Implement the rewrite logic per TECH §2.4 for the four single-method MECHANISABLE shapes: AUTH_PLAIN, PARAM_BODY, BODY_VALIDATED, PARAM. Includes the withRequestContext sub-variant for each: preserves the outer wrapper per TECH §8.1 / AC-7. Targets FunctionDeclaration AST nodes with names in {GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS}; rewrites to VariableStatement with export const METHOD = defineRoute(Schema, async (...) => { ... }). Preserves Next.js 15 Promise<params> second-argument shape per TECH §8.2. Skips export const maxDuration, dynamic, runtime config exports per TECH §8.3.

File ownership:

  • scripts/codemods/wrap-define-route.ts — add rewriteSingleMethod(sf, method, schema) function.
  • scripts/codemods/rewrite-single-method.ts — extracted helper if rewrite logic exceeds 80 LOC.

Dependencies: [8]

Acceptance criteria (PRODUCT.md mapping):

  • AC-2 (137 MECHANISABLE routes get rewritten — single-method portion).
  • AC-7 (withRequestContext outer-wrap preservation).
  • AC-6 (TODO comment for z.unknown() placeholder routes).

testStrategy: Vitest snapshot tests against each single-method fixture (auth-plain.ts, auth-plain-with-wrc.ts, param-body.ts, body-validated.ts, param-only.ts) assert the post-rewrite source text matches an inline snapshot. Test titles describe the observable transformation: “wraps an AUTH_PLAIN handler with defineRoute”, “preserves withRequestContext as the outer wrapper”, “preserves the Promise second argument unchanged”. Per test-philosophy §1, assertions are on the rewritten file text (observable output), not on the ts-morph FunctionDeclaration internal state.


Subtask 32.11 — Handler rewrite — multi-method shapes

Section titled “Subtask 32.11 — Handler rewrite — multi-method shapes”

Scope: Implement the rewrite logic for the three NEEDS-REVIEW multi-method shapes: MULTI_PARAM_BODY, MULTI_BODY, MULTI_PARAM. Each exported method is wrapped individually with its own per-method ResponseSchema; the codemod emits one codemod-needs-manual.json entry per method with reason code MULTI_METHOD_SCHEMA. withRequestContext sub-variant handling reuses 32.10’s logic.

File ownership:

  • scripts/codemods/wrap-define-route.ts — add rewriteMultiMethod(sf, methods, schemas) function.
  • scripts/codemods/rewrite-multi-method.ts — extracted helper if needed.

Dependencies: [10]

Acceptance criteria (PRODUCT.md mapping):

  • AC-2 (40 NEEDS-REVIEW routes wrapped; flagged in codemod-needs-manual.json).
  • AC-4 (NEEDS-REVIEW routes appear in the report with MULTI_METHOD_SCHEMA reason).

testStrategy: Vitest snapshot tests against multi-method fixtures (multi-param-body.ts, multi-body.ts, multi-param.ts) assert each exported method is independently wrapped and each method generates its own MULTI_METHOD_SCHEMA entry in the mock codemod-needs-manual.json. Test titles: “wraps GET and PATCH independently in a MULTI_PARAM_BODY route”, “emits one MULTI_METHOD_SCHEMA entry per method”.


Subtask 32.12 — CLI + output artefact emitters

Section titled “Subtask 32.12 — CLI + output artefact emitters”

Scope: Implement the CLI per TECH §5 (argv parse via util.parseArgs, --apply, --scope, --help flags, exit codes 0/1). Implement the two output artefact emitters per TECH §6: docs/generated/codemod-dry-run.md (human-readable markdown report) and docs/generated/codemod-needs-manual.json (JSONL with the NeedsManualEntry schema). Both emitted on every run (dry-run and apply). The markdown report follows the format shown in PRODUCT §5; the JSONL follows TECH §6.2 schema.

File ownership:

  • scripts/codemods/wrap-define-route.ts — add parseCliArgs(), emitDryRunReport(routes), emitNeedsManualReport(routes).
  • scripts/codemods/emit-dry-run.ts — extracted markdown emitter helper.
  • scripts/codemods/emit-needs-manual.ts — extracted JSONL emitter helper.

Dependencies: [7]

Acceptance criteria (PRODUCT.md mapping):

  • AC-4 (both artefacts produced in every run; MANUAL + NEEDS-REVIEW routes appear with reason codes).
  • AC-1 (CLI runs to completion without error).

testStrategy: Vitest tests invoke the CLI via bun scripts/codemods/wrap-define-route.ts --help (asserts usage text + exit 0), --scope <fixture-dir> (asserts only scoped routes are reported), and the default dry-run (asserts both artefact files appear in docs/generated/). Title: “emits codemod-dry-run.md and codemod-needs-manual.json on every invocation”. For the JSONL emitter, a unit test asserts the emitted entry shape matches the NeedsManualEntry TypeScript type via parse() round-trip.


Scope: Implement the already-wrapped detector per PRODUCT §4. Before rewriting, scan the source file for a top-level defineRoute(...) call inside an export const METHOD = ... assignment. If found, mark the file as SKIPPED and add a // already wrapped entry to the dry-run report (no entry in codemod-needs-manual.json).

File ownership:

  • scripts/codemods/wrap-define-route.ts — add isAlreadyWrapped(sf, method) function called at the start of the per-route rewrite loop.

Dependencies: [10, 11]

Acceptance criteria (PRODUCT.md mapping):

  • AC-3 (idempotency: re-apply on an already-wrapped tree produces no further file changes).

testStrategy: Vitest test against fixture already-wrapped.ts asserts that isAlreadyWrapped() returns true and the rewrite loop produces no file modification (assert via sf.print() vs original text). A second test runs the apply mode twice in sequence against the same fixture and asserts the second run produces zero modifications. Title: “skips routes that already use defineRoute()”; “second consecutive apply run produces no further modifications”.


Subtask 32.14 — Apply mode + format pass

Section titled “Subtask 32.14 — Apply mode + format pass”

Scope: Wire sf.save() into the rewrite loop behind the --apply flag. After all routes have been saved, run bun run format against the modified file set (or invoke ts-morph’s organizeImports() before save) per TECH §8.5. In dry-run mode, sf.save() is skipped; only sf.print() vs original text is reported.

File ownership:

  • scripts/codemods/wrap-define-route.ts — add applyAll(routes) function and runFormatPass(modifiedFilePaths) helper.

Dependencies: [13, 12]

Acceptance criteria (PRODUCT.md mapping):

  • AC-2 (apply mode writes 137 routes; leaves 56 untouched on disk).
  • AC-8 (bun run test passes after apply — no test regressions caused by ts-morph print artefacts).
  • AC-9 (bun lint reports 0 errors after apply).

testStrategy: Vitest test runs wrap-define-route.ts --apply against a temporary copy of the fixture corpus and asserts: (a) each MECHANISABLE fixture file has changed on disk, (b) each NEEDS-REVIEW fixture has had each exported method rewritten, (c) each MANUAL fixture is bit-identical to its original. Title: “writes 137 MECHANISABLE routes to disk under —apply”; “leaves MANUAL routes bit-identical under —apply”.


Subtask 32.15 — Verifier integration with type-drift-detect

Section titled “Subtask 32.15 — Verifier integration with type-drift-detect”

Scope: Implement the verifier integration workflow per PRODUCT §7 / TECH §7. After a successful --apply run, the developer-facing workflow is: (1) run bun run ast-dataflow type-drift-detect --pretty for human review, (2) update docs/generated/type-drift-baseline.json to reflect closed gaps, (3) run bun run ast-dataflow type-drift-detect --ci and confirm exit 0. This Subtask does NOT modify lib/ast-dataflow/queries/type-drift-detect.ts (the verifier is already shipped); it documents the integration in a README.md adjacent to the codemod and adds the workflow as a section in docs/runbooks/ops-t1-codemod.md (new runbook file).

File ownership:

  • scripts/codemods/README.md — new file documenting the post-migration workflow.
  • docs/runbooks/ops-t1-codemod.md — new runbook covering the developer workflow (dry-run → schema authoring → apply → verifier).

Dependencies: [14]

Acceptance criteria (PRODUCT.md mapping):

  • AC-10 (bun run ast-dataflow type-drift-detect --ci passes after the baseline is updated to reflect the closed gaps for the AC-5 routes).

testStrategy: End-to-end integration test against the full fixture corpus: run codemod in dry-run, run apply, run type-drift-detect --pretty, update a stub baseline file, run type-drift-detect --ci, and assert exit 0. Title: “type-drift-detect —ci passes after the codemod applies and the baseline is updated”. The test exercises the verifier’s public CLI interface end-to-end (per test-philosophy §1: behaviour, not implementation).


Subtask 32.16 — Acceptance gate (AC-1..AC-10 end-to-end probe)

Section titled “Subtask 32.16 — Acceptance gate (AC-1..AC-10 end-to-end probe)”

Scope: Author the end-to-end acceptance probe that runs the codemod against the full 193-route corpus (in a working-copy worktree, not the fixture set) and asserts each of AC-1..AC-10 in turn. This Subtask is the final gate before the codemod is considered done. If any AC fails, the diagnosis traces back to a specific upstream Subtask via the Subtask → AC mapping in §4.

File ownership:

  • __tests__/scripts/codemods/wrap-define-route-acceptance.test.ts — new Vitest test file running the full corpus.
  • (Optional) scripts/codemods/acceptance-gate.ts — CLI helper that runs the full corpus + AC checks for use by a developer outside the test harness.

Dependencies: [15]

Acceptance criteria (PRODUCT.md mapping):

  • AC-1, AC-2, AC-3, AC-4, AC-5, AC-6, AC-7, AC-8, AC-9, AC-10 — all 10 ACs asserted end-to-end.

testStrategy: Vitest acceptance test asserts each AC line-by-line; each it() block maps to one AC. Per-AC mode split:

  • AC-1, AC-2, AC-3, AC-4, AC-5, AC-6, AC-7 — read-only audit against the live app/api/ directory in dry-run mode; verifies post-rewrite text via sf.print() without saving any file to disk. Safe to run against the working tree.
  • AC-8, AC-9, AC-10 — apply-against-temp-copy mode. The test copies app/api/ (and lib/validation/schemas.ts, docs/generated/type-drift-baseline.json) into a tmpdir()-scoped fixture corpus, runs the codemod with --apply against the temp copy (NOT the working tree), then runs bun run test + bun lint + type-drift-detect --ci against the temp copy. The temp directory is torn down at test exit. This isolation is required because AC-8 / AC-9 / AC-10 each measure a post-apply state that cannot be obtained from a pure dry-run.

Titles read as product specs: “AC-1: dry-run completes without exception on 193 routes”, “AC-2: apply mode rewrites exactly 137 MECHANISABLE routes”, etc. Per test-philosophy §5. The test runs under the existing Vitest sharding configuration; the temp-copy ACs (AC-8/9/10) must not exceed the 4-shard quality-test job budget — if they do, mark the temp-copy block as a separate integration test under __tests__/integration/.


4a. Per-Subtask records — LIVE Option-4 decomposition ({32.25}/{32.26}/{32.27})

Section titled “4a. Per-Subtask records — LIVE Option-4 decomposition ({32.25}/{32.26}/{32.27})”

This is the authoritative dispatch plan under Option-4. The three Subtasks form a sequential chain (32.25 → 32.26 → 32.27): the contract redesign must land before strictness (the probe needs the pass-through wrapper to run the route suite without B4 double-wrap noise), and both must land before the gate re-finalise (the gate asserts all ten ACs green under the new model). All deps are sibling-only within Task ID-32. The full load-bearing dispatch briefs (file paths, function names, verify-X lines, spec-slice refs) live in the Subtask details fields in docs/reference/task-list.json (owned by the orchestrator) — the records below are the PLAN-level overview + traceability.

Every {32.25}/{32.26}/{32.27} details field MUST repeat the §0 continuous real-corpus probe mandate.

Subtask {32.25} — Redesign defineRoute as a pass-through validator

Section titled “Subtask {32.25} — Redesign defineRoute as a pass-through validator”

Scope: Redesign lib/api/define-route.ts from the S11 payload-returning contract to the Option-4 pass-through contract per TECH §2.4a. The wrapper calls the handler, then branches on the return value: a returned Response/NextResponse is passed through with status / headers / redirects / streaming + non-JSON bodies UNCHANGED, validating ONLY the cloned JSON body of 2xx application/json responses (INV-PT); a raw (non-Response) payload is validated then NextResponse.json-wrapped (the polymorphic minority arm). The failure policy (INV-FP, TECH §2.4a.2) is fail-open in production, LOUD in dev + CI + test — LOUD when NODE_ENV !== 'production' OR process.env.CI is set (so a CI runner with NODE_ENV=production + CI=true FAILS LOUD), fail-open ONLY when in production AND not CI. The S11 “returns a 500 envelope on mismatch” test is reworked to assert the new split (throw under NODE_ENV=test; original-response + logged drift under NODE_ENV=production). Drop / repoint the dead silent-failure-prevention-spec.md reference in the file header (TECH §12).

File ownership:

  • lib/api/define-route.ts — pass-through redesign (adds the canonical logger value import from @/lib/logger, direct file import per the no-barrel rule; keeps z type-only).
  • __tests__/lib/api/define-route.test.ts — rework the 500-envelope test to the INV-PT pass-through + INV-FP environment-split assertions.

Dependencies: [] (root of the Option-4 chain).

Implements: INV-PT + INV-FP (PRODUCT §8.0). Reinterprets AC-8 (pass-through, not payload-returning).

Continuous probe: runs the §0 real-corpus probe to prove the redesign does NOT regress the route unit suite UNDER pass-through (the B4 reinterpretation).

testStrategy (one-line acceptance): define-route.test.ts proves the pass-through split — a handler returning NextResponse.json({error},{status:401}) yields that exact 401 unchanged (not a re-wrapped 200, not a 500); a matching 2xx JSON body passes unchanged; a 3xx / 204 / text/event-stream passes through with no schema parse; a drifting 2xx body throws under NODE_ENV=test and returns the original + logs under NODE_ENV=production; the §0 probe (vitest run __tests__/api on a temp-copy apply) shows no regression under pass-through (verifies real behaviour per docs/reference/test-philosophy.md).

Subtask {32.26} — Tighten the R-WP17 response schemas to strict (INV-S)

Section titled “Subtask {32.26} — Tighten the R-WP17 response schemas to strict (INV-S)”

Scope: Rework scripts/codemods/generate-response-schemas.ts to derive strictness from the real source interfaces per TECH §3.1a, then regenerate the managed block in lib/validation/schemas.ts (the BEGIN/END generated: R-WP17 ResponseSchema constants (ID-32.20) block, currently lines 2314–3334). Eliminate the 84 .loose() + 10 z.unknown() in that block: plain object literals → zod-4 default z.object({...}) (strips additive wire fields, REJECTS a renamed/removed/retyped declared field — NOT z.strictObject, which would 500 on a legitimately-added field); string-literal unions → z.enum/z.literal; named-interface refs → recurse into the referenced type. Retain z.unknown() ONLY for genuinely un-narrowable Json/opaque-DB/ external-generic properties, and .loose() ONLY where the source declares a real [k: string]: T index signature — each such exception recorded on the machine-checkable allow-list (per §3.1a) citing the source property. Empirically ~zero R-WP17 interfaces have a genuine index signature, so the .loose() exception should fire essentially never. Corpus is on zod ^4.4.3 — use zod-4 method names (TECH §12).

File ownership:

  • scripts/codemods/generate-response-schemas.ts — strictness rules + allow-list emission.
  • lib/validation/schemas.ts — regenerated BEGIN/END generated block (~2314–3334).

Dependencies: [25] (the tightened schemas must run UNDER the {32.25} pass-through + INV-FP loud-in-test policy so newly-rejected drift surfaces as a loud test failure, not a silent prod 500).

Implements: INV-S (PRODUCT §8.0). Tightens AC-5 (real binds must additionally be STRICT — binding itself already shipped via 32.20/32.21/32.22).

Continuous probe: runs the §0 probe to surface any pre-existing real drift the tightened schemas now reject (S262 OQ-1), under INV-FP loud-in-test.

testStrategy (one-line acceptance): a static check over the BEGIN/END generated block asserts zero .loose() and zero z.unknown() except allow-listed entries each citing a real index-signature or opaque-Json/external source property; a runtime check proves a schema with a renamed declared field (item_countitemCount) now FAILS safeParse where the pre-amendment .loose() schema passed it; the §0 probe stays green (any surfaced drift is a loud test failure to be triaged, not a silent pass).

Subtask {32.27} — Re-finalise the 32.16 acceptance gate under the Option-4 model

Section titled “Subtask {32.27} — Re-finalise the 32.16 acceptance gate under the Option-4 model”

Scope: Recover the acceptance test pair from tag s262-32.16-acceptance-gate, flip the it.fails(...) placeholders to live assertions, and re-finalise the gate so all of AC-1..AC-10 (PRODUCT §8.1) are GREEN under the Option-4 model. Retire the now-stale escalation canary (the assertion that apply ABORTS on withRequestContextBare: the B1 fix means apply no longer aborts, so that canary is dead — TECH §12). Add the AC-9 tsc/no-undef gate (a tsc --noEmit / no-undef check proving every emitted defineRoute(...) has its z + schema imports resolved — the gate that would have caught B3, which ESLint missed) and the INV-S static check (the allow-list-justified .loose()/z.unknown() assertion from {32.26}).

File ownership:

  • __tests__/scripts/codemods/wrap-define-route-acceptance.test.ts (and/or the integration acceptance test under __tests__/integration/) — recovered from s262-32.16-acceptance-gate, un-it.fails’d, canary retired, AC-9 tsc/no-undef
    • INV-S static checks added.

Dependencies: [25, 26] (the gate asserts the pass-through contract AND the strict schemas; both must be in place for all ten ACs to go green).

Implements: the full AC-1..AC-10 matrix under the Option-4 model; closes the de-vacuum’d AC-9 (lint-delta + tsc/no-undef) and the INV-S static check.

Continuous probe: the §0 probe is the AC-8 oracle here — the gate’s green-on-all-AC closing assertion IS the route-unit-suite pass under pass-through.

testStrategy (one-line acceptance): the recovered acceptance suite runs the codemod against a temp-copy of the live app/api corpus and asserts each of AC-1..AC-10 green under the Option-4 model — AC-7 EXACT-callee match (no withRequestContextBare false-positive), AC-8 route suite green under pass-through, AC-9 lint-delta after ≤ before AND tsc/no-undef clean (no B3 ReferenceError), plus the INV-S static check — with the stale escalation canary removed and no it.fails remaining.


5a. Subtask-to-AC / invariant traceability matrix — LIVE (Option-4)

Section titled “5a. Subtask-to-AC / invariant traceability matrix — LIVE (Option-4)”

Authoritative under Option-4. The §5 matrix below is the SUPERSEDED S11/S62C mapping for the historical 32.5–32.16 decomposition.

Invariant ownership (PRODUCT §8.0):

SubtaskINV-PTINV-FPINV-S
{32.25} pass-through defineRoute
{32.26} strict R-WP17 schemas
{32.27} acceptance gate re-finalise

AC ownership (PRODUCT §8.1, reframed):

SubtaskAC-1AC-2AC-3AC-4AC-5AC-6AC-7AC-8AC-9AC-10
{32.25} pass-through defineRoute
{32.26} strict R-WP17 schemas
{32.27} acceptance gate re-finalise

Legend: = primary owner; = verifies / asserts (the gate proves the invariants hold end-to-end); = not covered.

Dependency chain (sibling-only, all within Task ID-32):

{32.25} pass-through defineRoute (INV-PT + INV-FP) deps = []
└──> {32.26} strict R-WP17 schemas (INV-S) deps = [25]
└──> {32.27} acceptance gate re-finalise deps = [25, 26]
(all AC-1..AC-10 green; AC-9 tsc/no-undef + INV-S check)

The mechanics from the SUPERSEDED 32.5–32.16 chain (classify → infer → wrap → emit; §§2–4) are REUSED by {32.25}–{32.27}; only the contract, strictness, and AC framing changed. No cross-Task Subtask dependency is introduced (the Task-level dep on ID-31 remains at the Task record per §7).


4b. ID-50 corpus-rollout decomposition — LIVE (S267, the working-tree rollout)

Section titled “4b. ID-50 corpus-rollout decomposition — LIVE (S267, the working-tree rollout)”

Authoritative dispatch plan for Task ID-50 (the working-tree corpus rollout that ID-32 deliberately deferred). Authored by the ops-rollout sub-orchestrator (S267) from the {50.1} assessment (ops-t1-codemod/ASSESS-S267.md). PRODUCT/TECH inherit unchanged from the ratified OPS-T1 specs — no new product surface. The load-bearing per-Subtask dispatch briefs live in the details fields of ID-50 in task-list.json; the records below are the PLAN-level overview + sequencing rationale. Every wave repeats the §0 continuous real-corpus probe mandate.

Premise (gate-proven). Full --apply to the working tree = ~369 tsc errors = 118 TS2345 ctx-contravariance + the test-call-site signature bulk + 24 runtime RSVE (7 files). All mechanical; working tree is currently GREEN with zero routes wrapped.

Scope. 195 routes → 132 TRANSFORM + 45 NEEDS_REVIEW = 177 wrapped; 18 MANUAL excluded by the codemod. Retirement pre-filter carves out app/api/upload/route.ts (§12.2 STILL-OPEN) → net 176. digestchange-reports + bidprocurement renames confirmed landed; source-documents/[id]/diff RETAINED-v1 (§12.1).

Decomposition (sibling-only deps, NUMBERS):

SubtaskScopedepsGreen outcome
{50.3}Generic defineRoute ctx type (Design B variadic-tuple primary / A overload-pair fallback; decide on temp-copy residual) — resolves 118 TS2345[]define-route.test.ts + temp-copy probe: zero residual ctx TS2345 over migrated routes
{50.4}Codemod-assisted test-call-site migration tooling (inject createTestRequest, derive path from import)[3]tool validated on temp copy; no working-tree wrapping
{50.5}Wave A — items+workspaces+layers+tags (~33) wrap + migrate[3,4]scoped tsc+next build+probe green
{50.6}Wave B — procurement (29) wrap + migrate[3,4]scoped tsc+next build+probe green
{50.7}Wave C — intelligence+review+governance (~29) wrap + migrate[3,4]scoped tsc+next build+probe green
{50.8}Wave D — admin+taxonomy+entities+coverage (~38) wrap + migrate[3,4]scoped tsc+next build+probe green
{50.9}Wave E — long-tail remainder (~47, EXCL upload) wrap + migrate[3,4]scoped tsc+next build+probe green
{50.10}Reconcile the 24 RSVE strictness-drift (7 named files; fix shapes, NEVER .loose())[6,7]route suite runs LOUD-clean under strict schemas
{50.11}Close — all-176-wrapped verify + AC-10 type-drift-detect --ci baseline-to-zero + document upload carve-out[5,6,7,8,9,10]full tsc+next build+knip+bun run test + AC-10 exit 0

Waves {50.5}–{50.9} own disjoint app/api/<seg> + __tests__/api/<seg> file sets → parallel-eligible once {50.3}+{50.4} land; the orchestrator cherry-picks sequentially.

Sequencing constraint (audit coordination — RATIFIED S267, OQ-50.2-B option a): ID-50’s call-site migration is the canonical first mover. It is a compile-correctness prerequisite, so the s37 test-audit’s W-RC/W-RD api-tree assertion rewrites MUST follow ID-50’s migration on the green tree (or rebase onto ID-50’s wrapped files) — a wrapped route’s test does not type-check until migrated, and W-RD cannot rewrite assertions in a non-compiling file. The s37 audit is NOT an active terminal as of S267 (live terminals: ID-9, ID-34/35, ID-50, ID-51) → no live parallel collision; this ordering is the standing rule whenever the audit is next scheduled.


5. Subtask-to-AC traceability matrix — SUPERSEDED (S11/S62C historical)

Section titled “5. Subtask-to-AC traceability matrix — SUPERSEDED (S11/S62C historical)”

SUPERSEDED-S265. The LIVE matrix is §5a. This matrix maps the historical 32.5–32.16 decomposition to the pre-amendment 10-AC framing; retained for the record of what the shipped mechanics covered.

SubtaskAC-1AC-2AC-3AC-4AC-5AC-6AC-7AC-8AC-9AC-10
32.5 scaffold + defineRoute
32.6 classifier
32.7 fixtures
32.8 inference Source A
32.9 inference Source B (opt)
32.10 rewrite single-method
32.11 rewrite multi-method
32.12 CLI + emitters
32.13 idempotency
32.14 apply + format
32.15 verifier integration
32.16 AC gate

Legend: = primary owner; = partial / prerequisite; = not covered.

Every AC has at least one primary owner upstream of 32.16. 32.16 acts as the final integration gate covering all 10.


6. Open Questions (OQ) for Liam ratification

Section titled “6. Open Questions (OQ) for Liam ratification”

OQ-1 — defineRoute() wrapper authoring scope (HIGH priority)

Section titled “OQ-1 — defineRoute() wrapper authoring scope (HIGH priority)”

PRODUCT §9 declares lib/api/define-route.ts out of scope for the codemod itself but states “this must be authored separately as part of the OPS-T1 implementation workpackage, before the codemod is run.” TECH.md and AC-1..AC-10 assume the wrapper exists at the point the codemod’s fixture tests run.

The wrapper does not exist in the current codebase (verified via ls lib/api/define-route.ts on the worktree at the time of PLAN authoring).

Planner recommendation: Fold the wrapper authoring into Subtask 32.5 as written. Rationale: (a) the wrapper is a self-contained ~50-100 LOC module, (b) waiting on a separate task would block all 11 downstream Subtasks, (c) the codemod tests cannot pass without it. The “out of scope” wording in PRODUCT §9 reflects a codemod-vs-wrapper-design boundary, not a scheduling boundary.

Alternative: Liam creates a new Task ID-32a (or pulls forward an existing backlog item) that authors the wrapper as a precondition; ID-32 then strictly implements the codemod against the existing wrapper. This is cleaner per spec-boundary discipline but adds one Task-level dependency and one extra dispatch cycle.

Decision needed before Subtask 32.5 dispatches.

OQ-2 — Source C inference scope (LOW priority)

Section titled “OQ-2 — Source C inference scope (LOW priority)”

TECH §3 recommends deferring Source C (handler return-statement walk). PLAN as drafted excludes Source C entirely. Liam may want to schedule a follow-on Task for Source C if the post-apply z.unknown() placeholder count proves higher than expected.

Planner recommendation: Confirm Source C remains out of scope for this PLAN cycle; if Source C is needed, surface as a separate backlog item post-32.16 acceptance.

Alternative: Add Subtask 32.9b authoring Source C inline; raises Subtask count to 13 implementation + 4 spec = 17 (still within 25-ceiling), adds ~2-3h, but reduces the z.unknown() count for non-baseline routes.

Decision needed before: Subtask 32.16 acceptance gate dispatch (post-apply placeholder count is the data point that retroactively validates or invalidates the Source-C-defer choice).

OQ-3 — bun lint baseline assumptions (MEDIUM priority)

Section titled “OQ-3 — bun lint baseline assumptions (MEDIUM priority)”

AC-9 requires bun lint to report 0 errors after --apply. This presumes the current bun lint baseline is already 0. If there are pre-existing ESLint errors in app/api/**/route.ts (e.g. from the local/no-unchecked-supabase-error rule), the codemod’s apply pass could surface them as new “regressions” against the AC-9 gate when they were pre-existing.

Planner recommendation: Subtask 32.16’s AC-9 check uses a pre-apply baseline vs post-apply diff, not an absolute 0-error assertion. Confirm with Liam whether the AC-9 acceptance criterion should be reinterpreted as “no new lint errors introduced by the codemod” rather than “absolute 0 lint errors”.

Alternative: Treat AC-9 as written (absolute 0-error assertion); Subtask 32.5 then carries a prereq slice fixing the existing bun lint baseline to 0 before any codemod rewrite lands. Adds unbounded scope to 32.5 (depending on pre-existing lint debt) and may itself fork into a separate Task.

Decision needed before: Subtask 32.16 testStrategy authoring (diff-vs-absolute framing determines the assertion shape and the allowed-failures bucket for AC-9).


Every Subtask dependency — in both the SUPERSEDED §4 chain (32.5–32.16) and the LIVE §4a Option-4 chain ({32.25} deps [] → {32.26} deps [25] → {32.27} deps [25, 26]) — references another Subtask integer ID within Task 32 only. No Subtask references any external Task (e.g. ID-31, ID-30). The Task-level dependencies: ["31"] on ID-32 captures the canonical-pipeline prerequisite at the appropriate level; no Subtask-level cross-Task dep needed. (Per PRODUCT §9 / TECH §10, the DB-layer warp-analog is the SEPARATE Task ID-47 — it is NOT pulled into ID-32’s Subtask space, so no cross-Task Subtask dep arises there either.)

Cross-Task dependency surface verified clean for both chains. No escalation required at this layer.


SubtaskEstimate (hours)Parallelisable with
32.5 scaffold + defineRoute wrapper2-3
32.6 classifier1-2
32.7 fixtures (14)2-3
32.8 inference Source A2-332.9, 32.12
32.9 inference Source B (optional)1-232.8, 32.12
32.10 rewrite single-method3-432.11
32.11 rewrite multi-method2-332.10
32.12 CLI + emitters2-332.8, 32.9
32.13 idempotency1-2
32.14 apply + format1-2
32.15 verifier integration1-2
32.16 AC gate2-3
Total20-32 h

Task record’s existing estimate is 16-24h. The PLAN total (20-32h) is slightly higher because OQ-1’s fold-in of defineRoute() wrapper authoring adds ~2-3h that the original estimate did not include. Liam may wish to adjust the Task estimate after OQ-1 resolution.

Parallel execution across 32.8/32.9/32.12 and 32.10/32.11 could compress wall-clock time by ~4-5h if multiple Executors are available.


  • ops-t1-codemod/PRODUCT.md — user-facing behaviour, 10 ACs.
  • ops-t1-codemod/TECH.md — ts-morph implementation strategy, 14 fixtures, 6 gotchas.
  • ops-t1-codemod/route-shape-inventory.md — 193 routes empirical inventory.
  • docs/reference/test-philosophy.md — 6 audit criteria governing testStrategy lines.
  • docs/reference/task-list.json ID-32 — Task record (PLAN.md update lands via sub-orchestrator after Checker pass).
  • lib/ast-dataflow/queries/type-drift-detect.ts — verifier (already shipped).
  • docs/generated/type-drift-baseline.json — 37-interface baseline (Source A input).