PT Flow Migration Plan — old-style → v2-generic, + legacy ports
Date: 2026-07-09 · Branch context: refactor/tidyup-foundation (tidy) vs feat/pt-flows-registry (legacy checkout of ahu-ocr-akta-notaris)
0. TL;DR
Current priority (Efran, 2026-07-09): 1. All PP flows → 2. All Apostille/Legalisasi flows → 3. All PT flows. This inverts the PT-first framing of the original draft — sequencing in §5 below is now ordered accordingly.
Three kinds of work:
- Transform old-style → new-style — flows that run on their own bespoke, hand-written processor (
v1-fork) get rebuilt as a declarativeFlowConfigfed to the one sharedrunFlowengine (v2-generic). Behaviour must be byte-identical, proven by a parity oracle, then smoke-tested. - Port legacy-only flows —
PEMBUBARAN_PTandBERAKHIRNYA_STATUS_PTwere already built asv2-genericonfeat/pt-flows-registry, but that branch is 284 commits behind tidy, so they need interface reconciliation before landing. - Build a genuinely missing flow —
PENGGABUNGAN(merger) is a real SABH transaction type (id_aksi_transaksi=11, confirmed §2.1) that no branch has ever built. Not greenfield speculation — a confirmed gap.
Confirmed truly out of scope: PEMISAHAN (spin-off) is not a real SABH transaction type — no id_aksi_transaksi corresponds to it anywhere in TransaksiPerseroan::getJenisTransaksi(). There is nothing to migrate or build; drop it from consideration entirely (see §2.1).
1. Why "Peleburan / Laporan RUPS" are v2-generic but "Pendirian / Perubahan / PP" are old-style
This trips people up, so state it plainly: the label is about which execution engine the flow runs on — not how mature, correct, or well-tested it is.
Two engines live in the codebase right now
=== 'v2-generic' ?"} end Q -->|"YES"| GEN Q -->|"NO (v1-fork)"| FORK subgraph GEN["v2-generic — ONE shared engine"] direction TB R["runFlow(id, cfg)
flow-engine/processor.ts"] C["FlowConfig (data, not code)
defineFlow({ primaryAkta, companyLookup,
extraction, rules[], sections[], rematch, hooks })"] C -. "config drives" .-> R R --> GT["tenants: PELEBURAN_PT · LAPORAN_RUPS_TAHUNAN"] end subgraph FORK["v1-fork — one bespoke processor PER flow"] direction TB P1["submission-processor.ts (PENDIRIAN_PT)"] P2["perubahan-processor.ts (PERUBAHAN_PT)"] P3["akuisisi-processor.ts (AKUISISI_PT)"] P4["pp-*-processor.ts (PP family)"] P5["apostille-processor.ts (2-phase)"] end
- v1-fork ("old-style"): each flow is imperative code. It has its own dedicated processor (
perubahan-processor.ts,pp-perubahan-processor.ts, …) and its own fat route (routes/perubahan.ts= 1763 lines,routes/pp-perubahan.ts= 1668 lines).dispatchProcessorhard-switches on theSubmissionTypeto the matching function. Every flow re-implements the same pipeline (extract → company lookup → rematch → validate → sections → finalize), so cross-flow consistency drifts and every fix is copy-pasted N times. - v2-generic ("new-style"): the flow is declarative data. A
defineFlow({...})config object (FlowConfiginflow-engine/types.ts) describes the flow; the singlerunFlow()engine executes it.dispatchProcessorshort-circuitsif (entry.engine === 'v2-generic') return runFlow(...)before the per-type switch. Routes are thin (peleburan.ts= 398,laporan-rups.ts= 229) because the generic engine + generic route families carry the weight.
The apparent paradox — "I've smoke-tested the old-style ones, never the new-style ones"
That's expected, because "new-style" ≠ "more proven." There are two independent axes:
| Axis | Pendirian / Perubahan / PP family | Peleburan / Laporan RUPS |
|---|---|---|
| Architecture | old-style (bespoke v1-fork) |
new-style (declarative v2-generic) |
| Maturity / real-world smoke | high — oldest flows, Efran-driven | low — never smoke-tested |
They're inversely correlated right now, and that's the normal shape of a mid-migration codebase: you build the new engine, prove it on two new, low-risk pilot flows (Peleburan, Laporan RUPS), and only then migrate the big, mature flows onto it. The mature flows are "old-style" precisely because they predate the engine — not because anything's wrong with them.
Correction vs. an earlier draft of this plan:
AKUISISI_PTis not in the "heavily smoke-tested" bucket with Pendirian/Perubahan/PP. Efran confirmed (2026-07-09): all legacy (v1-fork) flows are smoke-tested EXCEPT Akuisisi, Peleburan, and Laporan RUPS Tahunan. Project docs corroborate: the 2026-07-03 ReviewData plan explicitly listsservices/akuisisi-processor.tsalongsideroutes/peleburan.ts/routes/laporan-rups.tsas "un-smoke-tested new PT flows" to leave untouched. So Akuisisi is old-style and unproven — treat it with the same caution as Peleburan/RUPS, not as a battle-tested donor.Guardrail that falls out of this: before piling more flows onto the generic engine, smoke-test Peleburan + Laporan RUPS (and Akuisisi, in its current v1-fork form) — Peleburan/RUPS are the only evidence the
v2-genericengine works end-to-end on a real submission, and neither has been driven yet. Validating the engine first de-risks every migration wave below.
2. Three independent axes — don't conflate them
Every PT flow moves along three separate tracks, each with its own registry/mechanism and its own migration order. A flow can be ahead on one axis and behind on another:
| Axis | Registry | Question it answers | Mechanism |
|---|---|---|---|
| A. Processing engine | FLOW_REGISTRY (flow-engine/registry.ts) |
Does extraction/company-lookup/validation/rematch run on the shared runFlow engine, or on a bespoke hand-written processor? |
v1-fork vs v2-generic |
| B. Review-screen projection | REVIEW_REGISTRY (review/review-registry.ts) |
Does GET /api/submissions/:id/review know how to project this type into the generic ReviewData contract for the shared React review page? |
strangler: null (legacy bespoke page) vs a registered projector |
| C. Real-world validation | none (informal / ClickUp / Efran's own testing) | Has Efran actually driven this flow on a real submission end-to-end? | smoke-tested vs never-driven |
The review-registry.ts header states this explicitly: "review migration is orthogonal to processing migration — PENDIRIAN_PP migrates its review here while staying a v1-fork processor. When a flow is fully v2 end-to-end the two registries can merge; until then this map answers exactly one question."
Full ledger — all three axes, every flow
| Flow | SubmissionType | A. Processing | B. Review projection | C. Smoke-tested |
|---|---|---|---|---|
| Pendirian PT | PENDIRIAN_PT |
v1-fork (submission-processor.ts) |
✅ migrated, flipped — Efran-approved, legacy page + old /submit route retired (2026-07-08) |
✅ yes |
| Perubahan PT | PERUBAHAN_PT |
v1-fork (perubahan-processor.ts) — shadow config exists |
❌ not started — bespoke PerubahanDeltaReviewPage.tsx still serves it |
✅ yes |
| Akuisisi (pengambilalihan) | AKUISISI_PT |
v1-fork (akuisisi-processor.ts, rides perubahan extractor) — shadow config exists |
❌ not started — bespoke AkuisisiDeltaReviewPage.tsx still serves it |
❌ not smoke-tested (Efran-confirmed exception, 2026-07-09) |
| Perbaikan Data PT | PERBAIKAN_DATA_PT |
v1-fork (perbaikan-processor.ts) |
❌ not started — bespoke PerbaikanReviewPage.tsx |
✅ yes |
| Peleburan (konsolidasi) | PELEBURAN_PT |
v2-generic ✅ (flows/peleburan-pt.ts) |
❌ not in REVIEW_REGISTRY — has its own bespoke pre-strangler page/route (PeleburanReviewPage.tsx → /api/peleburan/submissions/:id/review-data), never folded into the generic strangler |
❌ not smoke-tested (Efran-confirmed exception) |
| Laporan RUPS Tahunan | LAPORAN_RUPS_TAHUNAN |
v2-generic ✅ (flows/laporan-rups-tahunan.ts) |
❌ not in REVIEW_REGISTRY — same pattern, own bespoke page/route (LaporanRupsReviewPage.tsx → /api/laporan-rups/submissions/:id/review-data) |
❌ not smoke-tested (Efran-confirmed exception) |
| Pendirian PP | PENDIRIAN_PP |
v1-fork (pp-pendirian-processor.ts) |
✅ migrated, flipped — Efran-approved (4 waves), legacy retired (2026-07-04) | ✅ yes |
| Pembubaran PP | PEMBUBARAN_PP |
v1-fork (inline; no reprocess entrypoint) | ✅ migrated, flipped — Efran-approved (2 waves), legacy retired (2026-07-06) | ✅ yes |
| Peralihan PP→PT | PERALIHAN_PP_KE_PT |
v1-fork (pp-peralihan-pt.ts) |
✅ Efran-approved 2026-07-09 — flip landing in a separate concurrent session (projector + V2 page built 2026-07-08; canonical route swap + legacy page//review-data retirement in flight elsewhere) |
✅ yes |
| Perubahan PP | PERUBAHAN_PP |
v1-fork (pp-perubahan-processor.ts) |
❌ not started — next up per Phase B roadmap, now highest overall priority (§0) | ✅ yes |
| Perbaikan Data PP | PERBAIKAN_DATA_PP |
v1-fork (pp-perbaikan-pp-processor.ts) |
❌ not started — next up, paired with Perubahan PP | ✅ yes |
| Apostille / Legalisasi | APOSTILLE |
v1-fork, 2-phase (apostille-processor.ts) — missing "jalur prioritas" (priority-lane) flow/logic, flagged by Efran 2026-07-09, explicitly out of scope for now (tracked, not blocking) |
❌ not started | ✅ yes |
| Pembubaran PT | PEMBUBARAN_PT |
stub (dark) here / v2-generic on the parallel feat/pt-flows-registry session |
n/a (flow doesn't run here yet) | n/a |
| Berakhirnya Status PT | BERAKHIRNYA_STATUS_PT |
enum absent here / v2-generic on the parallel session | n/a | n/a |
| Penggabungan (merger) | — (enum missing) | confirmed real SABH transaction — unbuilt everywhere. Classifier-recognized only (AKTA_TXN_TYPES since 2026-07-09); no SubmissionType, no flow, no fork, on either branch. NOW ON THE ROADMAP (§2.1) |
n/a | n/a |
| ~~Pemisahan (spin-off)~~ | — | confirmed NOT a real SABH transaction type — dropped from consideration entirely (§2.1) | n/a | n/a |
2.1 SABH re-confirmation (2026-07-09): what's actually real
Checked directly against ahu-online/protected/modules/sabh — TransaksiPerseroan::getJenisTransaksi($id_aksi) is the canonical id→label map for Perseroan (PT) id_aksi_transaksi values, cross-checked against migrations:
id_aksi_transaksi |
Label (SABH) | Our flow | Status |
|---|---|---|---|
| 9 | Pendirian | PENDIRIAN_PT |
built |
| 10 | Perubahan (/ "Penyesuaian" subcase) | PERUBAHAN_PT |
built |
| 11 | Penggabungan | — | confirmed real, unbuilt everywhere → roadmap item |
| 12 | Pembubaran | PEMBUBARAN_PT |
confirmed real — v2-generic on the parallel session, dark stub here → port |
| 19 | Pengambilalihan (Akuisisi) | AKUISISI_PT |
built (v1-fork) |
| 35 | Peleburan | PELEBURAN_PT |
built (v2-generic) |
| 36 | Permohonan Berakhirnya Status Badan Hukum | BERAKHIRNYA_STATUS_PT |
confirmed real — added to SABH via migration 20250808_add_aksi_transaksi_permohonan_berakhir_status.sql; v2-generic on the parallel session, enum absent here → port |
No id_aksi_transaksi for Pemisahan exists anywhere in SABH — checked getJenisTransaksi's full id range plus a broad grep for stray id_aksi_transaksi literals across ahu-online; every other id found (2,3,6,7,8,14,15,21,25,28,29) belongs to Yayasan/Perkumpulan transactions or unrelated "beli profil" services, none to Perseroan spin-off. Conclusion: Pemisahan is not a real transaction — remove it from scope permanently, not just "defer."
PERBAIKAN_DATA_PT/PERBAIKAN_DATA_PP are confirmed as their own separate SABH module (views/perbaikanData/, views/verifikasiPerbaikanData/) — an administrative correction workflow, not an id_aksi_transaksi at all. No change needed; already correctly modeled as its own flow.
Important reframing on Pembubaran PT / Berakhirnya Status PT
This isn't a one-way "steal from legacy" — tidyup/progress.md records that the feat/pt-flows-registry work is a parallel session on a convergent, independently-arrived-at design (same Record<SubmissionType, FlowEntry> shape, same v1-fork/v2-generic split). The recorded plan is to RECONCILE at merge, never build a second registry: read their landed flow-engine/** as a second implementation of the same contract, reconcile the two, and land one registry — not blindly port-and-overwrite.
3. Review-projection mechanism (Axis B — already running, separate track)
This axis has its own strangler pattern, already proven on 4 flipped/flipping tenants (Pendirian PP, Pembubaran PP, Pendirian PT, and — Efran-approved 2026-07-09 — Peralihan PP→PT, whose flip is landing in a separate concurrent session). It does not touch the processing engine (Axis A) at all — a flow can flip its review screen while its processor stays v1-fork.
(ReviewProjection:
sections + projectReview)"] B["2 · Register in
REVIEW_REGISTRY
(compiler-forced, per type)"] C["3 · Build the V2 page
(useReviewEngineData →
generic /api/.../review)"] D["4 · Efran smoke-tests
the V2 page"] E["5 · Flip canonical route
+ retire legacy page
+ legacy /review-data"] A --> B --> C --> D --> E D -.->|"not yet approved"| STOP["stays on legacy
bespoke page"]
- Projector — a pure
projectReview(submissionId) → ReviewData | nullthat reads whatever the existing processor (v1-fork or v2-generic — doesn't matter) already wrote to the DB, and reshapes it into the one normalizedReviewDatacontract. Optionalprepare()for pre-projection side effects (e.g. promoting deferred SKIPPED validations). - Register — add to
REVIEW_REGISTRY: Record<SubmissionType, ReviewProjection | null>. Adding aSubmissionTypeto the enum makes an unregistered entry a compile error (must be explicitnull). - V2 page — a thin page using the shared
useReviewEngineDatahook againstGET /api/submissions/:id/review; contract-validated againstReviewDataSchemain non-prod. - Efran smoke-tests the V2 page on real submissions — this is a hard gate, not a formality (Peralihan PP→PT is sitting here right now).
- Flip — canonical route renders the V2 page,
/review-v2becomes a redirect (keeps bookmarks alive), legacy hand-built page + its dedicated/review-dataendpoint are deleted.
Where Peleburan and Laporan RUPS sit: they predate this strangler pattern entirely. They already have their own bespoke ReviewData-shaped page + dedicated route (/api/peleburan/submissions/:id/review-data, /api/laporan-rups/submissions/:id/review-data) built directly alongside their v2-generic processing flow — never registered in REVIEW_REGISTRY, never strangled. Folding them in is optional cleanup, not required for Axis A work.
Practical implication for Axis A (processing) migration: when PENDIRIAN_PT, PERUBAHAN_PT, AKUISISI_PT, etc. move their processor to v2-generic, their review screens don't need to change in lockstep — Axis B only cares that the DB rows the projector reads still exist in the same shape. Sequence them independently; just re-verify each projector still parses correctly after an Axis A flip.
4. Processing-engine migration mechanism (Axis A, the reusable procedure)
The tidy foundation already ships the migration harness — __shadow__ configs + an equivalence oracle (flow-engine/__tests__/flow-parity.test.ts). The per-flow procedure:
FlowConfig
(defineFlow, NOT registered)"] B["2 · Parity oracle
fork vs shadow on
cloned fixture →
deep-equal results"] C["3 · Flip registry
v1-fork → config
+ BUILT_AKTA_TXN_TYPES"] D["4 · Delete bespoke
processor +
collapse fat route"] E["5 · Smoke test
real submission
end-to-end"] A --> B --> C --> D --> E B -.->|"RED = plumbing bug,
fix engine not validator"| A
- Shadow config — a
defineFlow<Ctx>({ type, engine:'v2-generic', … })that reuses the fork's own rule bodies and context. Precedent:flows/__shadow__/perubahan-pt.shadow.ts,akuisisi-pt.shadow.ts(TESTS-ONLY, not inFLOW_REGISTRY). - Parity oracle — seed two clones of one submission; run the fork on one and the shadow's
buildContext → runValidations → persiston the other; deep-equal the persistedValidationResultrows on{ruleCode, ruleLabel, status, message}. A red diff is an engine-plumbing bug, fixed in the engine, never by weakening the diff. - Flip — swap the
FLOW_REGISTRYentry from{engine:'v1-fork'}to the config; add the txn type toBUILT_AKTA_TXN_TYPESwhere classifier-routed. Dispatch already prefersv2-generic. - Delete — remove the bespoke processor and fold its route into the generic route-family surface.
- Smoke test — mandatory; these are flows the team actually runs.
⚠️ The current oracle only covers the validation half (context-lift → runner → persist). It does not yet assert parity on extraction, company lookup, rematch, section shaping, or finalize. Before migrating a mature flow, the oracle must be extended to a full-pipeline parity harness, or migration risks silently regressing behaviour the validation-only diff can't see.
5. Sequenced waves (Axis A — processing)
Re-sequenced 2026-07-09 to match Efran's stated priority: PP flows → Apostille/Legalisasi flows → PT flows (§0). This inverts the PT-first draft below it in favor of business priority; Wave 0 (engine validation) still goes first regardless of family, since every later
v2-genericmigration depends on it being proven.The project's own
tidyup/tasks.md"Phase B" roadmap sequences Axis B (review) as: Perubahan PP + Perbaikan Data PP next, then "(post smoke-test) diff wave: Perubahan PT + Akuisisi PT; then Perbaikan PT; Apostille last" for Axis A. That Axis-B ordering already agrees PP comes before the PT diff wave; it predates the explicit Apostille-before-PT call and doesn't cover Penggabungan (undiscovered at the time).
(prerequisite for every later wave)"] W1["WAVE 1 · PP family (priority #1)"] W2["WAVE 2 · Apostille / Legalisasi (priority #2)"] W3["WAVE 3 · PT family (priority #3)"] W0 --> W1 --> W2 --> W3 W0 --- w0a["smoke Peleburan + Laporan RUPS
(the only unproven v2-generic evidence)"] W1 --- w1a["Perubahan PP + Perbaikan Data PP
(review projection, next per Phase B)"] W1 --- w1b["PP context builder + PP rule sets
(processing engine prerequisite)"] W2 --- w2a["jalur-prioritas gap stays OUT of scope
(tracked, not blocking)"] W2 --- w2b["2-phase engine extension
(classify → gated extract)"] W3 --- w3a["port PEMBUBARAN_PT → BERAKHIRNYA_STATUS_PT
(reconcile parallel-session drift)"] W3 --- w3b["PERUBAHAN_PT · AKUISISI_PT
(shadows exist)"] W3 --- w3c["PENDIRIAN_PT · PERBAIKAN_DATA_PT"] W3 --- w3d["build PENGGABUNGAN
(new — no fork anywhere to migrate from)"]
Wave 0 — validate the engine (prerequisite, do regardless of family — lowest risk, highest unblocking)
- 0a. Smoke Peleburan + Laporan RUPS. No code; proves the
v2-genericengine works on a real submission before anything else rides it. This is now the only remaining unproven piece of the engine — everything else Efran has already exercised. - 0b. Extend the parity oracle to full-pipeline (see §6) before any Wave 3 processing migration relies on it.
Wave 1 — PP family (priority #1)
- Axis B (review):
PERUBAHAN_PP+PERBAIKAN_DATA_PP— next per the existing Phase B roadmap, paired together (shares the FF4-grandfathered fork pair). - Axis A (processing):
PENDIRIAN_PP,PERUBAHAN_PP,PERBAIKAN_DATA_PP,PEMBUBARAN_PP,PERALIHAN_PP_KE_PTall stay v1-fork today. Larger lift than PT: the generic engine's context is PT-akta-shaped (buildPtAktaContext); PP needs its own context builder + PP rule sets before any shadow config is possible.PEMBUBARAN_PPalso lacks a standalone reprocess entrypoint (processes inline in its route) — needs one to fit the dispatch model. - Peralihan PP→PT's review flip is already in flight in a separate session (Axis B done); its processing (Axis A) stays v1-fork, same PP-context blocker as the rest of the family.
Wave 2 — Apostille / Legalisasi (priority #2)
- Two-phase (classify → gated extract). The generic engine's single
primaryAktamodel doesn't express this; requires an engine extension (multi-phase / gated extraction) before it can become av2-generictenant. - Known gap, explicitly out of scope for now: missing "jalur prioritas" (priority-lane) flow/logic — flagged by Efran 2026-07-09. Track it; don't block the rest of the Apostille migration on it.
- New rebuild codebase exists at
ahu-rebuild/ahu-rebuild-apostille(ahu-apostille-api+module-apostille) — check there before assuming the legacyahu-apostillerepo is still the source of truth for future Apostille work.
Wave 3 — PT family (priority #3 — was previously framed as first; now last)
- 3a. Port
PEMBUBARAN_PT→BERAKHIRNYA_STATUS_PT. Both confirmed real SABH transactions (§2.1), alreadyv2-genericon the parallelfeat/pt-flows-registrysession. Add theBERAKHIRNYA_STATUS_PTenum member first (absent in tidy). Work = interface reconciliation, not a fresh build —feat/pt-flows-registryis 284 commits behind tidy'stypes.ts(focusedPasses,allowedStatusTransaksi,failGate,rematch.rupsAttendanceall postdate it). Reconcile per §2's "convergent parallel session" note — merge, don't overwrite. Berakhirnya depends on Pembubaran landing first (itsBSBHP_REGISTRY_STATEprerequisite gate reads an approved-pembubaran registry snapshot; shares the 6-PDF likuidasi package). - 3b.
PERUBAHAN_PT,AKUISISI_PT. Shadows already exist and pass validation parity. Remaining: extend parity to full pipeline (blocked on Wave 0's oracle work), flip registry, deleteperubahan-processor.ts/akuisisi-processor.ts, collapse the 1763-line perubahan route, smoke (Akuisisi has never been smoke-tested — do this before, not after, flipping it). Akuisisi keeps its koran-window rule +forcePeralihanOnhook (already modelled in the shadow). - 3c.
PENDIRIAN_PT,PERBAIKAN_DATA_PT. No shadow yet; build shadow → parity → flip → deletesubmission-processor.ts/perbaikan-processor.ts→ smoke. - 3d. Build
PENGGABUNGAN(merger) — genuinely new, not a migration. Confirmed real (id_aksi_transaksi=11, §2.1); classifier already recognizes it but no branch has aSubmissionType, context, rules, or route for it. No fork to shadow — this is freshv2-genericwork from scratch, likely modeled on Peleburan's multi/single-entity precedent (a merger absorbs one+ PTs into a surviving PT, structurally closer to Peleburan's consolidation shape than to a diff-based Perubahan). Lowest-confidence estimate in this whole plan since there's no donor implementation anywhere to reconcile against.
6. Known engine gaps to close during migration
| Gap | Impact | Fix |
|---|---|---|
| Parity oracle = validation-only | Migrating a mature flow can regress extraction/lookup/rematch/sections undetected | Extend to full-pipeline parity before Wave 3's PT migrations |
| Multi-source lookup is PELEBURAN-COUPLED | runFlow hardcodes import('../services/peleburan-sources'); a 2nd multi-source flow would run peleburan's resolver |
Introduce a per-flow companyLookup.resolve hook (noted inline in processor.ts) |
| Interface drift on legacy ports | feat/pt-flows-registry is 284 commits behind; its defineFlow/context/rules signatures are older |
Reconcile each ported file against current types.ts during Wave 3a |
| PP context absent from engine | Generic context = PT-akta only | Build PP context builder + rule sets (Wave 1 prerequisite — now highest priority) |
| Apostille 2-phase not expressible | Single primaryAkta extraction model |
Engine extension for gated/multi-phase extraction (Wave 2) |
| Fat routes not yet absorbed | perubahan/pp routes are 1500–1800 lines of bespoke handlers | Generic route-family must cover this surface before deleting them |
| No donor for Penggabungan | Unlike every other Wave 3 item, there's no fork or parallel-session implementation to shadow/reconcile against | Budget it as new design work, not a port (Wave 3d) |
7. Recommended order of operations
- Smoke Peleburan + Laporan RUPS (engine proof — do this first, it's free; the only unproven
v2-genericevidence left). - PP family (priority #1): build the PP context builder + rule sets, land Axis B for Perubahan PP/Perbaikan Data PP, then work Axis A.
- Apostille family (priority #2): design the engine's 2-phase extension; leave "jalur prioritas" tracked but out of scope.
- PT family (priority #3): port
PEMBUBARAN_PT→BERAKHIRNYA_STATUS_PT(reconcile, don't overwrite), extend the parity oracle to full-pipeline, migratePERUBAHAN_PT+AKUISISI_PT(smoke Akuisisi before flipping it), thenPENDIRIAN_PT+PERBAIKAN_DATA_PT, then buildPENGGABUNGANfresh.
Permanently out of scope: PEMISAHAN — confirmed not a real SABH transaction type (§2.1).