PLAN — Perbaiki logic klasifikasi paket Berakhirnya Status Badan Hukum PT
ADDENDUM 2026-07-16 (baca dulu): arah diputuskan HYBRID — formulir template BSBHP
(kode BSBHP-03..06, pola sp-pendirian-pp) jadi jalur utama untuk Surat Permohonan,
Laporan Likuidasi, dan lembar sampul Bukti Pengumuman 149/152; plan classifier di bawah
tetap dikerjakan sebagai FALLBACK dengan scope yang di-re-scope. Desain template, rule
pengenalan kode formulir, dan tabel re-scope per tahap:
https://x056.think.val.id/TEMPLATE-BSBHP-design.md — kerjakan desain template itu lebih
dulu, lalu tahap plan ini sesuai kolom "Status baru" di tabel re-scope-nya.Untuk: agen Opus 4.8 di VM, repo
ahu-ocr-akta-notaris-POC, branchfix/berakhirnya-status-badan-hukum(sudah di origin).
Tujuan: semua dokumen di 5 sample transaksi terklasifikasi benar ke jenisnya di halaman Klasifikasi Dokumen — auto-mendarat bila sinyal kuat, minimal chip "Saran:" yang BENAR bila tidak — tanpa menyentuh perilaku flow lain.
Baseline analisis: 2026-07-16, dari reproduksi UI user (sample-1 / PT MAJA JAYA) + harness keyword offline terhadap text-layer PDF. Peta ground truth per sample: https://x056.think.val.id/SAMPLES.md
Status plan: disetujui user; kerjakan sampai selesai, verifikasi Playwright boleh didelegasikan ke Sonnet.
0. Konteks — apa yang terjadi & kenapa fix kemarin belum cukup
Commit 41eab6d4 ("classify supporting docs despite confidently-wrong GPU labels") menambah:
- CONFIDENTLY_WRONG_SUPPORTING_LABELS = {OTHER, SURAT_KETERANGAN_BANK, BUKTI_TRANSFER-SURAT_KETERANGAN_BANK} → re-examine by shape tanpa peduli confidence;
- rescue GPU BERITA_ACARA_RUPS → LAPORAN_LIKUIDASI (hanya bila head-anchored autoAcceptable);
- suggestion tail untuk label confidently-wrong + kasus AKTA_BERAKHIRNYA→SURAT_PERMOHONAN.
Reproduksi user dengan sample-1 (PT MAJA JAYA) tetap gagal:
| File (GT) | Hasil di board | Akar masalah |
|---|---|---|
| SCAN AKTA PERUBAHAN II (AKTA_BERAKHIRNYA) | ✅ auto 100% | — |
| SCAN SIRKULER (BERITA_ACARA_RUPS) | ✅ auto 99.9% | — |
| SURAT PERNYATAAN DAN NERACA (LAPORAN_LIKUIDASI) | ❌ Tidak Dikenali, "Classifier: Surat Per…" @99.8% | Gap A |
| SCAN PEMBERITAHUAN PEMBUBARAN (SURAT_PERMOHONAN) | ❌ tidak auto (chip saja by design) | Gap E |
| Iklan Koran II (BUKTI_PENGUMUMAN_149) | ❌ ke "Dokumen lain", saran axis salah | Gap C + D |
| IKLAN KORAN III (BUKTI_PENGUMUMAN_152) | ❌ ke "Dokumen lain", tersaran _149 |
Gap B |
Klaim "Laporan Likuidasi sudah auto ✅" hanya benar untuk laporan berjudul literal
"Laporan Pertanggungjawaban…" yang GPU-nya kebetulan bilang BERITA_ACARA_RUPS
(bentuk di sample-5). Tiga bentuk laporan lain (pernyataan bermeterai, bilingual,
dwifungsi) tidak tertangani.
1. Temuan terverifikasi (harness offline, text-layer pdftotext)
Harness: panggil suggestByKeyword(text) dan suggestByKeyword(text, { namaPerseroan })
langsung terhadap teks tiap PDF sample. PERINGATAN: runtime memakai PaddleOCR
(getLayoutProvider("paddleocr").extractTextWithWords), bukan text-layer PDF — sebelum
mengubah kalibrasi apa pun, bangun ulang baseline dengan teks PaddleOCR (Tahap 0).
Temuan text-layer ini adalah hipotesis kuat, bukan kebenaran runtime:
- Laporan varian "SURAT PERNYATAAN…" (sample-1) sudah dikenali keyword rule →
LAPORAN_LIKUIDASI @0.75("pernyataan hasil akhir likuidasi") — tapi rule ini
TIDAK PERNAH jalan karena GPU melabeli file itu "Surat Per…" @0.998 (label
high-confidence yang tidak ada diCONFIDENTLY_WRONG_SUPPORTING_LABELS). - Anchor
namaPerseroanmenyelamatkan IKLAN KORAN III: whole-page →_149
(salah, persis yang user lihat); dengan anchor "PT MAJA JAYA" →_152✓. - Box iklan pemohon sering TIDAK ADA di teks — di sample-1 Koran II, sample-2
2025-11-26-4, sample-4 kedua koran: nama PT pemohon 0 hit di text-layer
(box kecil di pojok halaman, luput OCR), sementara box PT LAIN mendominasi.
Akibatnya whole-page fallback menghitung iklan PT lain: dua koran GT_152
tersaran_149(halaman penuhkeberatan/tagihanmilik PT lain), Koran II GT
_149tersaran_152(halaman penuh boxhasil akhir likuidasiPT lain). - Frasa "sisa HASIL LIKUIDASI" khas pengumuman Ps.149 ("Pengumuman Rencana
Pembagian Sisa Hasil Likuidasi…") matchAXIS_152_RE(HASIL\s+LIKUIDASI) →
box 149 asli pun bisa kalah/seri, dan seri jatuh ke_152. Box-box asli justru
sering menyebut literal "pasal 149" / "Pasal 152 Undang-Undang" — sinyal
terkuat yang belum dipakai axis. - PDF bundel lampiran-di-depan mematahkan head-anchor 800 char:
- Surat permohonan sample-5 halaman 1 = Pencabutan NIB OSS → semua rule miss (null);
- Laporan bilingual sample-3 halaman 1 = daftar Lampiran/Appendix yang memuat
"penerimaan pemberitahuan pembubaran" → false-match rule head
PEMBERITAHUAN\s+PEMBUBARAN→ tersaranSURAT_PERMOHONAN(salah). - Regex permohonan tidak meng-cover judul "Permohonan Pemberitahuan Berakhirnya…":
alternation head tidak memuatPEMBERITAHUANsetelahPERMOHONAN, dan jarak
PERMOHONAN[\s\S]{0,30}BADAN HUKUMterlalu pendek untuk
"Permohonan Pemberitahuan Berakhirnya Status Badan Hukum" (±34 char). - Rule koran terlalu rakus terhadap akta: akta PKR/PENEGASAN sample-3 (yang
text-layer-nya utuh) whole-page tersaranBUKTI_PENGUMUMAN_152karena akta
MENGUTIP pengumuman koran. Hari ini aman (label GPUAKTA_*dilindungi gate),
tapi jadi bom kalau GPU salah melabeli akta sebagai OTHER. - Axis undefined menyarankan
BUKTI_PENGUMUMANpolos — lane itu tidak ada di
BERAKHIRNYA_DOC_TYPES; kalau operator klik chip-nya, file malah pindah ke
"Tidak Dikenali". - Threading anchor kalah urutan:
runUploadAndClassify(classify-button.tsx)
memproses file berurutan dan hintnamaPerseroanbaru terisi SETELAH akta
terklasifikasi — padahal urutan nama file menempatkanIklan…/IKLAN…SEBELUM
SCAN AKTA…. Path "Tambah Dokumen" (KlasifikasiPage.handleAddFiles) malah
tidak pernah mengirim hint dan tidak membacaaktaPtNamesama sekali.
2. Guardrail — yang TIDAK boleh berubah
Prinsip: semua perubahan perilaku harus tergate pada konteks berakhirnya
(label GPU yang salah-untuk-berakhirnya, shape keyword berakhirnya, atau flow yang
terdeteksi berakhirnya). Flow lain (pendirian, perubahan, perbaikan, PP, akuisisi,
laporan RUPS, peleburan, penggabungan, pembubaran, peralihan PP→PT, apostille,
sigverify) harus lolos test suite existing TANPA perubahan snapshot/ekspektasi.
refineAktaLabel, tier peralihan-PP,extractJenisPerubahanEarly, seluruh
CONTAMINATED_COARSE_LABELS— jangan disentuh.- Perilaku
BERITA_ACARA_RUPSdi flow lain: rescue BA-RUPS→LAPORAN_LIKUIDASI harus
tetap hanya fire pada head-anchored autoAcceptable laporan (guard regression test
yang sudah ada diklasifikasi-berakhirnya-suggest.test.tswajib tetap hijau). BUKTI_PENGUMUMANgenerik dipakai flow pembubaran/peleburan/penggabungan
(maxCount 1, suggest-only) — jangan mengubah semantiknya untuk mereka; axis
_149/_152hanya bermakna di paket berakhirnya.- Kontrak API
POST /classify/:fileIdbersifat aditif — field baru boleh, mengubah
field lama tidak. - Jangan retrain / menyentuh GPU classifier (ADR-0012) — semua perbaikan di layer
keyword/LLM/wiring.
File yang boleh diubah (perkiraan; kalau butuh lebih, pastikan tergate berakhirnya):
- backend/src/services/supporting-doc-classifier.ts (+ test)
- backend/src/services/berakhirnya-doc-classifier.ts (+ test)
- backend/src/routes/klasifikasi.ts — bagian refine/suggest/endpoint berakhirnya saja (+ test)
- frontend/src/components/klasifikasi/classify-button.tsx
- frontend/src/pages/KlasifikasiPage.tsx (threading hint + path Tambah Dokumen)
- frontend/src/components/klasifikasi/file-card.tsx HANYA bila perlu untuk chip axis
3. Tahapan kerja
Tahap 0 — Baseline runtime (WAJIB sebelum kalibrasi apa pun)
- Rekonstruksi sample di VM bila belum ada (satu bundle per nomor transaksi; token
angka paling akhir TIDAK seragam — sample-3_1781232287/88, sample-5
_1782281591/92— jadi kunci = prefix nomor transaksi):
bash cd training_data for pair in "sample-1:4026061231700061" "sample-2:4026061135700054" \ "sample-3:4026061231700058" "sample-4:4026061831700078" \ "sample-5:4026062431700115"; do dir="${pair%%:*}"; txn="${pair##*:}"; mkdir -p "$dir" for d in AKTA_BERAKHIRNYA BERITA_ACARA_RUPS BUKTI_PENGUMUMAN_149 \ BUKTI_PENGUMUMAN_152 LAPORAN_LIKUIDASI SURAT_PERMOHONAN; do cp -n "$d/${txn}_"*.pdf "$dir/" 2>/dev/null done done
(sample-4 = 5 file: BAR-nya dwifungsi BA RUPS + LAPORAN_LIKUIDASI, md5 identik.) - Buat script harness (mis.
backend/scripts/berakhirnya-calibration.ts, atau folder
scratch — JANGAN commit hasil OCR): untuk tiap PDF sample, jalankan
getLayoutProvider("paddleocr").extractTextWithWords(buffer, { unwarp:false, orient:false }),
simpanrawText, lalu jalankansuggestByKeyworddengan dan tanpa anchor nama PT. - Catat matriks hasil vs ground truth (SAMPLES.md). Semua keputusan kalibrasi
Tahap 3–4 diambil dari matriks PaddleOCR ini, bukan dari temuan text-layer §1.
Khusus dicek: apakah PaddleOCR membaca box iklan pemohon yang luput di text-layer
(sample-1 Koran II pojok kanan-bawah; kedua koran sample-4). Kalau PaddleOCR juga
miss, catat file-nya — itu batas atas akurasi otomatis, bukan bug logic.
Tahap 1 (Gap A) — Laporan varian "surat pernyataan" tidak pernah di-re-examine
GPU melabeli "SURAT PERNYATAAN DAN NERACA" (dan kemungkinan "surat pery hasil
likuidasi" sample-2) sebagai label SURAT_PERNYATAAN*/BUKTI_TRANSFER-SURAT_PERNYATAAN_SETOR-sejenis
dengan confidence ~0.998 → lolos semua gate. Perbaikan:
- Verifikasi dulu label GPU persisnya via harness Tahap 0 (log
rawClassification
dari endpoint classify, atau panggil classifier URL langsung). - Tambahkan label tersebut ke
CONFIDENTLY_WRONG_SUPPORTING_LABELSdengan
komentar WHY bergaya codebase (label sah di flow pendirian; aman karena
re-examine hanya mengadopsi hit autoAcceptable dan rule pernyataan-likuidasi
butuhPERNYATAAN + LIKUID + frasa penyelesaianyang tidak ada di surat setor
pendirian asli → no-op untuk mereka). Tulis unit test yang MEMBUKTIKAN no-op itu:
teks surat pernyataan setor pendirian sungguhan → label tetap, suggestion null. - Pertimbangkan menaikkan rule "pernyataan hasil akhir likuidasi" menjadi
autoAcceptable: trueHANYA bila matriks Tahap 0 menunjukkan presisi 100% pada
sample yang ada + tidak menabrak dokumen lain; kalau ragu, biarkan chip.
Tahap 2 (Gap B/#9) — Anchoring namaPerseroan yang benar (second pass)
Threading berbasis urutan upload itu rapuh. Ikuti pola yang SUDAH ADA di codebase:
endpoint post-hoc POST /api/klasifikasi/recheck-likuidator + effect flow.phase === "done"
di KlasifikasiPage (lihat recheckedRef). Buat pass kedua serupa:
- Backend: endpoint
POST /api/klasifikasi/recheck-berakhirnya-axis(nama ikut
konvensi recheck-*): body{ fileIds: string[], namaPerseroan: string }→ untuk
tiap file, re-runsuggestByKeyword(ocrText, { namaPerseroan })dan kembalikan
suggestions{ fileId, docType, displayName }. OCR text sudah pernah dihitung di
classify — pertimbangkan cache teks per fileId di TEMP_DIR (mis. simpanrawText
saat classify ke{fileId}.ocr.txt) supaya pass kedua tidak OCR ulang; kalau
menambah cache, bersihkan di DELETE /file/:fileId. - Frontend: setelah batch selesai (phase done) dan ada hasil
AKTA_BERAKHIRNYA
(atau flow berakhirnya terdeteksi), ambilaktaPtName(SIMPAN dulu field ini ke
ClassificationResult— sekarang dibuang dirunUploadAndClassifydan tidak ada
di pathhandleAddFiles), lalu panggil endpoint untuk file yang mendarat
OTHER / Tidak Dikenali / ber-suggestionBUKTI_PENGUMUMAN*, dan perbarui chip.
Terapkan juga untuk path "Tambah Dokumen". - Threading inline yang sudah ada di
runUploadAndClassifyboleh tetap (harmless);
pass kedua yang menjadikannya deterministik terhadap urutan.
Tahap 3 (Gap C/#3–4) — Rekalibrasi axis 149/152
Urutan sinyal baru (kalibrasi final WAJIB dari matriks PaddleOCR Tahap 0):
- Literal pasal menang mutlak di teks yang discan:
PASAL\s*149→_149,
PASAL\s*152→_152(kalau keduanya muncul di window beranchor, majority;
masih seri → undefined). - Perbaiki keyword set: keluarkan
HASIL\s+LIKUIDASIdari axis 152 (bentrok dengan
"sisa hasil likuidasi" khas 149); pakaiHASIL\s+AKHIR|PERTANGGUNGJAWABAN|PEMBERESAN
untuk 152; tambahRENCANA\s+PEMBAGIANke 149 (bersama KREDITOR/TAGIHAN/KEBERATAN/PAILIT/KURATOR). - Fallback whole-page hanya untuk kliping, bukan halaman penuh: kalau anchor
diberikan tapi nama TIDAK ditemukan, dan teks terlihat seperti halaman koran penuh
(heuristik: >1 kemunculanPENGUMUMANber-box / teks panjang — kalibrasi
ambangnya dari matriks), kembalikanfineLabel: undefined— axis PT lain bukan
axis kita. Whole-page scan tetap untuk teks pendek/kliping tunggal. - Seri/ambigu → undefined, jangan default
_152ketika ada sinyal 149 sama kuat
(aturan lama "tie → _152" terbukti salah arah pada box "rencana pembagian sisa
hasil likuidasi"). Perbarui komentar baseline di atasmatchPengumumanAxis
dengan tanggal + hasil kalibrasi baru, meniru gaya komentar yang ada. - Chip axis-undefined jangan menawarkan lane yang tidak ada (temuan #8): saat
flow berakhirnya danfineLabelundefined, jangan sarankanBUKTI_PENGUMUMAN
polos — tahan chip generik "Bukti Pengumuman Koran" hanya bila lane-nya memang
ada di label set aktif, atau sarankan kedua lane via UI yang ada sekarang
(paling sederhana: tanpa fineLabel → tidak ada chip; operator drag manual).
Pilih yang paling kecil sentuhannya keshapeSuggestiondan uji.
Tahap 4 (Gap D/#5–7) — Head-anchor yang sadar-bundel
- Scan per halaman, bukan hanya 800 char pertama dokumen:
extractTextWithWords
menyediakan struktur halaman (kalau tidak, splitrawTextdengan penanda halaman
provider). Rule judul laporan/permohonan dicoba pada head tiap halaman (batasi
mis. 5 halaman pertama), first-match per prioritas rule tetap: laporan > permohonan.
Ini menyelesaikan bundel "Pencabutan NIB dulu" (sample-5) dan mengurangi false
PEMBERITAHUAN PEMBUBARANdari daftar lampiran (sample-3) — daftar lampiran tidak
punya struktur surat (PERIHAL/Kepada), jadi tambahkan guard struktur pada rule
PEMBERITAHUAN\s+PEMBUBARAN(harus disertaiKEPADA|PERIHAL|DIREKTUR|MENTERI|KEMENTERIAN
di halaman yang sama). - Lengkapi regex permohonan: tambah
PEMBERITAHUANke alternation
(PERMOHONAN\s+(BERAKHIR|PENGHAPUSAN|PEMBUBARAN|PENERBITAN|PEMBERITAHUAN)) dan
longgarkan jarak{0,30}→{0,60}padaPERMOHONAN[\s\S]{0,60}BADAN\s+HUKUM.
Tambah test dengan judul persis "Permohonan Pemberitahuan Berakhirnya Status
Badan Hukum". - Laporan bilingual: pastikan rule
LIQUIDATOR.{0,20}ACCOUNTABILITYikut discan
per halaman sehingga halaman judul report menang sebelum lampiran match permohonan. - Jangan longgarkan rule koran terhadap akta (temuan #7): kalau menyentuh rule
PENGUMUMAN, tambahkan guard bahwa dokumen ber-head akta ("AKTA", "Nomor …",
struktur notaris) tidak disarankan koran — cukup lewat urutan rule/head test,
jangan menyentuh jalur labelAKTA_*yang sudah dilindungi.
Tahap 5 (Gap E) — Kebijakan auto-adopt
Setelah Tahap 1–4 hijau di matriks PaddleOCR:
- SURAT_PERMOHONAN: kandidat auto-adopt bila match rule head/struktur surat DAN
label GPU-nya termasuk set salah-untuk-berakhirnya (bukan AKTA_* asli yang
cuma low-confidence). Head akta asli tidak pernah match rule permohonan (sudah
jadi premis branch AKTA_BERAKHIRNYA→chip) — kalau matriks menunjukkan 0 false
positive pada 5 sample + test flow lain, boleh naikkan autoAcceptable.
- Koran _149/_152: auto-adopt HANYA bila literal PASAL 149/152 ditemukan di
window beranchor nama PT pemohon (sinyal ganda). Selain itu tetap chip — OCR
koran memang rawan dan operator tetap pemutus (desain yang disepakati).
- Laporan: lihat Tahap 1. Kasus dwifungsi sample-4 (1 PDF = BAR + laporan): JANGAN
auto-split; biarkan mendarat BA RUPS dan pastikan checklist kelengkapan
memperlakukan laporan sebagai terpenuhi lewat mekanisme dwifungsi yang sudah
dirancang di BSBHP_REQUIRED_DOCS (Tahap D delta-plan) — cek rule engine-nya,
jangan diduplikasi di classifier.
Tahap 6 — Verifikasi & regresi
cd backend && bunx tsc --noEmit && bun test— SEMUA test lama hijau tanpa
mengubah ekspektasi flow lain;cd frontend && bunx tsc --noEmit(+ test FE yang ada).- Unit test baru co-located di
__tests__/mengikuti gaya deskriptif existing
(lihatsupporting-doc-classifier.test.ts,klasifikasi-berakhirnya-suggest.test.ts). - Ulangi harness Tahap 0 → matriks akhir; lampirkan di laporan.
- Playwright e2e per sample (boleh delegasi ke Sonnet): upload seluruh isi
sample-N/sebagai satu batch → screenshot board → bandingkan lane/chip dengan
SAMPLES.md. Target per sample: semua file di lane benar ATAU ber-chip saran benar;
sample-4 dihitung benar bila BAR diterima sebagai BA RUPS + kebutuhan laporan
terpenuhi dwifungsi. Uji JUGA path "Tambah Dokumen" untuk minimal satu koran. - Laporan akhir
.md: matriks sebelum/sesudah per file per sample, daftar file yang
mentok di batas OCR (box iklan tak terbaca PaddleOCR) — itu didokumentasikan
sebagai limitation, bukan disembunyikan. Upload ke think-server sesuai konvensi
CLAUDE.md repo.
4. Konvensi yang wajib diikuti
- Gaya komentar codebase: komentar menjelaskan WHY + tanggal baseline/kejadian
nyata (contoh yang ada: "baseline 2026-07-15", "real 2026-07-09: …"). Setiap
perubahan kalibrasi mencantumkan tanggal + hasil matriks yang mendasarinya.
Jangan menulis komentar naratif "perubahan ini memperbaiki…". - Bun + Hono, test via
bun test, co-located__tests__, injectable deps untuk
test (polaclassify:parameter direfineSupportingLabel). - Commit kecil per tahap, format existing:
fix(berakhirnya): …/
feat(berakhirnya): …/test(berakhirnya): …, body menjelaskan WHY, diakhiri
Co-Authored-Bysesuai konvensi sesi. - Soft-fail: semua tier saran wajib try/catch dan jatuh kembali ke label input
(polalog.warn("[klasifikasi] …failed, keeping label")). - Tipe FE/BE diduplikasi (tidak ada shared package) — kalau menambah field response,
update kedua sisi. - Jangan commit hasil OCR/artefak harness;
.pw-scratch/tetap tidak di-commit. - Push ke
fix/berakhirnya-status-badan-hukum(JANGAN force-push; branch dibaca
dari Mac juga).
5. Definition of Done
- [ ] Matriks PaddleOCR Tahap 0 dibuat SEBELUM kalibrasi, dan matriks akhir sesudahnya.
- [ ] Sample-1: 6/6 benar (laporan pernyataan & permohonan mendarat/ber-chip benar;
IKLAN KORAN III ber-axis_152; Koran II minimal ber-chip koran — axis benar
bila PaddleOCR membaca box-nya). - [ ] Sample-2..5: semua file di lane benar atau ber-chip saran benar; dwifungsi
sample-4 terpenuhi; laporan bilingual sample-3 TIDAK lagi tersaran permohonan. - [ ] Tidak ada chip yang menyarankan lane yang tidak ada di label set aktif.
- [ ] Seluruh test suite lama hijau tanpa mengubah ekspektasi flow non-berakhirnya;
test baru membuktikan no-op untuk surat pernyataan setor pendirian & BA RUPS
flow lain. - [ ]
bunx tsc --noEmitbersih di backend & frontend. - [ ] Laporan akhir + limitation OCR di-upload ke think-server, commit di-push.