think
16px
820px

Migration Implementation Phases — brainstorm v1

Date: 2026-07-09 · Companion to: 2026-07-09-pt-flow-v2-migration-plan.md (the what/why ledger; this doc is the how/when)
Priority frame (Efran): 1. All PP flows → 2. All Apostille/Legalisasi → 3. All PT flows.


0. The central tension this plan resolves

Fresh code exploration (2026-07-09, both processors read end-to-end) surfaced an inconvenient truth:

Business priority is inversely ordered from engine fit.

Family Priority Fit with today's FlowConfig/runFlow Why
PP #1 Worst No akta (synthetic carrier docs), BO roster build/rematch, jenis/section-gated rule subsets, konfirmasi/expiry lifecycle — none expressible today. Only the 39 rule bodies (+ persistence + a future buildPpContext) are drop-in wrappable.
Apostille #2 Middle Pipeline is engine-shaped except one thing: a user gate mid-pipeline (AWAITING_CLASSIFICATION between classify and extract). 13 live rules are pure/wrappable. AP-vs-LGL divergence is a single category-eligibility predicate.
PT #3 Best The engine was literally built around PT-akta. Two shadows already pass validation parity; two more flows exist fully-built on the parallel session.

Resolution: don't read "PP first" as "PP processing-engine migration first." Split every family's work into the two axes (A = processing engine, B = review projection) and spend where the value is:

  • PP #1 is served immediately by Axis B (review projections — the proven, cheap strangler; 4 tenants already flipped) plus targeted Axis-A seam design that PP forces anyway and Apostille reuses.
  • The heavy PP Axis-A flips happen only after the seams exist and are proven on the simplest PP flow.
  • PT Axis-A work (the easiest, most-prepared migrations) fills any slack and lands last per priority — but its harness (full-pipeline parity oracle) is built early because PP/Apostille flips need it too.
