Pembubaran PT (Dissolution) Flow — Design Spec
Date: 2026-07-02
Status: Draft design, pending spec review
Author: Efran Nathanael (with Claude)
SubmissionType: PEMBUBARAN_PT (SABH id_aksi_transaksi = 12) — distinct from the existing PEMBUBARAN_PP (Perseroan Perorangan, UU CK regime). Keep the spellings distinct everywhere.
Related: docs/specs/2026-07-01-target-architecture-design.md (canonical FlowConfig/Rule/FLOW_REGISTRY interfaces — this spec designs within them, it does not redesign them), docs/research/sabh-pt-untouched-flows-2026-07-01.md (Pembubaran = rank #6, transformability 4 / QoL 3 / effort M; Likuidator Management = rank #10, folded in here), docs/superpowers/specs/2026-07-01-akta-transaction-type-classifier-design.md (routing substrate; §6.2 doc-set predicates), docs/superpowers/specs/2026-07-01-akuisisi-pt-flow-design.md (the sibling flow this mirrors — and deliberately diverges from on backend shape).
1. Overview & Goal
Pembubaran PT is the dissolution of a registered PT (SABH id_aksi 12), notaris- or court-appointed-likuidator-initiated. The SABH wizard today: look up the PT (SK terakhir as the identity key), then hand-key the dissolution deed(s) — dual deeds possible (akta pembubaran + BA/Risalah RUPS) — the RUPS date, repeating newspaper-announcement rows, per-shareholder attendance checkboxes, and the substitute notary; then, inside the same flow, register the likuidator from the surat penunjukan/pertanggungjawaban likuidator (nama, nomor/tanggal surat, full address block, telepon/email, dasar penunjukan — 100% hand-typed today). The user uploads these documents; we OCR, look up the PT in SABH, cross-validate, review per-section, and submit.
Goal: ship a working PEMBUBARAN_PT flow at full depth (upload → classify → extract → validate → review → verifikator → finalize), matching the akuisisi/perubahan experience — as the first registry-first flow: a FlowConfig entry on the FLOW_REGISTRY slice with wrapped rules and sanctioned hooks, not a fourth copy of the 800-line processor fork. The Likuidator Management block (roadmap row #10) folds INTO this flow — it has no standalone SABH screen; it is a step inside PembubaranController.
Two facts shape everything:
1. No cap-table diff. Unlike perubahan/akuisisi, pembubaran changes no roster, modal, or KBLI. The review is not a delta view — it is a flat sectioned review (deed block, RUPS attendance, likuidator, koran, identities) against the registry snapshot. This makes it a clean engine:'v2-generic' flow: the akta Phase 1–5 skeleton applies, the hardest descriptor (diff) does not. Per the program ship order (LAPORAN_RUPS_TAHUNAN → PELEBURAN_PT → PEMBUBARAN_PT → BERAKHIRNYA_STATUS_PT, C16), Pembubaran is the third v2-generic tenant; the engine + shared seams are already proven by RUPS-Tahunan and Peleburan before it lands. It is the first consumer of SURAT_LIKUIDATOR (which it builds) and the doc-set predicate (which it owns).
2. Pembubaran ≠ Berakhirnya. Both are terminal, both can arrive as a generically-titled PKR, and their deed shapes overlap. Pembubaran (id_aksi 12) starts liquidation (deed appoints the likuidator); Berakhirnya (id_aksi 36) ends it (deed accepts the likuidator's final accounting, package includes laporan akhir likuidasi + Pasal 149/152 pengumuman pair). The §4.2 doc-set predicate routes between them; Berakhirnya stays classifier-gated (berakhirnya ∉ BUILT_AKTA_TXN_TYPES) until its own flow ships.
2. Scope
In scope (phase 1)
- New PEMBUBARAN_PT SubmissionType, implemented as a FlowConfig on the FLOW_REGISTRY slice (engine: 'v2-generic'). No new processor fork; existing flows stay engine: 'v1-fork'.
- Classifier Phase-B wiring: pembubaran joins BUILT_AKTA_TXN_TYPES; the Pembubaran-vs-Berakhirnya doc-set predicate at inferSubmissionType (§4.2).
- Dual-deed extraction via the perubahan extractor (akta_kedua block already in backend/src/schema/akta-perubahan.ts ~L469) + tanggal_rups + notaris_pengganti.
- RUPS attendance pre-tick: reuse rups-attendance-matcher.ts, extended with a registry-roster fallback (the deed may not restate shareholders; SABH's checkboxes are against the registry list).
- NEW extractor: Surat Likuidator (SURAT_LIKUIDATOR) — nama, nomor/tanggal surat, alamat + RT/RW/kodepos/kecamatan(/kelurahan), telepon, email, dasar_penunjukan — grounded-pass pattern (§4.4).
- First non-building consumer of the SHARED Bukti Pengumuman clipping extractor (bukti-pengumuman-extract.ts, built in Peleburan — canonical field keys nama_koran / tanggal_pengumuman / nomor_pengumuman, nullable) for the BUKTI_PENGUMUMAN DocumentType (repeatable koran clippings); plus the shared parameterized extractKoranFields inline-in-deed koran fallback.
- Classification strategy for both new DocumentTypes the x056 GPU classifier (22 labels) does not know (§4.5).
- Tuned validation matrix (§4.6): identity/address/kontak cross-checks required per the akuisisi precedent (commit aa4551d), registry status checks (PT active, not blocked, not already dissolved), likuidator KTP cross-check, koran as advisory WARNING.
- Frontend: thin mirrors of the akuisisi pages (extraction + review + status). NO ReviewEngine / FrontendReviewDescriptor — that greenfield is sequenced later; do not front-run the ReviewData reshape (target-arch §7 critical #1).
Out of scope (deferred — §9)
- The Berakhirnya Status flow (consumes labels this flow introduces; own spec).
- nomor_pengumuman extraction (a Berakhirnya-stage field, Pasal 149/152 clippings).
- Court-initiated dissolution documents (penetapan pengadilan — a new court-doc class, roadmap "Pemblokiran" territory).
- Likuidator login-account CRUD (UserLikuidatorController — no business data, confirmed in discovery).
- sisa_kekayaan (lives in the Berakhirnya laporan akhir likuidasi, not here).
- A dedicated AKTA_PEMBUBARAN_SCHEMA / akta_subtype (same phase-2 posture as akuisisi).
- SABH submit-back API (whole-PoC deferral).
3. Current Context (what we reuse)
Prerequisite: the FLOW_REGISTRY slice (target-arch Pillar 1; tracked as its own build task). PEMBUBARAN_PT is designed to be born generic — there is no legacy fork to act as its parity oracle, and per the binding program decision, no fork may be written. If the slice slips, hold this flow rather than fork.
Reuse-as-is (no change):
| Component | Path | Role for Pembubaran |
|---|---|---|
| Akta schema + extractor | backend/src/schema/akta-perubahan.ts (nomor_akta/tanggal_akta, akta_kedua dual-deed block, tanggal_rups, notaris_pengganti, penghadap) | Deed header payload — extraction rides the perubahan extractor via classification-override, exactly like AKTA_AKUISISI → subtype 'perubahan' in document-processor.ts (~L391–472) |
| SABH lookup + snapshot | backend/src/services/company-lookup.ts (lookupCompany, loadOldData, loadPerseroanBlokirState, lookupNotarisIdByName), sabh-pt-snapshot.ts | Locate the PT by nama_perseroan + ref_pendirian_nomor_sk (SK terakhir as lookup key — same Phase-2 as akuisisi-processor.ts L71–108) |
| RUPS attendance | backend/src/services/rups-attendance-matcher.ts + BeritaAcaraRupsExtraction | Pre-tick per-shareholder kehadiran (extended, §4.3) |
| Notaris pengganti | backend/src/services/notaris-pengganti-enrichment.ts | Substitute-notary block (Notaris Pengganti bulletproofing carries over) |
| Cross-validator bodies | backend/src/services/cross-validator.ts — wrapped bit-for-bit as Rules (target-arch decision #5) | §4.6 matrix |
| Cards + matching | KTP/NPWP two-pass extractors, identity-matcher.ts, contact-info-matcher.ts, domisili extraction | Identity/address/kontak cross-checks |
| Koran grounded pass | backend/src/services/akta-koran-extract.ts (extractKoranViaFocusedLlm) | Generalized (§4.4) for the clipping + inline-deed passes |
| Verifikasi machinery | PerubahanSectionApproval / VerifikasiPerubahan tables, verifikator flow (MENUNGGU_VERIFIKATOR → VOTING → SELESAI), submit gate shape in routes/perubahan.ts (~L1226–1310) | Shared — same section-approval gate vocabulary as akuisisi |
| Classifier substrate | backend/src/services/akta-txn-classifier.ts — pembubaran already in AKTA_TXN_TYPES, AKTA PEMBUBARAN title rule already emits it, KEYWORD_CONFIRM.pembubaran = /pembubaran|likuidasi/i | Only the BUILT_AKTA_TXN_TYPES gate + prompt cue change |
| Frontend kit | AkuisisiExtractionPage.tsx / AkuisisiDeltaReviewPage.tsx, flow-steps.ts, destination-for.ts, detect-banner.tsx, classifier-labels.ts | Thin-mirror templates |
NOT reused (deliberately): change-type-detector.ts / jenis-selection / perubahan-diff.ts / post-perubahan-roster.ts — there is no jenis-perubahan selection, no cap-table diff, no roster composition in a dissolution. The FlowConfig simply omits them (hooks.mapChangeTypes absent, no diff sections).
Genuinely NEW: the Surat Likuidator extractor, the Bukti Pengumuman clipping pass, two DocumentTypes + their classification strategy, four rules (PT_ALREADY_DISSOLVED, LIKUIDATOR_SURAT_COMPLETE, LIKUIDATOR_KTP_MATCH, PEMBUBARAN_KORAN), the registry-roster fallback in the RUPS matcher, and the two thin frontend pages.
4. Design
4.1 Data flow
upload akta pembubaran (+ BA RUPS, surat likuidator, koran clippings, KTP/NPWP/Domisili/Data Kontak)
→ klasifikasi: x056 coarse label → akta-txn-classifier → AKTA_PEMBUBARAN
(SURAT_LIKUIDATOR: in-app grounded LLM classification; BUKTI_PENGUMUMAN: manual board assignment — §4.5)
→ inferSubmissionType: Berakhirnya doc-set predicate (§4.2) → PEMBUBARAN_PT
→ FLOW_REGISTRY['PEMBUBARAN_PT'] → runFlow:
Phase 1 extract AKTA_PEMBUBARAN (perubahan extractor via override; dual-deed + tanggal_rups + notaris_pengganti)
Phase 2 company-lookup (nama + ref_pendirian_nomor_sk) → oldData snapshot; pause AWAITING_COMPANY_SELECTION if ambiguous
Phase 3 remaining docs: BA RUPS, SURAT_LIKUIDATOR (new extractor), BUKTI_PENGUMUMAN (koran pass),
KTP/NPWP/Domisili/Data Kontak; identity matching; RUPS attendance pre-tick (registry-roster fallback)
Phase 4 (no change-type detection — skipped by config)
Phase 5 runValidations(cfg) → persistValidationResults (async rules partitioned, Promise.all preserved)
→ READY → per-section review (PerubahanSectionApproval, §4.7) → submit gate
→ verifikator (MENUNGGU_VERIFIKATOR → VOTING → SELESAI) → finalize (hooks.onFinalize records local dissolution)
4.2 Routing + the Pembubaran-vs-Berakhirnya doc-set predicate (classifier design §6.2)
pembubaran joins BUILT_AKTA_TXN_TYPES in akta-txn-classifier.ts (this removes it from detectUnsupportedAktaType); berakhirnya stays OUT. Everything remains classifier-gated until ship.
The deed alone cannot always separate the two terminal flows, so inferSubmissionType (backend/src/routes/submissions.ts ~L84) gains a doc-set predicate that runs BEFORE the pembubaran branch. This predicate is defined ONCE in submissions.ts and referenced identically by the Berakhirnya spec (C5 reconciliation). It lands with PEMBUBARAN_PT (its first consumer). Ladder = the canonical inferSubmissionType ladder owned in submissions.ts (registry slice §5 item 7 — most-specific-wins), each flow adds only its OWN rung at ship time:
Perbaikan > Berakhirnya(doc-set predicate) > Peleburan > Akuisisi > Pembubaran > Perubahan > LaporanRupsTahunan > PendirianPP > PendirianPT
// eff(f) = f.userOverride ?? f.classification; carrier = mapToDocumentType(eff(f))
const BERAKHIRNYA_PACKAGE = (files: InferableFile[]) =>
files.some((f) => eff(f) === "AKTA_BERAKHIRNYA") ||
(files.some((f) => eff(f) === "AKTA_PEMBUBARAN") && (
files.some((f) => eff(f) === "LAPORAN_LIKUIDASI" || eff(f) === "SURAT_PERMOHONAN") || // labels arrive with the Berakhirnya flow
files.some((f) => eff(f) === "BUKTI_PENGUMUMAN_152") || // Pasal 152 fine label (Berakhirnya axis)
files.filter((f) => mapToDocumentType(eff(f)) === "BUKTI_PENGUMUMAN").length >= 2 // ≥2 docs mapping to the carrier DocumentType — count the carrier, NEVER the raw label string
));
- Predicate true → the submission is a Berakhirnya package. Because
berakhirnya ∉ BUILT_AKTA_TXN_TYPES, it surfaces on the Klasifikasi board as "Terdeteksi: Berakhirnya Status Badan Hukum (belum didukung)" — never silently routed into PEMBUBARAN_PT. The≥2 docs mapping to BUKTI_PENGUMUMANclause is live from day one (this flow introduces that DocumentType); theLAPORAN_LIKUIDASI/SURAT_PERMOHONAN/BUKTI_PENGUMUMAN_152clauses activate when the Berakhirnya flow adds those labels — the predicate is written now so that flow only appends label names. The count clause matches the carrier DocumentType viamapToDocumentType, so it keeps firing after Berakhirnya introduces the_149/_152fine labels (a raw-label-string count would silently break — C5). - Predicate false +
AKTA_PEMBUBARANpresent →PEMBUBARAN_PT. - Classifier prompt cue (content-level disambiguation,
SYSTEM_PROMPTinakta-txn-classifier.ts): pembubaran = the deed dissolves the PT and APPOINTS a likuidator (liquidation begins); berakhirnya = the deed accepts the likuidator's final accounting / pertanggungjawaban and declares the badan hukum ended (liquidation ends). Add one line each to the type descriptions. - A single koran clipping (the Pasal 147 dissolution announcement) does NOT trip the predicate — only the pair does.
4.3 Extraction plan
Akta Pembubaran (hybrid, mirrors akuisisi §4.3). Stored classifiedType = 'AKTA_PEMBUBARAN'; extraction reuses the perubahan extractor via the classification-override path. Generalize the two hardcoded options?.classificationOverride === "AKTA_AKUISISI" checks in document-processor.ts (L416, L472) into a small AKTA_LABEL_TO_SUBTYPE map (AKTA_AKUISISI → 'perubahan', AKTA_PEMBUBARAN → 'perubahan') — the third hardcode would be a fork-with-ifs smell. Yields: nomor_akta/tanggal_akta, the akta_kedua block (dual deeds — akta pembubaran + PKR/BA RUPS as second deed), tanggal_rups, nama_notaris + notaris_pengganti, nama_perseroan, ref_pendirian_nomor_sk (lookup key), penghadap. Roster fields may come back sparse/empty — that is fine (no diff consumes them); the RUPS matcher falls back to the registry roster.
BA RUPS. Existing BERITA_ACARA_RUPS label → BeritaAcaraRupsExtraction → matchRupsAttendance. Extension (additive param): today the matcher writes kehadiranRups onto aktaPemegangSaham rows extracted from the deed (rups-attendance-matcher.ts L51–53 keys on classifiedType ∈ {AKTA_PERUBAHAN, AKTA} — also add AKTA_PEMBUBARAN, or match on the flow's primary-akta type from config). When the deed restates no shareholders, match BA-RUPS attendees against oldData.pemegang_saham names instead and persist the result on a submission-level JSON (rupsKehadiranRegistry Json? — [{ nama, persentase, kehadiran }]), which the Kehadiran section renders and the quorum rule consumes. SABH's attendance checkboxes are against the registry list, so this fallback is the primary path for pembubaran.
Surat Likuidator — NEW extractor backend/src/services/surat-likuidator-extract.ts (grounded-pass pattern, modeled on akta-koran-extract.ts + apostille-generic-extract.ts, NOT a broadening of any shared prompt — memory reference_apostille_extraction_llm):
- Input: the document's cached OCR rawText (PaddleOCR layout path).
- Deterministic first: regex RT/RW (RT\s*\.?\s*(\d{1,3})\s*/?\s*RW…), kodepos (\b\d{5}\b near alamat), telepon ((\+62|08)\d{7,12}), email — regex-extract before the LLM, LLM fills the rest.
- One focused guided-JSON call (3B cleanup endpoint CLEANUP_LLM_URL, temperature: 0): { nama, nomor_surat, tanggal_surat, alamat, rt, rw, kode_pos, kelurahan, kecamatan, telepon, email, dasar_penunjukan }, all ["string","null"].
- Ground every value against rawText via the normalized-substring check (norm() as in akta-koran-extract.ts L48–53); dasar_penunjukan is free text spanning lines — ground token-wise (≥80% of its normalized tokens present) rather than whole-substring, or it will always null out.
- Persist as ExtractedField rows on the document (fieldKeys: likuidator_nama, likuidator_nomor_surat, likuidator_tanggal_surat, likuidator_alamat, likuidator_rt, likuidator_rw, likuidator_kode_pos, likuidator_kelurahan, likuidator_kecamatan, likuidator_telepon, likuidator_email, likuidator_dasar_penunjukan) — zero new tables; reuses field-confirmation, bbox-matching, and the confirm UI wholesale. Confidence: grounded values get the 75 "please review" tier (memory reference_confidence_tiers_and_timer) — never auto-LOCK.
- Scoped to the surat penunjukan shape ONLY (the likuidator-appointment letter: header + address/kontak block). It deliberately does NOT swallow the laporan-akhir/pertanggungjawaban — that is the DISTINCT LAPORAN_LIKUIDASI DocumentType built in the Berakhirnya flow (incl. sisa_kekayaan), which the Pembubaran↔Berakhirnya doc-set predicate (§4.2) keys on. A shape-agnostic extractor would collapse the two and break that routing (C2).
Bukti Pengumuman (koran clipping) — SHARED, not built here. Two distinct shared components, both consumed (not authored) by this flow:
1. Clipping pass — for each BUKTI_PENGUMUMAN doc, run the ONE shared clipping extractor bukti-pengumuman-extract.ts (built in Peleburan, its first consumer) with the canonical field keys nama_koran / tanggal_pengumuman / nomor_pengumuman (all nullable; nomor_pengumuman stays null here — only Berakhirnya consumes it), persisted as ExtractedField rows on the clipping. Deterministic masthead heuristic first (first large-text line / known mastheads incl. "Berita Negara"), grounded 3B fallback. Known limitation: PaddleOCR line-splits dense newsprint (memory reference_ocr_engine_benchmark_apostille) — extraction is best-effort; empty fields fall to manual entry in review, never an OCR failure.
2. Inline-deed fallback — when zero clippings are uploaded, run the shared parameterized extractKoranFields(rawText, { context: 'pembubaran' }) (the ONE refactor of extractKoranViaFocusedLlm serving akuisisi/peleburan/pembubaran — the current prompt hardcodes "akta pengambilalihan") over the deed rawText in hooks.afterAktaExtraction (exactly the akuisisi koran hook), persisting nama_koran / tanggal_pengumuman on the akta document.
Repeatable rows: SABH's form has repeating newspaper rows; each clipping is one row, plus at most one inline-deed row.
4.4 FlowConfig (the actual registry entry)
Against the canonical interface (target-arch §2 — verbatim, not redesigned):
// backend/src/flow-engine/flows/pembubaran-pt.ts
export const PEMBUBARAN_PT_FLOW = defineFlow<PtAktaRuleContext>({
type: 'PEMBUBARAN_PT',
engine: 'v2-generic', // born generic — no fork exists, none may be written
primaryAkta: { // canonical shape: {classifiedType, missingError, extractError} ONLY
classifiedType: 'AKTA_PEMBUBARAN',
missingError: 'Akta Pembubaran tidak ditemukan',
extractError: 'Gagal memproses Akta Pembubaran',
},
skipExtractionTypes: ['AKTA_PEMINDAHAN_HAK', 'PENETAPAN_GANTI_NAMA'],
companyLookup: true, // nama_perseroan + ref_pendirian_nomor_sk (SK terakhir)
extraction: { // canonical ExtractionPlan (registry slice §4.2)
overrideByClassifiedType: { AKTA_PEMBUBARAN: 'perubahan' }, // subtype override → perubahan extractor (§4.3)
focusedPasses: { // the ONE per-docType focused-pass seam
SURAT_LIKUIDATOR: 'surat-likuidator', // §4.3 NEW (built here, penunjukan shape)
BUKTI_PENGUMUMAN: 'bukti-pengumuman', // SHARED clipping extractor (built in Peleburan)
},
},
buildContext: buildPembubaranContext, // buildPtAktaContext + { likuidator, koranEntries, rupsKehadiranRegistry, localDissolution }
rules: PEMBUBARAN_PT_RULE_SET, // §4.6 — wrapped cross-validator bodies + 4 new rules; rups-quorum wrapper passes quorumThreshold 3/4 (Ps.89)
sections: PEMBUBARAN_SECTIONS, // §4.7 — serialisable SectionSchema[], published via contract/
requiredDocs: ['AKTA', 'SURAT_LIKUIDATOR', 'KTP', 'NPWP', 'DOMISILI', 'DATA_KONTAK'], // DocumentType[] — DOMISILI is the existing enum the SURAT_PERNYATAAN_DOMISILI label maps to
rematch: { // canonical RematchConfig; queueing is engine-level (queueReMatchAndValidate)
checklistResetDocTypes: ['KTP', 'NPWP', 'DOMISILI', 'SURAT_LIKUIDATOR', 'BUKTI_PENGUMUMAN'],
standardPtMatchers: true,
preValidationSteps: [(id) => matchRupsAttendance(id, db)], // registry-roster fallback §4.3; no jenis machinery in this flow
},
hooks: {
afterAktaExtraction: pembubaranKoranInlineFallback, // §4.3 inline-deed koran pass (shared extractKoranFields refactor)
onFinalize: recordLocalDissolution, // §4.6 PT_ALREADY_DISSOLVED source (mirrors PEMBUBARAN_PP's COMPLETED-lookup pattern, pp-pembubaran.ts L67–72)
},
routeFamily: 'perubahan', // canonical union value — Pembubaran rides the perubahan route family (company-select / review-data / section-approval / submit / verifikator)
labels: { akta: 'Akta Pembubaran', flow: 'Pembubaran PT' },
});
~40 lines + one context builder + 4 rule files + 1 new extractor file (surat-likuidator-extract.ts; the koran clipping extractor is shared from Peleburan). No customProcess — the PT-akta Phase 1–5 skeleton fits (Phase 4 short-circuits because the config carries no change-type hook). mapChangeTypes absent by design.
4.5 New DocumentTypes + classification strategy (binding decision 5)
The x056 GPU classifier (ahu-classifier-v2, 22 labels) has no surat-likuidator or bukti-pengumuman label, and — being an untrained-for-these LayoutLMv3 — it will NOT abstain: it will confidently map these to a nearest label above the 0.6 gate (the "confident-wrong" trap, classifier spec §4). Never retrain the GPU for these (frozen w.r.t. new labels, classifier spec §5.6). Strategy per type:
| DocumentType | Strategy | Why |
|---|---|---|
SURAT_LIKUIDATOR |
In-app grounded LLM classification (apostille-classifier pattern): deterministic title rules first (/SURAT\s+(PENUNJUKAN|PERTANGGUNGJAWABAN)\s+LIKUIDATOR/i, /\bLIKUIDATOR\b/ in first 800 chars + pembubaran context), 3B grounded fallback on the OCR prefix; runs as a re-check pass over non-card docs when the submission set contains an AKTA_PEMBUBARAN (scoped — not global, so it can't disturb other flows' classifications). Manual board re-assignment remains the override, as for every label. |
The letter is text-rich with a near-deterministic heading — title regex alone catches most; and a missed SURAT_LIKUIDATOR silently kills the likuidator extraction + KTP cross-check (the flow's definitional net-new), so waiting for manual assignment is too fragile. The GPU's likely mislabel (SURAT_PERNYATAAN_*) would otherwise be trusted. |
BUKTI_PENGUMUMAN |
Manual board assignment (primary) + the ONE SHARED deterministic keyword pre-suggester — the UNION keyword set /(PENGUMUMAN)/i + (PELEBURAN\|KONSOLIDASI\|PEMBUBARAN\|LIKUIDASI\|RUPS\|RINGKASAN RANCANGAN) + masthead/date shape (built in Peleburan, reused verbatim here; never auto-accepted, always amber needs-review). No LLM classification pass. |
Koran clippings are visually unmistakable to the operator but hostile to text classification: PaddleOCR line-splits dense newsprint (memory reference_ocr_engine_benchmark_apostille), so grounded LLM classification over shredded text is unreliable — a wrong auto-label is worse than a 2-second manual pick. One shared suggester across peleburan/pembubaran/berakhirnya (C3) avoids three divergent keyword rules for the same DocumentType. The doc is optional + shallow, so misclassification cost is low and advisory-only (§4.6 koran severity). |
Both labels enter KlasifikasiPage/ClassificationListView label sets so the operator can assign/override; both map in mapToDocumentType and get getProcessingOrder slots (SURAT_LIKUIDATOR → 4, BUKTI_PENGUMUMAN → 6). Confirmed/corrected labels feed the same flywheel persistence as akta txn labels (classifier spec §5.5).
4.6 Validation matrix (TUNED — the priority deliverable)
Required documents (?type=pembubaran DocTypeLabel set in routes/klasifikasi.ts, mirroring the akuisisi precedent aa4551d where identity/address/kontak docs are REQUIRED so cross-validation always has inputs):
| Doc | Required | Max | Rationale |
|---|---|---|---|
AKTA_PEMBUBARAN |
✅ | 1 | Primary deed (dual-deed second akta lives in its akta_kedua block) |
SURAT_LIKUIDATOR |
✅ | 1 | The likuidator block is mandatory in SABH's wizard (_formPembubaranLikuidator.php) |
KTP |
✅ | n | Pengurus/penghadap + likuidator identity cross-check |
NPWP |
✅ | n | Pengurus NPWP cross-check |
SURAT_PERNYATAAN_DOMISILI |
✅ | 1 | Company address cross-check |
DATA_KONTAK |
✅ | n | Telepon/email completeness cross-check |
BERITA_ACARA_RUPS |
◻ recommended | n | Deed may be a PKR incorporating the RUPS, or the BA may be the akta_kedua |
BUKTI_PENGUMUMAN |
◻ optional | n | Publication may legitimately post-date filing (Pasal 147 — see PEMBUBARAN_KORAN) |
OTHER |
◻ | n | e.g. penetapan pengadilan on the court path (not extracted, phase 1) |
Rules (PEMBUBARAN_PT_RULE_SET = [...wrapped shared, ...pembubaran-specific]; wrapped = { code, label, run: ctx => validateX(ctx.input) }, bodies verbatim from cross-validator.ts — target-arch decision #5):
| Rule code | Source | Severity (max) | Notes / legal citation |
|---|---|---|---|
NIK_KTP_AKTA |
wrapped | FAIL | Penghadap/pengurus and likuidator NIK vs KTP (likuidator KTP joins the KTP pool) |
NAMA_NPWP_KTP |
wrapped | existing | Per-pengurus NPWP↔KTP name |
KTP_COMPLETENESS / NPWP_COMPLETENESS |
wrapped | existing | |
NAMA_PT_CONSISTENCY |
wrapped | existing | Akta vs domisili vs data-kontak vs NPWP perseroan |
DOMISILI_NAMA |
wrapped | existing | |
NPWP_PERSEROAN_FUZZY |
wrapped | existing | matchBakumDJP ≥ 70 (SABH parity) |
CONTACT_INFO_COMPLETE |
wrapped | WARNING | |
PASSPORT_AKTA |
wrapped | existing | WNA pengurus/likuidator |
REQUIRED_SUPPORTING_DOCS |
wrapped | existing | Against the doc table above |
UPLOADED_DOC_UNUSED |
wrapped | WARNING | Stray-doc hygiene |
AKTA_DATE_WINDOW |
wrapped | existing | 30-day window (perubahan-family isPerubahan: true) |
OLD_DATA_CONSISTENCY |
wrapped, async |
existing | Deed-stated PT data vs SABH snapshot |
PERSEROAN_STATE |
wrapped, async |
FAIL | Registry gate: status_transaksi === 1 (active), not PerseroanBlokir-blocked, FAILs on 5 (peleburan) / 6 (berakhirnya) — i.e. not already dissolved/ended (cross-validator.ts L1418) |
NOTARIS_TERAKHIR |
wrapped, async |
existing | Filing notaris vs registry id_notaris |
RUPS_ATTENDANCE_QUORUM |
wrapped + param | WARNING | Consumes matcher output (registry-roster fallback §4.3). Pembubaran tightening: quorum threshold ¾ (UUPT Pasal 89 ayat (1) — pembubaran RUPS requires ≥¾ attendance), passed as a param to the existing validator (default stays >50% for other flows). severityFloor: 'WARNING' — attendance extraction is heuristic name-matching; never hard-block on it. |
PT_ALREADY_DISSOLVED |
NEW, async |
FAIL | PoC-local double-dissolution guard: a COMPLETED PEMBUBARAN_PT submission whose oldData.perseroan.id_perseroan matches → FAIL (mirrors pp-pembubaran.ts L67–72 / pp-perubahan.ts L1453 pattern). Complements PERSEROAN_STATE, which only sees SABH's view. |
LIKUIDATOR_SURAT_COMPLETE |
NEW | WARNING | All 12 likuidator fieldKeys present/non-empty; lists the missing ones. WARNING not FAIL — OCR gaps are completable in review (telepon/email often absent from the letter, SABH types them manually today too). |
LIKUIDATOR_KTP_MATCH |
NEW | FAIL on contradiction / WARNING on absence | likuidator_nama vs the KTP pool via identity-matcher scoring. Matched KTP with conflicting NIK/name → FAIL. No KTP plausibly matching the likuidator → WARNING ("KTP likuidator belum diunggah") — the likuidator may be an existing pengurus whose KTP already matched. |
PEMBUBARAN_KORAN |
NEW | WARNING — NEVER FAIL | Presence + window advisory. Legal citation: UUPT Pasal 147 ayat (1) — the likuidator must announce the dissolution in a newspaper and Berita Negara within 30 days counted FROM the dissolution, i.e. it is a post-deed obligation; at filing time the announcement may legitimately not exist yet. WARNING when no koran entry; WARNING when tanggal_pengumuman > tanggal_akta + 30d (window exceeded — likuidator non-compliance signal, still not our gate). severityFloor: 'WARNING' clamps it structurally (interface §2). Follows the intentional-advisory precedent (AKUISISI_KORAN_WINDOW, project_pp_nik_age_advisory). |
Tuned OUT (not in the rules array — the whole point of a per-flow matrix): MODAL_BUKTI_SETOR, PEMEGANG_SAHAM_BUKTI_SETOR, PP29_MODAL, KBLI_VALID, PERUBAHAN_MODAL, PERUBAHAN_KBLI, SHARES_TRANSFER_BALANCE, SHARES_TOTAL_LEMBAR, PS_TOTAL_100, DATA_ACTUALLY_CHANGED, JENIS_SELECTION_NONEMPTY / JENIS_DESELECT_REASONS / CONTRADICTORY_JENIS, AKUISISI_KORAN_WINDOW. A dissolution touches no cap table, modal, or KBLI — running these would emit noise-SKIPPEDs at best and false WARNINGs at worst. Omission-by-config (not input-shape-SKIPPED) is the registry-first way.
Async rules (OLD_DATA_CONSISTENCY, PERSEROAN_STATE, NOTARIS_TERAKHIR, PT_ALREADY_DISSOLVED) carry async: true so the generic runner partitions them into Promise.all (target-arch HIGH reconciliation — no latency regression on field edits).
4.7 Sections + review (frontend: thin mirrors, NO ReviewEngine)
PEMBUBARAN_SECTIONS: SectionSchema[] (serialisable, published via the config; becomes the FE's requiredSectionKeys source):
| key | label | approvable | required |
|---|---|---|---|
perseroan |
Data Perseroan (SABH) | ✅ | ✅ |
akta_pembubaran |
Akta Pembubaran (+ akta kedua, notaris pengganti) | ✅ | ✅ |
rups_kehadiran |
RUPS & Kehadiran Pemegang Saham | ✅ | ✅ |
likuidator |
Likuidator | ✅ | ✅ |
pengumuman_koran |
Pengumuman Koran | ✅ | ◻ (advisory-only content) |
identitas |
Dokumen Identitas (KTP/NPWP) | ✅ | ✅ |
data_kontak |
Data Kontak | ✅ | ✅ |
- Pages:
frontend/src/pages/PembubaranExtractionPage.tsx+PembubaranReviewPage.tsx— thin mirrors of the Akuisisi pages (which mirror Perubahan), minus the diff/jenis cards, plus: a Kehadiran card (registry roster + pre-ticked checkboxes, editable), a Likuidator card (12 confirm-able fields, bbox-grounded), a Koran card (repeatable rows, manual-entry fallback). Confirm (blue) semantics per existing PT conventions; card auto-LOCK rules unchanged. - Section approvals persist as explicit
PerubahanSectionApprovalrows and the submit gate reads them (memoryreference_perubahan_section_approval— never derive from child confirmed state). The submit gate is a sibling of theAKUISISI_PTblock inroutes/perubahan.ts(~L1237–1310): expected keys = the 6 required section keys above (static — noappearingSectionKeys(jenis)computation, there is no jenis), thenVerifikasiPerubahancreate + audit event withsubmissionType: "PEMBUBARAN_PT". - Explicitly deferred: no
FrontendReviewDescriptor, noReviewEngine, noReviewDatareshape — these pages are two more thin forks by design, migrated to the engine when Pillar 4 lands (the flat section list here is deliberately engine-friendly).
5. Integration / wiring checklist
backend/prisma/schema.prisma—enum SubmissionType+=PEMBUBARAN_PT(comment: "distinct from PEMBUBARAN_PP"). The DocumentType additions (SURAT_LIKUIDATOR,BUKTI_PENGUMUMAN) arrive via the registry slice's single six-DocumentType additive batch (LAPORAN_KEUANGAN,LAMPIRAN_LAPORAN_TAHUNAN,SURAT_LIKUIDATOR,LAPORAN_LIKUIDASI,BUKTI_PENGUMUMAN,SURAT_PERMOHONAN), not a per-flow migration (C17). The domisili requiredDoc uses the existingDOMISILIenum (theSURAT_PERNYATAAN_DOMISILIlabel maps to it) — not a new type.Submission+=rupsKehadiranRegistry Json?;prisma generate(compile errors fan out to every exhaustive switch/Record).backend/src/flow-engine/flows/pembubaran-pt.ts— the FlowConfig (§4.4) + registry key inFLOW_REGISTRY(missing key = compile error).backend/src/services/akta-txn-classifier.ts—pembubaran→BUILT_AKTA_TXN_TYPES; add the appoints-vs-discharges cue toSYSTEM_PROMPT(§4.2).backend/src/routes/submissions.ts—inferSubmissionType: Berakhirnya doc-set predicate +AKTA_PEMBUBARAN → PEMBUBARAN_PTbranch (return-type union widens);mapToDocumentType+=SURAT_LIKUIDATOR/BUKTI_PENGUMUMAN(noteAKTA_PEMBUBARANalready maps →AKTAvia thestartsWith("AKTA_")rule, L30);getProcessingOrderslots.backend/src/services/submission-dispatch.ts—processorKindForTypecase"PEMBUBARAN_PT"→ the flow-engine branch (runFlow(id, FLOW_REGISTRY.PEMBUBARAN_PT)); theconst _exhaustive: nevertripwire fails compile until added.- reMatch dispatch —
queueReMatchAndValidate's type branch routesPEMBUBARAN_PT→reMatchAndValidateFlow(id, cfg)(same config, one registry two consumers). This is the validation-wipe gotcha (memoryreference_rematch_dispatch); with the registry, forgetting it is structurally impossible for v2 flows — but the v1 shim switch still needs the case while forks coexist. backend/src/routes/klasifikasi.ts—PEMBUBARAN_DOC_TYPES(table §4.6) + serve?type=pembubaran+LABEL_MAPentries.backend/src/services/document-processor.ts— theAKTA_LABEL_TO_SUBTYPEmap generalization (§4.3) + dispatchSURAT_LIKUIDATOR/BUKTI_PENGUMUMANto the new passes; scoped SURAT_LIKUIDATOR re-check pass (§4.5).backend/src/routes/perubahan.ts—PERUBAHAN_FAMILYset +="PEMBUBARAN_PT"(L32) so company-select / review-data / retry / advisory / voting admit it; thePEMBUBARAN_PTsubmit-gate block (§4.7); finalize allow-list admitsPEMBUBARAN_PT.rups-attendance-matcher.ts— acceptAKTA_PEMBUBARANclassifiedType + the registry-roster fallback param (§4.3).- Frontend —
lib/classifier-labels.ts(AKTA_PEMBUBARAN: "Akta Pembubaran",SURAT_LIKUIDATOR,BUKTI_PENGUMUMAN),components/klasifikasi/detect-banner.tsx(DetectedFlow+ labels/descriptions),pages/KlasifikasiPage.tsx(detectedFlow+ create-route),routes.tsx(pembubaran/:submissionId/{extraction,review,status}),lib/flow-steps.ts(PEMBUBARAN_LABELS, type union),lib/destination-for.ts(type union + route). - Verifikator dashboard/list filters recognize the new type label.
6. Data model / persistence (additive only)
- Enums:
SubmissionType += PEMBUBARAN_PT;DocumentType += SURAT_LIKUIDATOR, BUKTI_PENGUMUMAN. No removals. - Submission: reuse
companyLookupResult,oldData,selectedNomorTransaksi,selectedTahun(perubahan-family columns). New:rupsKehadiranRegistry Json?(§4.3 registry-roster attendance).changeTypeResult/selectedJenisPerubahanstay NULL for this type (unused by design). - Extractions: NO new tables — likuidator + koran values are
ExtractedFieldrows on their documents (§4.3), which buys confirm/bbox/override/rematch for free and keeps this flow migration-cheap for the Pillar-3 satellite split.BeritaAcaraRupsExtractionreused as-is. - Shared HITL:
PerubahanSectionApproval,VerifikasiPerubahan, audit events — no schema change, keyed by submission. - Additive
migrateonly; nodb push(respect the migration re-baseline once Persistence Phase-0 lands).
7. Error handling & gotchas
- reMatch registry note:
PEMBUBARAN_PTMUST be dispatched inqueueReMatchAndValidate(serialized, never fire-and-forget) or field edits silently wipe validations — with the FLOW_REGISTRY this is one registry lookup, but the coexistence shim (checklist #6) is the survival-critical line while v1 forks remain. - Section approvals: explicit persisted rows only; the submit gate reads
PerubahanSectionApproval— never derive from child confirmed/LOCKED state (regression c0698d8). - Exhaustive switches:
processorKindForType'snevercheck + the FLOW_REGISTRYRecord<SubmissionType,…>both fail compile untilPEMBUBARAN_PTis handled — add all cases before trusting green. - PEMBUBARAN_PP collision: grep-audit for sloppy
includes("PEMBUBARAN")string matching anywhere (routes, dashboards, filters) — the two types must never alias. Exact-equality checks only. - Koran is advisory:
PEMBUBARAN_KORANmust never FAIL and never block the step gate (severityFloor: 'WARNING'+ a test asserting it). - Classifier scope: the SURAT_LIKUIDATOR re-check pass runs only on submissions containing an
AKTA_PEMBUBARAN— a global pass could re-label docs in other flows. - Extraction override: the stored
classifiedType = 'AKTA_PEMBUBARAN'must map to subtype'perubahan'at extraction via the override map — never a fresh x056 re-classify (which would confidently disagree). - Berakhirnya packages: must gate to "belum didukung", never fall through into
PEMBUBARAN_PT(§4.2 predicate + test). - Registry outage: async rules already degrade to SKIPPED/WARNING per existing cross-validator behavior; the spesimen-style pool-wedge fix pattern (hard timeout + circuit breaker, memory
reference_apostille_registry_hang) applies toPT_ALREADY_DISSOLVED's local query trivially (local DB) and to SABH lookups via the existingsabh-dbclient. - PaddleOCR on newsprint: clipping extraction is best-effort (line-split gotcha); empty koran fields → manual entry in the Koran card, not an error state.
8. Testing strategy (TDD)
Tests first (superpowers:test-driven-development); DB tests target ahu_ocr_test via the bunfig preload — never the dev DB (memory feedback_test_db_isolation). Mirror the akuisisi suites (akuisisi-routing.test.ts, akuisisi-routes.test.ts).
- Routing/predicate:
AKTA_PEMBUBARAN → PEMBUBARAN_PT; pembubaran no longer "unsupported"; Berakhirnya predicate —AKTA_BERAKHIRNYAgates,AKTA_PEMBUBARAN + ≥2 BUKTI_PENGUMUMANgates,AKTA_PEMBUBARAN + 1 clippingroutes toPEMBUBARAN_PT; ladder regressions (Perbaikan/Akuisisi/Perubahan/PP unaffected); a Pembubaran deed the GPU labelsAKTA_PERUBAHANmust not producePERUBAHAN_PT(confident-wrong guard). - Registry entry:
FLOW_REGISTRY.PEMBUBARAN_PTexists (compile) +dispatchProcessor/queueReMatchAndValidateresolve it (the wipe-class test: edit a field, assert validations survive). - Likuidator extractor: unit fixtures (real redacted surat penunjukan + pertanggungjawaban OCR text) — 12 fields; grounding rejects hallucinated values; token-wise grounding accepts multi-line
dasar_penunjukan; deterministic RT/RW/kodepos/telepon regexes win over LLM output. - Koran pass: clipping fields extracted; inline-deed fallback fires only when zero clippings;
PEMBUBARAN_KORANemits WARNING (never FAIL) for absent, in-window PASS, out-of-window WARNING. - Rules:
PT_ALREADY_DISSOLVEDFAILs on a seeded COMPLETEDPEMBUBARAN_PTfor the sameid_perseroan, PASSes otherwise;PERSEROAN_STATEFAILs on status 5/6/blocked;LIKUIDATOR_KTP_MATCHFAIL-on-mismatch / WARNING-on-absent / PASS-on-pengurus-doubling; quorum uses ¾ for this flow (param) and >50% elsewhere (regression); tuned-out rules genuinely absent from persisted results (not SKIPPED rows). - RUPS fallback: matcher writes
rupsKehadiranRegistryfromoldData.pemegang_sahamwhen the deed roster is empty; deed-roster path still works. - Submit gate: all 6 required sections approved → COMPLETED +
VerifikasiPerubahan; any missing → 400 withunapprovedSections;pengumuman_korannot required. - Classification strategy: SURAT_LIKUIDATOR title rule + scoped re-check; BUKTI_PENGUMUMAN keyword pre-suggestion never auto-assigns.
- Regression:
PERUBAHAN_PT/AKUISISI_PTrouting, diff, and gates untouched;PEMBUBARAN_PPcompletely unaffected (exact-spelling audit test).
9. Deferred (phase 2 / out of PoC scope)
Berakhirnya Status flow (consumes BUKTI_PENGUMUMAN + will add LAPORAN_LIKUIDASI/SURAT_PERMOHONAN labels + nomor_pengumuman); court-path documents (penetapan pengadilan class); sisa_kekayaan; dedicated AKTA_PEMBUBARAN_SCHEMA; keberatan-kreditor surface; SABH submit-back; ReviewEngine descriptor for this flow (migrates with Pillar 4 — the flat sections here are engine-ready by construction).
10. Open questions / risks
- Dasar pembubaran attestation. SABH's
_formPembubaran.phpmay carry a UUPT Pasal 142 ground selection (keputusan RUPS / jangka waktu berakhir / penetapan pengadilan). The deep-read did not list it. Verify in the SABH views during implementation; if present, addpembubaranAttestation Json?(attestation-lite, akuisisi §4.5 pattern) — additive, cheap. - Does the perubahan extractor populate
akta_kedua+tanggal_rups+notaris_penggantifrom a real akta pembubaran? The schema carries the fields and the deed shapes are cousins, but verify on a sample deed before committing the hybrid — the fallback is a focused grounded header pass, not a new schema. - Registry-roster attendance matching quality.
oldData.pemegang_sahamnames vs BA-RUPS attendee spellings may diverge more than deed-internal names do; thenormalizeName+ honorific stripping may need a fuzzy tier. Measure on real BA RUPS fixtures. - Likuidator-doubling ambiguity. When the likuidator is also a pengurus, one KTP serves two matches — confirm
LIKUIDATOR_KTP_MATCHaccepting an existing identity-match KTP is the right call (current design: yes). - Court-initiated filings. If SABH's wizard accepts penetapan-pengadilan dissolutions without a RUPS block, our BA-RUPS "recommended" framing is fine, but the quorum rule should SKIP cleanly (it does — no BA doc → SKIPPED). Confirm no harder gate exists on that path.
- Sequencing. This flow is born on the FLOW_REGISTRY slice; if the slice build slips, hold the flow (do not fork) — the binding no-new-forks decision makes the slice the critical path.
Cross-spec reconciliation (2026-07-02)
Applied from .superpowers/sdd/spec-critique-full.md:
- C2 — the Surat Likuidator extractor narrowed to the penunjukan shape ONLY (§4.3); the "covers BOTH letter shapes / shape-agnostic" claim removed. The laporan-akhir/pertanggungjawaban is the DISTINCT LAPORAN_LIKUIDASI DocumentType (built in Berakhirnya, incl. sisa_kekayaan) — the two are kept distinct because the §4.2 doc-set predicate keys on the distinction.
- C3 — BUKTI_PENGUMUMAN board classification switched to the ONE SHARED deterministic keyword pre-suggester (UNION keyword set PENGUMUMAN + PELEBURAN|KONSOLIDASI|PEMBUBARAN|LIKUIDASI|RUPS|RINGKASAN RANCANGAN + masthead shape, built in Peleburan), never auto-accepted, no LLM (§4.5).
- C4 — koran extraction now references the ONE SHARED clipping extractor bukti-pengumuman-extract.ts with the canonical field keys nama_koran / tanggal_pengumuman / nomor_pengumuman (replacing the earlier per-flow date fieldKey and the koran-clipping extractor name); the inline-deed fallback uses the shared parameterized extractKoranFields(rawText, {context}) refactor (§4.3, §4.6 rule).
- C5 — the Pembubaran↔Berakhirnya doc-set predicate rewritten to the canonical form (adds the SURAT_PERMOHONAN and BUKTI_PENGUMUMAN_152 clauses; the ≥2 count now matches the carrier DocumentType via mapToDocumentType, not the raw label string) and stated as defined ONCE in submissions.ts, quoted identically in the Berakhirnya spec (§4.2).
- C9 — required-docs table already matched the canonical resolution (required: AKTA_PEMBUBARAN, SURAT_LIKUIDATOR, KTP, NPWP, DOMISILI, DATA_KONTAK; optional: BA_RUPS, BUKTI_PENGUMUMAN); PEMBUBARAN_KORAN stays WARNING-never-FAIL (Ps.147) — no change needed.
- C11 — §4.4 FlowConfig conformed to the registry slice's canonical shapes: primaryAkta loses subtype (→ extraction.overrideByClassifiedType), extraction.passes[] → focusedPasses, rematch → canonical RematchConfig (checklistResetDocTypes/standardPtMatchers/preValidationSteps; queueing is engine-level), requiredDocs added.
- C12 — routeFamily: 'perubahan' (the one already-valid value) confirmed; Pembubaran keeps riding the perubahan family.
- C14 — the RUPS quorum wrapper passes quorumThreshold: 3/4 (Ps.89) via the shared rups-quorum.ts param seam; default stays >1/2 (Ps.86); registry-roster (oldData) attendance fallback is the shared one.
- C15 — all ladder mentions now quote the canonical ladder (Perbaikan > Berakhirnya(doc-set predicate) > Peleburan > Akuisisi > Pembubaran > Perubahan > LaporanRupsTahunan > PendirianPP > PendirianPT).
- C16 — "ideal first v2 flow" reworded: Pembubaran is the third v2-generic tenant per the program ship order; it flips only its OWN BUILT_AKTA_TXN_TYPES entry and asserts Berakhirnya stays gated only until Berakhirnya's own flow ships (not permanently).
- C17 — DocumentTypes arrive via the registry slice's single six-type additive batch; DOMISILI (existing) is the domisili carrier (not a new SURAT_PERNYATAAN_DOMISILI type) in the typed requiredDocs/checklistResetDocTypes arrays.