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.
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 PtAktaCtx → PeleburanCtx). |
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)
- 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.
- 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. - Dormant-until-flip — every seam lands unset-by-default; existing tenants prove byte-identical via parity re-runs before any new consumer registers.
- 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).
- 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.