flowchart LR P0["PHASE 0
Prove engine +
build harness"] P1A["PHASE 1A
PP review projections
(completes PP Axis B)"] P1B["PHASE 1B
Engine seams for
non-akta flows"] P1C["PHASE 1C
PP context + rules
+ shadows"] P1D["PHASE 1D
PP flips
(easiest → hardest)"] P2["PHASE 2
Apostille
(reuses 1B seams + gate)"] P3["PHASE 3
PT ports + flips
+ Penggabungan"] P0 --> P1A P0 --> P1B --> P1C --> P1D --> P2 --> P3 P1A -.-> P1D

Phase 0 — Prove the engine, build the shared harness (prerequisite for everything)

Goal: the v2-generic engine goes from "plausible" to "proven," and the migration tooling that every later flip depends on exists.

# Task Detail Size
0.1 Smoke Peleburan + Laporan RUPS (Efran) The only never-driven v2 tenants — the sole evidence gap on the engine itself. Fix whatever falls out. Everything downstream assumes this is green. S (test) + ? (fixes)
0.2 Full-pipeline parity harness v2 Extend flow-parity.test.ts beyond ValidationResult rows. Diff surface per cloned-submission pair: (a) ValidationResult rows (already covered); (b) ExtractedField rows after rematch; (c) Submission status/error transition sequence; (d) SubmissionChecklist rows; (e) IdentityMatch rows. Keep the prime directive: red oracle = engine-plumbing bug, fixed in the engine, never by weakening the diff. M
0.3 Decouple multi-source lookup Replace runFlow's hardcoded import("../services/peleburan-sources") with companyLookup.resolve?: (submissionId) => Promise<void> per the inline TODO in processor.ts. Pure refactor; peleburan parity re-run proves it. Unblocks Penggabungan (3D) later. S
0.4 (optional, defer-able) Fold Peleburan/RUPS bespoke review-data routes into REVIEW_REGISTRY Cleanup only — they predate the strangler. Do it opportunistically when touching those flows. S

Gate to Phase 1: 0.1 green (Efran-approved), 0.2 merged.


Phase 1 — PP family (priority #1)

1A — Finish PP Axis B (review projections) — start immediately, parallel with Phase 0

The strangler is proven (Pendirian PP, Pembubaran PP, Pendirian PT flipped; Peralihan PP→PT flip in flight). Two PP flows remain, and they're already "next up" in tidyup/tasks.md Phase B:

# Task Notes Size
1A.1 PERUBAHAN_PP projector + V2 page Projector reads carrier doc (PP_PERUBAHAN_CARRIER) + oldData snapshot + jenis-gated sections + BO entries + validations. Copy the pp-pendirian projector pattern (review/projectors/pp-pendirian-review.ts). The jenis-selection step stays route-owned; the projector only reads. M
1A.2 PERBAIKAN_DATA_PP projector + V2 page Near-clone of 1A.1 (the processors are near-clones — string section keys instead of numeric PpJenis). Do second, harvest 1A.1's structure. S–M
1A.3 Efran smoke → flip → retire legacy pages + their /review-data endpoints Same flip discipline as the four precedents (canonical /review, /review-v2 redirect, delete legacy). S per flow

Exit: every PP flow reads through REVIEW_REGISTRY. PP Axis B complete.

1B — Engine seams for non-akta flows (the design phase — do once, reuse for Apostille)

Exploration verdict: customProcess could technically host PP flows but "largely defeats the engine's purpose." These four seams make PP (and Apostille) first-class tenants instead. Design all four together (one spec), implement incrementally:

Seam What Consumed by Sketch
S1 — Primary-doc generalization + carrier seeding primaryAkta → nullable already works (peleburan); add hooks.seedDocuments?(submissionId) that runs before the extraction loop — hosts Perubahan/Perbaikan PP's synthetic-carrier creation + LOCKED snapshot-scalar writes PERUBAHAN_PP, PERBAIKAN_DATA_PP new optional hook; no behavior change when unset
S2 — Context-gated rules rules: Rule<Ctx>[] \| ((ctx: Ctx) => Rule<Ctx>[]) — jenis/section-selection picks the active subset (ruleCodesForJenis today) PERUBAHAN_PP, PERBAIKAN_DATA_PP union type; validation-runner resolves before running; delete-stale scope = the full code list so deselected rules clean up
S3 — Post-edit re-derive steps PP's BO rematch on identity edits. RematchConfig.preValidationSteps already exists — verify it fires on the field-edit path, extend if it only fires on rematch PENDIRIAN_PP, PERUBAHAN_PP mostly wiring, possibly zero new engine surface
S4 — Pause/resume gates Generalize peleburan's AWAITING_COMPANY_SELECTION pattern: gates?: [{ status, isCleared(submissionId), onResume }] — a first-class "park the run, resume when cleared" seam APOSTILLE (AWAITING_CLASSIFICATION) — design now, implement in Phase 2 the peleburan branch in runFlow becomes the first internal consumer

Deliberately NOT a seam: Pembubaran PP's konfirmasi/expiry lifecycle (MENUNGGU_KONFIRMASI → 7-day expiry → KEDALUWARSA). That's a finalize handshake, not processing — it stays route-owned forever. The engine's job ends at READY/VERIFICATION_READY; what happens at submit/finalize is already per-flow route territory (precedent: verifikator staging vs terminal flows).

# Task Size
1B.1 Seam spec (one doc, all four seams, with parity-oracle implications) S
1B.2 Implement S1 + S2 (+S3 verification) behind unset-by-default options; all existing tenants byte-identical (parity re-run) M

1C — PP context + rule wrapping + shadows

# Task Detail Size
1C.1 buildPpContext Per exploration: submission + docs + identityMatches + ordered pemilikManfaatEntries + resolved founder-BO matched-doc ids + field maps (SP/carrier, owner KTP, owner NPWP) + KBLI rows/master set + parsed modal + name-availability + oldData as PpRegistrySnapshot + resolveOwnerDocId helper. One builder, per-flow narrowing (like PtAktaCtxPeleburanCtx). M
1C.2 Wrap the 39 PP rule bodies into Rule<PpCtx> sets Bodies already return the engine's RuleResult; wrappers project scattered args from ctx. Add a wrapper-fidelity-style guard test (wrap-time typo protection). M
1C.3 Shadow configs + parity oracle per flow pp-pendirian.shadow.ts, pp-perubahan.shadow.ts, pp-perbaikan.shadow.ts, pp-pembubaran.shadow.ts — fork vs shadow on cloned fixtures, full-pipeline diff (Phase 0.2 harness). M–L

1D — PP flips (easiest → hardest)

Order Flow Why this order Extra work
1 PEMBUBARAN_PP Simplest: 4 rules, KTP-only, no BO/carrier/KBLI. Needs the standalone reprocess entrypoint anyway (dispatch gap — currently "none"). Konfirmasi stays in routes (see 1B). reprocess entrypoint
2 PERUBAHAN_PP + PERBAIKAN_DATA_PP Near-clones; exercise S1 (carrier) + S2 (jenis gating) together. Collapse the FF4-grandfathered fork pair (per tasks.md). S1+S2 consumers
3 PENDIRIAN_PP Largest (892-line processor), BO-heavy; goes last with all seams proven. S3 consumer
4 PERALIHAN_PP_KE_PT Akta-bearing (closest to PT shape) but bespoke chain + PP-asal bolt-on (match/picker/finalize-gate). Candidate to slide into Phase 3 with the PT wave if it drags — decide at the time. PP-asal as rules + hook

Each flip = shadow parity green → registry flip → delete fork processor → collapse route → Efran smoke → next.

Exit criteria for "All PP flows" (priority #1) — per Efran's Full-A+B ruling (2026-07-09): Axis B all flipped (1A) and Axis A all four/five tenants on v2-generic with their fork processors deleted (1D). Both are required; PP is not declared done on projections alone.


Phase 2 — Apostille / Legalisasi (priority #2)

Exploration facts that make this cheaper than feared: AP vs LGL is one predicate (category eligibility — LGL=Perdagangan-only, AP=everything-else), not a pipeline fork; the 13 live rules are pure and wrappable; per-doc classify→review→extract is the only structural novelty; the legacy single-phase processApostilleSubmission is dead code.

# Task Detail Size
2.1 Delete dead processApostilleSubmission (single-phase path) Early, independent cleanup — shrinks the migration surface. S
2.2 Implement S4 (pause/resume gates) in the engine Peleburan's AWAITING_COMPANY_SELECTION branch becomes the first internal consumer (proves no-regression via parity); apostille's AWAITING_CLASSIFICATION becomes the second. M
2.3 ApostilleCtx + wrap 13 rules Applicant (KTP-projected) + DocView[] + account-token NIK. Bespoke runner retires in favor of validation-runner. M
2.4 Apostille FlowConfig primaryAkta: null, peer-doc loop, phase-1 classify via hooks or gate config, applicationType-keyed category predicate + labels. Shadow + parity + flip + delete apostille-processor.ts. M–L
2.5 Apostille Axis B (review projection) Projector must express per-doc sections — check the ReviewData contract fits a doc-board shape; extend the contract if not (it's zod-versioned, additive). M
2.6 Jalur prioritas — requirements capture only Confirmed absent from the entire codebase (nothing to migrate). When in scope: study ahu-rebuild-apostille (ahu-apostille-api + module-apostille — the rebuild has a "Data Prioritas" nav entry, a starting thread) + SABH-side rules; write a spec. No implementation now. S (doc only)

Exit: Apostille runs as a v2 tenant with the gate seam; prioritas spec'd but unbuilt.


Phase 3 — PT family (priority #3)

Cheapest per-flow work in the whole plan (engine is PT-shaped, harness exists by now, donors exist for everything except Penggabungan).

# Task Detail Size
3.1 Port PEMBUBARAN_PT Reconcile-not-overwrite from feat/pt-flows-registry (284 commits of interface drift: focusedPasses, allowedStatusTransaksi, failGate, rematch.rupsAttendance). Registry stub + dispatch branch already scaffolded here. Flip BUILT_AKTA_TXN_TYPES +'pembubaran'. M
3.2 Port BERAKHIRNYA_STATUS_PT Enum member first (absent here), then port; depends on 3.1 (BSBHP_REGISTRY_STATE reads approved-pembubaran snapshot; shares likuidasi extractors). Flip +'berakhirnya'. M
3.3 Flip PERUBAHAN_PT + AKUISISI_PT Shadows exist, validation parity green; run full-pipeline parity (0.2), smoke Akuisisi BEFORE flipping (never driven), flip, delete perubahan-processor.ts/akuisisi-processor.ts, absorb the 1763-line route into the route family. L (route absorption dominates)
3.4 Flip PENDIRIAN_PT + PERBAIKAN_DATA_PT Build shadows → parity → flip → delete submission-processor.ts/perbaikan-processor.ts. Pendirian is the oldest, most-encrusted fork — do it after 3.3's lessons. L
3.5 Build PENGGABUNGAN (new) No donor anywhere. Model on Peleburan (multi-PT → surviving PT; multi-source lookup now generic via 0.3). New enum + FlowConfig + context + rules + routes + review projection + classifier launch-switch. Spec first — SABH id_aksi_transaksi=11 semantics (absorbed PTs end, survivor amends AD). L
3.6 Endgame cleanup Delete processorKindForType switch (all v2), merge FLOW_REGISTRY + REVIEW_REGISTRY (the review-registry header's stated end-state), retire shadow configs whose forks are gone (shadows become the registered configs). M

Cross-cutting discipline (applies to every phase)

  1. TDD + equivalence oracles — the repo's stated rule: every fork collapse passes the old fork's suite as an oracle before deletion. Never fix a red oracle by weakening the diff.
  2. Efran smoke = hard gate per flip — matches the 4 review-projection precedents. Nothing flips dark-to-live without it. Flows stay dark behind BUILT_AKTA_TXN_TYPES / registry stubs until their gate passes.
  3. Dormant-until-flip — every seam lands unset-by-default; existing tenants prove byte-identical via parity re-runs before any new consumer registers.
  4. One session per flip — flips are small and reviewable (registry swap + deletions); the heavy lifting (shadows, wrappers, context builders) lands earlier as additive, low-conflict commits. This respects concurrent sessions (the Peralihan flip is happening in one right now).
  5. Fat-route absorption is its own workstream — deleting a fork processor ≠ deleting its route. The 1500–1800-line route files (perubahan, pp-perubahan, pp-perbaikan) shrink incrementally as generic route families absorb endpoints; don't block registry flips on route collapse.

Sizing summary (rough, S<1d · M=1–3d · L=1–2wk of focused agent work)

Phase Total Dominated by
0 ~1 wk full-pipeline parity harness
1A ~1 wk two projectors + pages + flip ceremony
1B–1C ~2 wk seam design/impl + PP context + 39 wrappers
1D ~1.5 wk four flips + reprocess entrypoint
2 ~2 wk gate seam + apostille config + projection
3 ~3–4 wk route absorption + pendirian + Penggabungan build

Decisions

  • RESOLVED (Efran, 2026-07-09): Scope of "All PP flows" = Full A+B. PP is not "done" until all Axis-B projections are flipped and every PP fork processor is deleted with its flow running v2-generic (1A and 1B–1D). Phase 1 runs in full before Phase 2.
  • · Peralihan PP→PT Axis A: PP wave (1D.4) or PT wave (Phase 3)? Decide when 1D.3 completes, based on how PT-shaped the seams turned out. (Given the Full-A+B ruling, default leans 1D.4 — it counts toward "all PP flows.")
  • · Apostille review projection (2.5) before or after its processing flip (2.4)? Default: projection first (Axis B is lower-risk and gives the smoke surface for the flip).
  • · Phase 0.4 (fold Peleburan/RUPS review-data into the registry): opportunistic or scheduled? Default: opportunistic.

Task briefs for Phase 0 + 1A live in tidyup/tasks.md ("Phase C — Flow-engine migration"). Later-phase briefs get written as their predecessors near completion.