think
16px
820px

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, branch fix/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_RUPSLAPORAN_LIKUIDASI (hanya bila head-anchored autoAcceptable);
- suggestion tail untuk label confidently-wrong + kasus AKTA_BERAKHIRNYASURAT_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:

  1. 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 di CONFIDENTLY_WRONG_SUPPORTING_LABELS).
  2. Anchor namaPerseroan menyelamatkan IKLAN KORAN III: whole-page → _149
    (salah, persis yang user lihat); dengan anchor "PT MAJA JAYA" → _152 ✓.
  3. 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 penuh keberatan/tagihan milik PT lain), Koran II GT
    _149 tersaran _152 (halaman penuh box hasil akhir likuidasi PT lain).
  4. Frasa "sisa HASIL LIKUIDASI" khas pengumuman Ps.149 ("Pengumuman Rencana
    Pembagian Sisa Hasil Likuidasi…") match AXIS_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.
  5. 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 → tersaran SURAT_PERMOHONAN (salah).
  6. Regex permohonan tidak meng-cover judul "Permohonan Pemberitahuan Berakhirnya…":
    alternation head tidak memuat PEMBERITAHUAN setelah PERMOHONAN, dan jarak
    PERMOHONAN[\s\S]{0,30}BADAN HUKUM terlalu pendek untuk
    "Permohonan Pemberitahuan Berakhirnya Status Badan Hukum" (±34 char).
  7. Rule koran terlalu rakus terhadap akta: akta PKR/PENEGASAN sample-3 (yang
    text-layer-nya utuh) whole-page tersaran BUKTI_PENGUMUMAN_152 karena akta
    MENGUTIP pengumuman koran. Hari ini aman (label GPU AKTA_* dilindungi gate),
    tapi jadi bom kalau GPU salah melabeli akta sebagai OTHER.
  8. Axis undefined menyarankan BUKTI_PENGUMUMAN polos — lane itu tidak ada di
    BERAKHIRNYA_DOC_TYPES; kalau operator klik chip-nya, file malah pindah ke
    "Tidak Dikenali".
  9. Threading anchor kalah urutan: runUploadAndClassify (classify-button.tsx)
    memproses file berurutan dan hint namaPerseroan baru terisi SETELAH akta
    terklasifikasi — padahal urutan nama file menempatkan Iklan…/IKLAN… SEBELUM
    SCAN AKTA…. Path "Tambah Dokumen" (KlasifikasiPage.handleAddFiles) malah
    tidak pernah mengirim hint dan tidak membaca aktaPtName sama 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_LABELSjangan disentuh.
  • Perilaku BERITA_ACARA_RUPS di flow lain: rescue BA-RUPS→LAPORAN_LIKUIDASI harus
    tetap hanya fire pada head-anchored autoAcceptable laporan (guard regression test
    yang sudah ada di klasifikasi-berakhirnya-suggest.test.ts wajib tetap hijau).
  • BUKTI_PENGUMUMAN generik dipakai flow pembubaran/peleburan/penggabungan
    (maxCount 1, suggest-only) — jangan mengubah semantiknya untuk mereka; axis
    _149/_152 hanya bermakna di paket berakhirnya.
  • Kontrak API POST /classify/:fileId bersifat 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)

  1. 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.)
  2. 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 }),
    simpan rawText, lalu jalankan suggestByKeyword dengan dan tanpa anchor nama PT.
  3. 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_LABELS dengan
    komentar WHY bergaya codebase
    (label sah di flow pendirian; aman karena
    re-examine hanya mengadopsi hit autoAcceptable dan rule pernyataan-likuidasi
    butuh PERNYATAAN + LIKUID + frasa penyelesaian yang 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: true HANYA 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-run suggestByKeyword(ocrText, { namaPerseroan }) dan kembalikan
    suggestions { fileId, docType, displayName }. OCR text sudah pernah dihitung di
    classify — pertimbangkan cache teks per fileId di TEMP_DIR (mis. simpan rawText
    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), ambil aktaPtName (SIMPAN dulu field ini ke
    ClassificationResult — sekarang dibuang di runUploadAndClassify dan tidak ada
    di path handleAddFiles), lalu panggil endpoint untuk file yang mendarat
    OTHER / Tidak Dikenali / ber-suggestion BUKTI_PENGUMUMAN*, dan perbarui chip.
    Terapkan juga untuk path "Tambah Dokumen".
  • Threading inline yang sudah ada di runUploadAndClassify boleh 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):

  1. 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).
  2. Perbaiki keyword set: keluarkan HASIL\s+LIKUIDASI dari axis 152 (bentrok dengan
    "sisa hasil likuidasi" khas 149); pakai HASIL\s+AKHIR|PERTANGGUNGJAWABAN|PEMBERESAN
    untuk 152; tambah RENCANA\s+PEMBAGIAN ke 149 (bersama KREDITOR/TAGIHAN/KEBERATAN/PAILIT/KURATOR).
  3. 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 kemunculan PENGUMUMAN ber-box / teks panjang — kalibrasi
    ambangnya dari matriks), kembalikan fineLabel: undefined — axis PT lain bukan
    axis kita. Whole-page scan tetap untuk teks pendek/kliping tunggal.
  4. Seri/ambigu → undefined, jangan default _152 ketika ada sinyal 149 sama kuat
    (aturan lama "tie → _152" terbukti salah arah pada box "rencana pembagian sisa
    hasil likuidasi"). Perbarui komentar baseline di atas matchPengumumanAxis
    dengan tanggal + hasil kalibrasi baru, meniru gaya komentar yang ada.
  5. Chip axis-undefined jangan menawarkan lane yang tidak ada (temuan #8): saat
    flow berakhirnya dan fineLabel undefined, jangan sarankan BUKTI_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 ke shapeSuggestion dan 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, split rawText dengan 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 PEMBUBARAN dari daftar lampiran (sample-3) — daftar lampiran tidak
    punya struktur surat (PERIHAL/Kepada), jadi tambahkan guard struktur pada rule
    PEMBERITAHUAN\s+PEMBUBARAN (harus disertai KEPADA|PERIHAL|DIREKTUR|MENTERI|KEMENTERIAN
    di halaman yang sama).
  • Lengkapi regex permohonan: tambah PEMBERITAHUAN ke alternation
    (PERMOHONAN\s+(BERAKHIR|PENGHAPUSAN|PEMBUBARAN|PENERBITAN|PEMBERITAHUAN)) dan
    longgarkan jarak {0,30}{0,60} pada PERMOHONAN[\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}ACCOUNTABILITY ikut 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 label AKTA_* 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

  1. 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).
  2. Unit test baru co-located di __tests__/ mengikuti gaya deskriptif existing
    (lihat supporting-doc-classifier.test.ts, klasifikasi-berakhirnya-suggest.test.ts).
  3. Ulangi harness Tahap 0 → matriks akhir; lampirkan di laporan.
  4. 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.
  5. 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 (pola classify: parameter di refineSupportingLabel).
  • Commit kecil per tahap, format existing: fix(berakhirnya): … /
    feat(berakhirnya): … / test(berakhirnya): …, body menjelaskan WHY, diakhiri
    Co-Authored-By sesuai konvensi sesi.
  • Soft-fail: semua tier saran wajib try/catch dan jatuh kembali ke label input
    (pola log.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 --noEmit bersih di backend & frontend.
  • [ ] Laporan akhir + limitation OCR di-upload ke think-server, commit di-push.