think
16px
820px

Hasil Audit — Checklist Debug Flow Pendirian PT

Sumber checklist: checklist-debug-pendirian-pt.md
Repo yang diaudit: ahu-ocr-dev/dev_5 (branch dev/5, HEAD dd783691)
Metode: audit kode statis + hasil tes manual. Tidak ada kode yang diubah.
Tanggal: 2026-08-04

Bagian 0 (pra-cek penghapusan Domisili & Surat Pernyataan) sudah dihapus dari laporan ini — task 86ex78yf7 di-revert, Domisili & Surat Pernyataan Setor tetap wajib. Item domisili di bagian lain ditandai N/A.

Penomoran temuan memakai satu skema: B = blocker, M = major, N = minor, D = perlu keputusan.

Status Arti
PASS Terverifikasi, disertai path + baris
FAIL Terverifikasi tidak sesuai, disertai path + baris
MANUAL Perlu dijalankan / dilihat di browser
N/A Tidak relevan setelah revert

Kolom Sumber: M = tes manual, K = bukti kode, M+K = keduanya sepakat.


Ringkasan

115 item · 84 PASS · 12 FAIL · 4 MANUAL · 15 N/A · 0 perlu keputusan

Lima blocker:

  1. B1 — tidak ada UI override validasi, padahal finalize memblokir setiap FAIL yang belum di-override. Notaris bisa terkunci tanpa jalan keluar.
  2. B2 — Step 3 tidak punya jalur error sama sekali; ID tidak valid berakhir di spinner abadi.
  3. B4 — kewajiban kelima sub-tipe Bukti Setor hanya ditegakkan di Step 2; Step 3 dan Step 4 bocor.
  4. B5 — Akta Pendirian tidak bisa diganti. Hapus lalu unggah ulang → 400 Cannot remove Akta Pendirian, seluruh sync batal.
  5. B6 — Tinjauan bisa dibuka lewat address bar saat ekstraksi belum selesai; membukanya justru menstempel Ekstraksi sebagai selesai.

(B3 diturunkan ke M14 — tampilan checklist lengkap diputuskan tidak diperlukan.)

Satu cacat berat di luar checklist, terbukti lewat eksperimen langsung — M6. Seluruh endpoint /api/regions/* di backend yang sedang berjalan tidak merespons sama sekali. Pool MySQL SABH dibuat tanpa enableKeepAlive/idleTimeout, jadi setelah koneksi menganggur lewat wait_timeout (8 jam) permintaan menggantung selamanya alih-alih gagal. Dibuktikan: memicu reload modul mengubahnya dari tanpa respons jadi 200 dalam 0,09 dtk. Akan kambuh setiap 8 jam idle. M15: sabh-db.ts punya cacat identik, dan itulah pool yang dipakai rule PP29 & KBLI.

Sub-bagian Cross-validation sudah selesai — SABH terbukti hidup dari environment ini, jadi seluruh itemnya bisa ditutup tanpa menunggu tes UI.


Daftar perbaikan berprioritas

🔴 Blocker

B1 · Tidak ada UI override validasi

Finalize menolak setiap FAIL yang belum di-override — review-data.ts:104-113:

where: { submissionId: id, status: "FAIL", overridden: false }
→ 400 VALIDATION_FAILED "Ada validasi yang masih GAGAL dan belum di-override"

Tapi tidak ada cara meng-override di flow Pendirian PT. Backend-nya lengkap (submissions.ts:1948-2006 — alasan wajib, ada audit trail), hook frontend-nya juga ada (use-submission.ts:311 useOverrideValidation), tapi hook itu nol pemanggil. Panel validasi Step 3 sepenuhnya read-only — validation-results.tsx, 117 baris, nol kata "override". Yang memanggil endpoint itu hanya flow lain: BerakhirnyaReviewPage.tsx:183, LaporanRupsReviewPage.tsx:93, PembubaranReviewPage.tsx:65, PeleburanReviewPage.tsx:80.

Komentar kodenya sendiri mengakui masalah ini dan menyodorkan debug bypass sebagai jalan keluar — review-data.ts:90-97. Kasus paling mungkin: AKTA_DATE_WINDOW FAIL untuk akta di luar jangka 60 hari.

Perbaikan: sambungkan useOverrideValidation ke panel validasi, atau turunkan rule yang bisa FAIL secara sah menjadi WARNING.

B2 · Step 3 tidak punya jalur error

PendirianExtractionPage.tsx:37 bahkan tidak mengambil isError dari hook-nya. Satu-satunya guard adalah :158-164if (isLoading || !submission) yang merender spinner.

Jalurnya: backend 404 (submissions.ts:469-471) → apiFetch melempar ApiError (api-client.ts:118-125) → retry 1× lalu menyerah (main.tsx:12) → data tetap undefinedspinner abadi. Berlaku untuk ID salah, backend mati, dan koneksi putus sebelum data pertama masuk.

Rute Pendirian juga tidak dibungkus ErrorBoundaryroutes.tsx:114 dan :117 telanjang, sementara enam rute verifikator dibungkus (:255, :259, :263, :267, :271, :294). Tidak ada errorElement di level router.

Perbaikan: tiru pola yang sudah benar di Step 4 — PtPendirianReviewPageV2.tsx:146-154. Perbaikan terkecil, dampak terbesar, dan berdiri sendiri.

~~B3~~ · Diturunkan ke Major (M14)

Keputusan: tampilan checklist lengkap tidak diperlukan — dokumennya tetap wajib dan tetap harus diverifikasi, tapi tidak perlu dirender sebagai daftar di Step 3.

Yang tersisa dari temuan ini pindah ke M14: saat tombol "Lanjut" mati, pesannya tidak menyebut dokumen mana yang kurang. Sisanya (panel hanya merender KTP/Paspor/NPWP) sekarang perilaku yang dikehendaki, bukan cacat.

Konsekuensi untuk B4: perbaikannya tidak lagi berarti "tampilkan lima baris checklist" — cukup gerbangnya yang menangkap, plus pesan yang menyebut apa yang kurang.

B6 · Akses langsung ke Tinjauan saat ekstraksi belum selesai

Terkonfirmasi lewat tes manual: mengganti address bar ke /pendirian/:id/review membuka halaman Tinjauan walaupun ekstraksi belum selesai dan validasi masih GAGAL. (Dari Step 2 tidak bisa karena id belum ada — jadi celahnya khusus setelah submission terbentuk.)

Tidak ada penjaga di lapisan mana pun:

Akibatnya notaris bisa meninjau dan mengonfirmasi field dari dokumen yang masih dalam proses ekstraksi — nilai yang dilihatnya bisa berubah di belakang layar setelah dikonfirmasi. Dan stempel extraction-ack membuat dashboard menganggap tahap itu selesai.

Komentar kodenya menyebut stempel itu disengaja ("routing refinement 2026-07-20") untuk mencegah deep-link terlempar balik ke Step 2 selamanya. Niatnya sah, tapi efek sampingnya jadi celah.

Perbaikan: gerbangi berdasarkan status — kalau submission belum READY/COMPLETED, alihkan ke halaman ekstraksi. extraction-ack tetap dikirim hanya ketika ekstraksi memang sudah selesai.

B4 · Kewajiban lima Bukti Setor hanya ditegakkan di Step 2

Kelima sub-tipe wajib — klasifikasi-doc-types.ts:9-13: Slip Bank, Rekening Koran, Neraca, Surat Keterangan Bank, Surat Pernyataan Setor.

Gerbang Menegakkan kelimanya? Bukti
Step 2 klasifikasi ✅ ya Tiap label wajib yang kosong → severity: "error" (classification-validation.tsx:53-59) yang memblokir canProceed (:108)
Step 3 ❌ tidak Hanya satu baris generik "Bukti Setor Modal" (submission-checklist.ts:116-122), terpenuhi oleh sub-tipe apa pun (pendirian-pt.ts:66-67classifiedType.startsWith("BUKTI_TRANSFER"))
Step 4 finalize ❌ tidak Gerbangnya hanya field belum terkonfirmasi + FAIL belum di-override (review-data.ts:99-113)

Rule yang seharusnya menjaga ini tidak pernah jalan di Pendirian. REQUIRED_SUPPORTING_DOCS terdaftar di rule set (pt-akta/index.ts:34), tapi baris pertamanya langsung keluar — cross-validator-required-docs.ts:9-16:

if (!input.isPerubahan || !scope) {
  return { ruleCode: "REQUIRED_SUPPORTING_DOCS", , status: "SKIPPED", message: "Bukan perubahan" };
}

Pendirian bukan Perubahan → selalu SKIPPED. Seandainya pun jalan, isinya hanya memeriksa AKTA_PEMINDAHAN_HAK, PENETAPAN_GANTI_NAMA, BERITA_ACARA_RUPS (:27-36) — nol pemeriksaan sub-tipe Bukti Setor. Karena SKIPPED disaring dari tampilan (ValidationResultsList.tsx:47), user tidak punya petunjuk bahwa pemeriksaan itu absen.

Skenario yang lolos: unggah kelimanya di Step 2 → lolos → di Step 3 hapus empat di antaranya → rematch me-reset baris BUKTI_SETOR lalu memenuhinya kembali karena masih ada satu tersisa → checklist hijau, tidak ada FAIL, tombol aktif → finalize lolos dengan empat dokumen wajib hilang.

Perbaikan (dua tempat, sebaiknya keduanya):
1. Pecah satu baris BUKTI_SETOR jadi lima baris per sub-tipe di submission-checklist.ts:116-122, dan ubah fulfillChecklist di pendirian-pt.ts:66-67 agar memenuhi baris spesifik. Ini otomatis menutup Step 3.
2. Tambahkan rule kelengkapan khusus Pendirian yang membaca PENDIRIAN_DOC_TYPES sebagai sumber kebenaran — menutup Step 4 sekaligus, dan tidak tertinggal saat daftar dokumen wajib berubah.

⚠️ Kerjakan bersama B3. Begitu kelima baris muncul, semuanya akan tidak terlihat di Step 3 — user akan terkunci oleh lima syarat sekaligus yang tak satu pun tampil di layar.

B5 · "Cannot remove Akta Pendirian" — Akta tidak bisa diganti

Ditemukan lewat tes manual; akar masalah terkonfirmasi di kode.

Repro: Step 2 → back ke Step 1 → hapus Akta Pendirian (berhasil) → tambah ulang Akta → "Lanjut" → gagal, 400 Cannot remove Akta Pendirian.

Rantainya:

  1. Menghapus baris Akta di board hanya mengubah atom — tidak ada panggilan server, jadi terasa berhasil
  2. Akta baru masuk sebagai baris tanpa documentId (file staging segar)
  3. "Lanjut" memanggil boardDocumentDiffstore/klasifikasi.ts:178-189. Akta lama masuk remove (ID terlampir yang tidak lagi di board), Akta baru masuk add
  4. POST /:id/sync-documents memproses remove lebih dulu — submissions-klasifikasi-board.ts:178-183
  5. detachSubmissionDocument menolak tanpa syarat — submission-document-detach.ts:49-51, dengan UNREMOVABLE_TYPE = "AKTA_PENDIRIAN" (:21)
  6. Handler langsung return pada penolakan pertama → seluruh sync batal, termasuk Akta baru yang seharusnya menggantikan

Akar masalahnya konseptual: board mengekspresikan "ganti Akta" sebagai remove + add, tapi penjaga hanya melihat remove-nya. Backend tidak punya konsep "replace" — hanya attach dan detach.

Cacat turunan — detach parsial. Loop di :178-183 tidak transaksional. Kalau remove berisi dokumen lain sebelum Akta, dokumen-dokumen itu sudah terlanjur ter-detach saat Akta memicu 400. Submission tertinggal setengah-jadi, dan board di layar tidak mencerminkannya.

Perbaikan:
- Kenali replacement di sync-documents: kalau remove memuat Akta dan add memuat baris berklasifikasi AKTA_PENDIRIAN, izinkan detach-nya (atau proses add dulu baru remove)
- Jadikan loop remove transaksional agar detach parsial tidak mungkin terjadi
- Selaraskan UI: kalau Akta memang tidak boleh dilepas, jangan biarkan barisnya dihapus di Step 1 tanpa peringatan — sediakan "Ganti Akta" sebagai satu aksi

🟠 Major

# Temuan Bukti
M12 Koreksi manual NPWP ditimpa classifier saat ekstraksi. Jalur NPWP selalu meng-klasifikasi ulang file dan menulis balik documentTypedocument-processor.ts:2104-2117. Berbeda dari jalur Akta (:463-466) dan Bukti Setor (:1395) yang menghormati classificationOverride, jalur NPWP mengabaikannya Koreksi manual di Step 2 berbalik sendiri setelah ekstraksi — override yang diisi user diam-diam dibuang. Berlaku untuk setiap NPWP, terlepas dari kualitas classifier-nya
M13 Format tanggal tidak seragam — tanggal akta YYYY-MM-DD, tanggal identitas DD-MM-YYYY. Akarnya dua: (1) tanggal_akta tidak terdaftar sebagai date field — format-doc-date.ts:71 DATE_FIELD_KEYS hanya memuat doc_date, tanggal_lahir, tanggal_terbit, berlaku_hingga; (2) formatDateDisplay (:37) hanya dipakai field-correction.tsx dan DocumentSetSection.tsxbukan oleh FieldRow/FieldListSection yang dipakai review PT Pendirian Formatter seragam "D MMMM YYYY" sudah ada dan benar, tapi permukaan review PT tidak memanggilnya. Nilai mentah tampil apa adanya: tanggal identitas tersimpan DD-MM-YYYY (:64-69), tanggal akta dalam bentuk apa pun keluaran OCR/LLM
M15 sabh-db.ts punya cacat pool yang sama dengan regions.ts — dan pool itulah yang dipakai rule PP29_MODAL dan KBLI_VALID. Saat probe (proses baru) semuanya sehat, tapi backend yang hidup lama rentan mode kegagalan yang sama seperti /api/regions/*: cross-validation menggantung, bukan gagal-cepat Perbaikannya satu tempat: tambahkan enableKeepAlive: true + idleTimeout ke tiga pool SABH, tiru pola yang sudah dipakai empat pool lain di repo yang sama
M14 Pesan blokir tidak menyebut dokumen yang kurang. Saat "Lanjut ke Tinjauan" mati karena checklist wajib belum lengkap, pesannya hanya "Dokumen belum lengkap"PendirianExtractionPage.tsx:203-204. Karena panel checklist memang tidak menampilkan dokumen non-identitas (disengaja), tidak ada permukaan lain yang bisa memberi tahu Cukup sebutkan nama dokumennya di pesan/tooltip — tidak perlu merender daftar checklist
M1 KTP yatim tidak pernah terlihat. KTP tanpa pasangan mendapat matchedPersonName: "" (identity-matcher.ts:118-127), sementara IdentityCards membangun kartu dari checklist orang di Akta (identity-cards.tsx:43-51) lalu mencari match by nama (:55). Nama kosong tidak cocok dengan kartu mana pun → tidak dirender di permukaan mana pun risiko berkas salah orang lolos tanpa disadari
M2 Tombol "Lanjut" tidak memblokir file tanpa jenis. File tak dikenali & belum dikonfirmasi dicatat severity: "warning" (classification-validation.tsx:73-77, :84-89), tapi gate hanya melihat "error" (:92, :108) perbaikannya satu kata
M3 Refresh di Step 1/2 menghapus board. Semua atom atom() polos tanpa persistensi (store/klasifikasi.ts:127, :191-194); rebuild dari server early-return saat lastSubmission kosong (KlasifikasiPage.tsx:979) atomWithStorage = perubahan kecil
M4 Kegagalan SABH menyamar sebagai "KBLI tidak ditemukan". catch memetakan semua kode ke null (sabh-db.ts:102-107), lalu diklasifikasikan invalid (cross-validator.ts:621-622). Terbukti saat runtime: dengan host SABH diarahkan ke IP blackhole, validateKbliCodes(["62019"]) mengembalikan null → dilaporkan "Tidak ditemukan", padahal errornya connect ETIMEDOUT. Bandingkan getBakumSetting() yang benar-benar mengembalikan null dan memicu SKIPPED validatePp29Modal sudah benar (:568-575) — tiru polanya
M5 Polling tanpa batas pada status non-terminal. Berhenti hanya pada READY/COMPLETED/ERROR (use-submission.ts:45, :80); fallback return 2500 (:81) membuat submission tersangkut di EXTRACTING dipoll selamanya tanpa indikator
M6 Pool SABH tanpa keepalive dan tanpa batas antre — terbukti menggantung di backend yang sedang berjalan. Endpoint /api/regions/* tidak merespons sama sekali (dites 20 dtk, tiga endpoint, semuanya 000), padahal backend yang sama menjawab /api/health dan /api/submissions dalam ~0 ms. Query SQL yang persis sama lewat proses baru selesai dalam 168 ms / 40 baris — jadi SABH-nya sehat, yang rusak pool-nya Tiga pool SABH tidak punya enableKeepAlive/idleTimeoutregions.ts:16-24, sabh-db.ts:14-22, sabh-region-lookup.ts:21-29 — sementara empat pool lain di repo punya: apostille-spesimen-db.ts:33-35, pp-registry-db.ts:30, auth-db.ts:35, pp-wilayah-resolve.ts:28. Dengan connectionLimit: 2 dan mysql2 yang mengantre akuisisi tanpa batas, koneksi basi = permintaan menggantung selamanya, bukan gagal. Angkanya cocok: MySQL SABH wait_timeout = 28800 dtk (8 jam), sementara proses backend sudah hidup 21+ jam. DIBUKTIKAN dengan eksperimen: memicu reload modul backend (tanpa mengubah kode apa pun) langsung mengubah /api/regions/provinsi dari menggantung tanpa respons menjadi 200 dalam 0,09 dtk — ketiga endpoint regions pulih (0,09 / 0,02 / 0,03 dtk). Pool baru = sehat; pool lama = koneksi mati yang tidak pernah dideteksi. Gejalanya akan kambuh setiap kali pool menganggur lewat 8 jam
M7 Wilayah dijodohkan LIKE %nama% lintas dua DB, bukan foreign key — kelurahan dicocokkan ke kecamatan by kemiripan nama (regions.ts:97-104, sabh-region-lookup.ts:45, :61) uji dengan nama kecamatan umum ("Cikarang", "Medan Kota")
M8 Data basi antar-tab. refetchOnWindowFocus: false global (main.tsx:12) tanpa sinkronisasi antar-tab (nol BroadcastChannel/listener storage). Step 4 tidak polling sama sekali perbesar bobotnya kalau demo multi-tab
M9 Urutan review bukan by confidence. Checklist meminta confidence terendah lebih dulu; yang ada urutan statis order: 0..10 (pt-pendirian-review.ts:131-143) nol sort by confidence di seluruh review-engine

🟡 Minor

# Temuan Bukti
N1 Wording "Nilai" belum jadi "Input yang benar". Satu baris, tapi menyentuh 7 halaman review + 2 test yang mengunci label lama FieldEditDialog.tsx:60
N2 Wording "maks 10MB" padahal batasnya 100 MB; halaman /process masih dirutekan aktif (routes.tsx:289) document-type-selector.tsx:27, :34, :41, :48, ProcessPage.tsx:164
N3 Tiga connection pool MySQL terpisah ke server SABH yang sama (2+2+3 = 7 koneksi per instance) — dan ketiganya persis pool yang kekurangan keepalive di M6 sabh-db.ts:14, regions.ts:16, sabh-region-lookup.ts:21
N4 isSabhAvailable() mengukur konfigurasi, bukan konektivitas. Terbukti: dengan host diarahkan ke IP blackhole, fungsi ini tetap mengembalikan true — jadi rule tidak pernah memilih jalur SKIPPED "SABH database tidak tersedia" dan jatuh ke jalur error masing-masing (lihat M4) sabh-db.ts:7-9
N5 Confidence badge tone tinggi mungkin terlalu lembut sehingga terkesan "hanya muncul untuk yang gagal" classification-board.tsx:77
N6 Kode mati alamat manual. PtAddressForm, PATCH /:id/address (submissions.ts:1617-1660), dan useUpdateAddress (use-submission.ts:368-386) semuanya nol pemanggil. Dibuat sebagai pengganti Surat Domisili; setelah revert, alamat datang dari dokumen (pendirian-pt.ts:62-64 copyDomisiliAddressToSubmission) putuskan: hapus, atau sambungkan sebagai pelengkap
N8 Ambang PP29 untuk PT tertutup = Rp 1. Terbaca dari SABH: min_modal_dasar_tertutup_pp29 = 1 (PT terbuka = Rp 3.000.000.000, sesuai regulasi). Artinya PP29_MODAL praktis tidak pernah gagal untuk PT tertutup — modal dasar berapa pun ≥ 1 lolos Kemungkinan besar disengaja (PP 8/2021 menghapus minimum modal dasar PT, diserahkan ke kesepakatan pendiri). Tapi labelnya "Minimum Modal Dasar (PP29)" jadi menjanjikan pemeriksaan yang efektifnya tidak ada — perlu dikonfirmasi ke tim hukum
N9 Kode KBLI non-5-digit dilaporkan "Tidak ditemukan", bukan "format salah". Filter LENGTH(kode) = 5 di query (sabh-db.ts:91) membuat 620 dan 0000000 ikut jatuh ke keranjang yang sama dengan kode yang memang tidak terdaftar Terbukti saat probe. Notaris tidak bisa membedakan "kodenya salah ketik" dari "kodenya belum terdaftar di SABH"
N10 Tabel wilayah memuat 40 provinsi, sementara Indonesia saat ini punya 38 — kemungkinan ada entri lama/duplikat di AHU_NOTARIAT.WILAYAH Perlu dicek sebelum dropdown provinsi dipakai serius; berhubungan dengan M7 (penjodohan LIKE %nama%)
N7 Dua token untuk konsep sama: SURAT_PERNYATAAN_DOMISILI (PT) vs SURAT_DOMISILI (PP, dipetakan ke enum DB DOMISILI) pp-perubahan.ts:53-58, pp-perbaikan-pp.ts:64

⚪ Perlu keputusan

Tidak ada lagi. Keempat pertanyaan terbuka sudah dijawab:

Pertanyaan Jawaban
Apakah task 86ex78yf7 (hapus Domisili) masih berlaku? Di-revert — Domisili & Surat Pernyataan Setor tetap wajib
Bukti Setor: lima sub-tipe wajib semua, atau satu cukup? Kelimanya wajibB4
Deep-link ke Tinjauan sengaja diizinkan? Tidak — terkonfirmasi sebagai celah → B6
Section approval — apa yang sebenarnya terlihat? Ditutup sebagai PASS; beda penamaan, bukan beda fungsi

Satu yang tersisa sebagai pilihan implementasi, bukan pertanyaan terbuka: N6 — kode mati alamat manual (PtAddressForm, PATCH /:id/address, useUpdateAddress, semuanya nol pemanggil). Hapus, atau sambungkan sebagai pelengkap Domisili.


Checklist lengkap — status akhir

File checklist-debug-pendirian-pt.md sengaja tidak diubah — itu catatan tes manual Anda.

Bagian 1 — Upload (8 PASS · 1 terjawab)

Item Status Sumber Catatan
Drag & drop banyak file PASS M
File muncul dengan nama benar PASS M
Hapus file dari list PASS M
Upload PDF diterima/ditolak jelas PASS M Diterima tanpa pesan konfirmasi
Batas ukuran file terjawab: 100 MB K config.ts:165 MAX_FILE_SIZE_MB \|\| "100", diterapkan di klasifikasi.ts:319. Wording UI salah → N2
Upload file rusak / .txt PASS M
Nama file karakter aneh PASS M
Refresh di tengah upload PASS M "hilang bersih" — akar masalahnya M3
Nol file → tombol nonaktif PASS M

Bagian 2 — Klasifikasi (17 PASS · 2 FAIL · 1 MANUAL · 2 N/A)

Item Status Sumber Catatan
Tombol "Cek dan Kelompokkan" memicu classifier PASS M
Indikator loading PASS M
Thumbnail per card PASS M
Label jenis dokumen PASS M
Confidence badge per card PASS K Dirender untuk setiap card: classification-board.tsx:316 → badge :326-330. Tone berubah di ambang 0.6 (:77) → N5
Confidence rendah ter-highlight PASS M
Dropdown jenis untuk confidence rendah PASS M
Re-assign jenis manual tersimpan PASS M
~~Dropdown tidak menampilkan SURAT_DOMISILI/SURAT_PERNYATAAN~~ N/A Revert — justru harus tampil
Warning kalau Akta belum ada PASS M
"Lanjut" nonaktif kalau ada file tanpa jenis FAIL M+K M2
"Lanjut" nonaktif kalau Akta belum ada PASS M+K classification-validation.tsx:53-59severity: "error", memang memblokir
"Lanjut" aktif kalau kedua syarat terpenuhi PASS K :108
Akta / KTP / NPWP dikenali benar PASS M
NPWP_LAMA terpisah dari NPWP baru MANUAL Tidak ada dataset uji. Pemisahannya sendiri ada di kode: isNpwpLama ditandai saat klasifikasi (klasifikasi.ts:557), ekstraksi memakai dua model Azure terpisah (document-processor.ts:2126). Lihat M12
Sub-klasifikasi SURAT_PERNYATAAN_SETOR terpisah PASS M
4 sub-tipe Bukti Setor lain classify-only FAIL K Premis item ini keliru — mereka menjalankan SETORAN-FACTS pass (PaddleOCR + LLM 3B) yang mengisi "Daftar Setoran": document-processor.ts:1398-1408. Hanya sub-tipe tak dikenal yang classify-only. Bukan cacat — tapi premis checklist perlu dikoreksi
Regresi kelas lama setelah retrain PASS M
NPWP tidak tertukar dengan KTP PASS M
~~Upload Surat Domisili asli~~ N/A Revert — kelas & labelnya memang ada
Dokumen di luar semua kelas → fallback PASS M
Threshold confidence masuk akal PASS M

Bagian 3 — Orkestrasi (7 PASS)

Item Status Sumber Catatan
Akta diproses lebih dulu PASS M
Polling tiap 2,5 detik PASS K use-submission.ts:73
Polling berhenti setelah selesai PASS K :45 + :80. Celah → M5
Progress bar mencerminkan kondisi sebenarnya PASS M
Waktu proses wajar (benchmark) PASS M Catatan: kunci cache berbasis isi file (queue.py:105), jadi angka ini hanya jujur untuk berkas yang belum pernah diproses
Satu dokumen gagal, lain tetap lanjut PASS M
Pesan error jelas kalau ekstraksi gagal PASS M Berlaku untuk error per-dokumen; error memuat halaman → B2

Bagian 3 — Kolom kiri (6 PASS)

Seluruh enam item PASS dari tes manual: collapse/expand, thumbnail, progress per file, drop zone, hapus file di tengah step, tambah file setelah ekstraksi.

Bagian 3 — Kolom kanan (5 PASS · 1 FAIL · 2 N/A)

Item Status Sumber Catatan
Document checklist tampil sesuai dokumen yang ada N/A Keputusan: tampilan checklist tidak diperlukan. Dokumennya tetap wajib & tetap diverifikasi — penegakannya di B4, pesan blokirnya di M14
~~Checklist tidak menampilkan Domisili & Surat Pernyataan~~ N/A Sama seperti di atas
Identity cards tampil PASS M
KTP auto-matching by name PASS M
KTP auto-matching: nama bergelar PASS M
KTP auto-matching: typo kecil PASS M Ambang skor 0.5 — identity-matcher.ts:118
KTP auto-matching: dua nama mirip tidak tertukar PASS M
KTP yang tidak cocok dengan siapa pun ditandai jelas FAIL M+K M1

Bagian 3 — Cross-validation (15 PASS · 2 FAIL · 0 MANUAL · 1 N/A)

Selesai. Semula diperkirakan butuh tes manual, ternyata bisa ditutup seluruhnya: SABH hidup dan terjangkau dari environment ini, sehingga jalur rule-nya bisa diuji langsung; logika rule-nya sendiri sudah dijaga 174 unit test yang semuanya lolos.

Cara verifikasinya (semuanya read-only):
1. Memanggil helper milik proyek sendiri — getBakumSetting() dan validateKbliCodes() di sabh-db.ts — yaitu jalur yang persis dipakai rule PP29_MODAL dan KBLI_VALID
2. Menjalankan bun test src/services/__tests__/cross-validator*.test.ts174 pass / 0 fail
3. Fault injection: mengarahkan SABH_DB_HOST ke IP blackhole untuk mengukur perilaku saat SABH mati

Koneksi nyata: AHU_BADAN_HUKUM @ 192.16x.x.x:3306, user bht-read — balas dalam 754 ms.

⚠️ Catatan penting: probe ini berjalan di proses baru, jadi pool-nya segar. Di backend yang hidup lama, pool sabh-db.ts rentan cacat yang sama seperti /api/regions/* — lihat M6 dan M15. Artinya rule PP29 & KBLI bisa menggantung, bukan gagal-cepat, walau logikanya sendiri sudah terbukti benar.

Beberapa item checklist menggabungkan dua klaim ("rule X berjalan dan hasilnya benar"). Baris seperti itu dipecah dua.

Item Status Sumber Catatan
Inventarisasi 8 rule FAIL K Bukan 8 tapi 32pt-akta/index.ts:15-48, dipakai utuh tanpa filter oleh pendirian-pt.ts:94. Terlihat 8 karena UI membuang SKIPPED dan menyembunyikan PASS (ValidationResultsList.tsx:45-49). Inventaris lengkap di bawah
Koneksi readonly SABH hidup dari environment PASS K Terbukti: koneksi berhasil, balas 754 ms. DB AHU_BADAN_HUKUM, user bht-read. Data referensi terbaca — 40 provinsi, 188.605 baris kelurahan, tabel m_kbli dan tbl_bakum_setting semuanya terjangkau
— readonly-nya benar (tidak menulis) PASS K Nol statement tulis di sabh-db.ts, regions.ts, sabh-region-lookup.tsdan kredensialnya memang user baca: SABH_DB_USER=bht-read. Jadi ditegakkan di dua lapis, bukan sekadar konvensi kode
Rule PP29 berjalan PASS K pt-akta/index.ts:45; impl cross-validator.ts:542-596
Rule PP29 hasilnya benar PASS K Ambang terbaca dari SABH: min_modal_dasar_tertutup_pp29 = **1**, min_modal_dasar_terbuka = **3.000.000.000**. Aritmetikanya benar dan gate jenis-nya ada unit test-nya. Tapi lihat N8 — ambang 1 rupiah membuat rule ini praktis tidak pernah gagal untuk PT tertutup
Rule KBLI berjalan PASS K pt-akta/index.ts:46; impl cross-validator.ts:598-647
Rule KBLI hasilnya benar PASS K Diuji ke data nyata: 62019 → "Aktivitas Pemrograman Komputer Lainnya" (status 1, 2020), 63111 → "Aktivitas Pengolahan Data" (status 1, 2020), 47111 → status 1, 2025. Kode palsu 99999 → null. Terkonfirmasi juga filter LENGTH(kode)=5 (sabh-db.ts:91): 620 dan 0000000 ikut dilaporkan "Tidak ditemukan", bukan "format salah" → N9
KBLI tidak ditemukan → jelas, tidak crash PASS K WARNING "Tidak ditemukan: <kode>" (cross-validator.ts:636-646); WARNING tidak memblokir (fail-gate.ts:5-9). Tapi → M4
SABH mati → flow tetap jalan dengan warning PASS K Tiga lapis: isSabhAvailable() false → SKIPPED; query gagal → catch + log.warn; rule melempar → Promise.allSettled (validation-runner.ts:59-67)
SABH mati → tidak hang PASS K Fault injection ke IP blackhole: getBakumSetting() gagal dalam 5.120 ms, validateKbliCodes() dalam 5.010 ms — keduanya sesuai connectTimeout: 5000, tidak menggantung. ⚠️ Yang diuji baru host tak terjangkau; query yang menggantung setelah koneksi terbentuk masih tanpa batas → M6
~~Rule ALAMAT_DOMISILI tidak ikut dijalankan~~ N/A Revert — DOMISILI_NAMA memang harus jalan (pt-akta/index.ts:25)
Nominal modal Akta vs Bukti Setor dibandingkan PASS K MODAL_BUKTI_SETOR pt-akta/index.ts:18; impl cross-validator.ts:271-320
Nominal modal hasilnya benar PASS K Dijaga unit test: validateModalBuktiSetor ("passes when total matches modal disetor", "skips when no bukti setor amounts"), plus MODAL_HIERARCHY (FAIL saat dasar < disetup / ditempatkan < disetor) dan NOMINAL_RECONCILE (ditempatkan = saham × nominal). Semua lolos
Nama perseroan konsisten antar dokumen PASS K NAMA_PT_CONSISTENCY pt-akta/index.ts:26, sumbernya termasuk dokumen Domisili (cross-validator.ts:724). Ada unit test khusus: "DOMISILI_NAMA + NAMA_PT_CONSISTENCY share one company-name scorer" — jadi keduanya dijamin konsisten satu sama lain
Validasi NIK berfungsi PASS K NIK_KTP_AKTA pt-akta/index.ts:16. Unit test menutup: "equal NIKs match", "fails when a NIK does not match", "passes when all NIKs match", WNI-scoping, dan niksAreEquivalent (normalisasi ketat). ⚠️ Cakupannya tetap sempit: hanya membandingkan NIK KTP vs Akta untuk KTP WNI yang sudah ter-match; tidak ada validasi format/checksum NIK 16 digit
Warning validasi bisa di-override user FAIL K B1
Override tersimpan (bertahan saat re-validasi) PASS K validation-runner.ts:71-110 — cabang update sengaja tidak menyentuh overridden. Pendirian memanggil tanpa blanketDelete (processor.ts:264, rematch.ts:94)
Override terbawa ke Step 4 PASS K Diproyeksikan pt-pendirian-review.ts:783, ditampilkan ValidationBanner.tsx:69. Moot dalam praktik selama B1 belum beres

Inventaris rule set Pendirian PT (32 rule)

Sumber tunggal: pt-akta/index.ts:15-48.

NIK_KTP_AKTA · NAMA_NPWP_KTP · MODAL_BUKTI_SETOR · PEMEGANG_SAHAM_BUKTI_SETOR · SHARES_SUM_CONSISTENCY · MODAL_HIERARCHY · NOMINAL_RECONCILE · KTP_COMPLETENESS · NPWP_COMPLETENESS · DOMISILI_NAMA · NAMA_PT_CONSISTENCY · PS_TOTAL_100 · CONTACT_INFO_COMPLETE · OLD_DATA_CONSISTENCY · PERUBAHAN_MODAL · DATA_ACTUALLY_CHANGED · SHARES_TRANSFER_BALANCE · SHARES_TOTAL_LEMBAR · REQUIRED_SUPPORTING_DOCS · AKTA_DATE_WINDOW · PERSEROAN_STATE · NPWP_PERSEROAN_FUZZY · NOTARIS_TERAKHIR · JENIS_SELECTION_NONEMPTY · JENIS_DESELECT_REASONS_COMPLETE · UPLOADED_DOC_UNUSED · CONTRADICTORY_JENIS · PEMEGANG_SAHAM_CONTACT · PASSPORT_AKTA · PP29_MODAL✱ · KBLI_VALID✱ · PERUBAHAN_KBLI

✱ = async, butuh SABH. Rule #14–17, #21–27, #32 secara semantik milik flow Perubahan — di Pendirian diperkirakan selalu SKIPPED sehingga tersaring dari UI, tapi ini belum dibuktikan. REQUIRED_SUPPORTING_DOCS sudah dipastikan selalu SKIPPEDB4.

Bagian 3 — Alamat PT manual (1 PASS · 9 N/A)

Sub-bagian ini dibuat sebagai pengganti Surat Domisili. Setelah revert, alamat memang datang dari dokumen Domisili (pendirian-pt.ts:62-64), jadi seluruh item "field X tersedia di Step 3" menjadi N/A.

Yang tetap berlaku:

Item Status Catatan
Dropdown region readonly terhadap DB PASS Seluruh handler regions.ts SELECT-only, semuanya regions.get(...), nol POST/PATCH. Idem sabh-region-lookup.ts
Sisa 9 item (field alamat/rt/rw/kode_pos, 4 dropdown, cascading, tersimpan & muncul di Step 4) N/A Komponennya ada dan lengkap tapi tanpa pemanggilN6

Bagian 4 — Review & Submit (12 PASS · 2 FAIL · 1 N/A)

Item Status Sumber Catatan
Smart Review menampilkan field satu per satu N/A — desain berbeda K Tidak ada mode satu-per-satu: FieldListSection me-map seluruh section.fieldRefs sekaligus (FieldListSection.tsx:49-53), dan nol jejak wizard/stepper di seluruh review-engine (grep currentFieldIndex/nextField/stepIndex → 0 hasil). Yang ada: dikelompokkan per seksi, semua field terlihat, konfirmasi per field. Kalau review terpandu satu-per-satu memang diinginkan, itu permintaan fitur, bukan bug — dan berpasangan dengan M9 (urutan by confidence)
Urutan: confidence terendah lebih dulu FAIL K M9
Bounding box menyorot lokasi field PASS (sebagian) M "masih ada yg error"
Edit nilai field tersimpan PASS M
Field locking UX konsisten dengan Akta PASS K Keduanya memakai semantik yang sama persis — status === "LOCKED": panel lama extraction-fields-panel.tsx:42, review baru FieldRow.tsx:43. Konsekuensinya juga sama: field terkunci tidak bisa diedit (:48) dan tidak menuntut konfirmasi (:47). Beda afordans: panel lama menampilkan penghitung eksplisit "N/M terkunci" (:61), review baru hanya memberi opasitas lebih redup (:70) — mekanismenya konsisten, penandanya kurang tegas
Sidebar checklist sinkron PASS M
Sidebar identity cards sinkron PASS M
Sidebar validation warnings sinkron PASS M Read-only — tanpa kontrol override → B1
Approve per section berfungsi PASS M Catatan kode: SectionApprovalBar tidak dirender halaman PT Pendirian — hanya PtPerubahanReviewPageV2.tsx:220, PtAkuisisiReviewPageV2.tsx:233, PtPerbaikanReviewPageV2.tsx:201. Di Pendirian yang berlaku konfirmasi per-field
Submit nonaktif kalau section belum di-approve PASS M Gate sebenarnya: semua field terkonfirmasi + nol FAIL belum di-override (review-data.ts:99-113)
Submit berhasil, data tersimpan PASS M
Cek data karakter-per-karakter (0/O, 1/l, 5/S) PASS M Ada salah baca OCR, tapi seluruhnya ter-highlight sebelum validasi dan bisa diedit. Nilai hasil edit ikut tersimpan dan tampil setelah submit — jalur koreksinya utuh dari ujung ke ujung
Tidak ada field tertukar posisinya PASS M
Format tanggal & angka konsisten FAIL M+K Tanggal akta YYYY-MM-DD, tanggal identitas DD-MM-YYYYM13. Angka: helper formatRupiahDisplay / formatThousandsDisplay ada di projector-utils
Submit dua kali tidak membuat dua submission PASS K Dua lapis: tombol dikunci saat pending (ReviewProgressHeader.tsx:228), dan server update status → COMPLETED, bukan create (review-data.ts:115) — panggilan kedua idempoten

Bagian 5 — Lintas-flow & ketahanan (12 PASS · 5 FAIL · 3 MANUAL)

Jotai memang dipakai (package.json:22), tapi hanya untuk board Step 1/2. Step 3 & 4 sepenuhnya server-driven lewat TanStack Query, nol atom.

Item Status Sumber Catatan
Tombol back Step 3 → Step 2 benar PASS M+K PendirianExtractionPage.tsx:234?resume= (flow-nav.ts:99-101)
— state Jotai utuh setelah back PASS M Mekanismenya bukan "state bertahan" tapi "board di-restage dari server" (KlasifikasiPage.tsx:184-256) — hasilnya sama saja bagi user
Tombol back Step 4 → Step 3 benar PASS M+K PtPendirianReviewPageV2.tsx:310. Saat COMPLETED, backTo jadi null — tombol back hilang
— state utuh setelah back PASS M+K Step 3 tidak menyimpan state klien yang bisa hilang
Refresh di Step 3 PASS K use-submission.ts:64-70
Refresh di Step 4 PASS K PtPendirianReviewPageV2.tsx:111review-data.ts:24-40
Refresh di Step 1/2 FAIL M+K M3. Catatan Anda: "reload step 1 ilang, harusnya engga"
Buka submission sama di dua tab PASS M Kode tetap mencatat risikonya → M8: nol sinkronisasi antar-tab dan refetchOnWindowFocus: false. Di Step 3 polling 2,5 dtk menutupinya; Step 4 tidak polling sama sekali, jadi uji ulang khusus di Tinjauan
ID tidak valid → error handling jelas FAIL K B2. Anda menandai PASS — lihat K2 di bawah
— pembanding: Step 4 dengan ID tidak valid PASS K PtPendirianReviewPageV2.tsx:146-154
Akses langsung Step 4 tanpa lewat Step 3 FAIL M+K Terkonfirmasi: Tinjauan terbuka walau ekstraksi belum selesai & validasi masih GAGAL → B6
Putus koneksi di tengah polling → recovery MANUAL Polling terus jalan tiap tik; tapi tanpa cabang isError, UI menampilkan data lama tanpa indikator apa pun
Halaman Pendirian tidak dibungkus ErrorBoundary FAIL K Bagian dari B2
Negative test: scan jelek MANUAL
File belum pernah masuk cache PASS — cara memaksanya diketahui K Kunci cache = ocr:{tipe}:{model}:{sha256 isi file} (task_cache.py:59-76), Redis db 2, TTL 7 hari (OCR_CACHE_TTL). Tiga cara menjamin jalur dingin: (1) pakai berkas yang isinya belum pernah diproses — ganti nama tidak cukup; (2) set OCR_CACHE_ENABLED=false di GPU server saat demo; (3) tunggu TTL habis. Model/prompt yang berubah otomatis meng-invalidasi cache
Simulasi flow verifikator — backend PASS K bun test src/routes/__tests__/verifikator-*.test.ts28 pass / 0 fail, stabil di 3 kali jalan berturut-turut. Mencakup decide, list, get, assignment, dan unified inbox
Simulasi flow verifikator — UI MANUAL Butuh akun ber-peran verifikator (routes.tsx:243-256). Yang wajib diperhatikan: mengembalikan berkas menghapus semua override validasi (verifikator.ts:316-319) — pastikan notaris diberi tahu saat itu terjadi
"Dokumen lain bukan persyaratan" dipakai PASS K classifier-labels.ts:37; backend klasifikasi-doc-types.ts:29, :41, :50
"NPWP Pengurus Perseroan" dipakai PASS K Konsisten di lima permukaan: classifier-labels.ts:24, document-checklist.tsx:48, extraction-progress.tsx:42, AnalyticsPage.tsx:42, review-pdf-viewer.tsx:38
"Input yang benar" menggantikan "Nilai" FAIL K N1

Kesehatan test suite

Dijalankan penuh sebagai bagian audit ini.

Suite Hasil
Backend (bun test) 3031 pass · 1 skip · 2 fail — 355 file, 8.441 assertion, 113 dtk
Frontend (vitest run) 1092 pass · 0 fail — 149 file, 86 dtk

Dua kegagalan backend keduanya environmental, bukan cacat produk — dan keduanya sudah ada sebelum audit ini (angka yang sama tercatat di commit dd783691):

Test Sebab
config gateway defaults > gpuServerDirectUrl falls back to gpuServerUrl Test mengharapkan default http://192.168.83.20:8200, tapi membaca env nyata yang berisi https://x056.ahu-azure.val.id/api/gpu. Test-nya tidak mengisolasi environment
processPendirianPpViaCustomModel — BO persistence Memanggil layanan PaddleOCR sungguhan dan gagal 400 Image decode failed. Unit test yang bergantung pada servis eksternal

Keduanya layak diperbaiki sebagai kebersihan test (isolasi env + mock servis), tapi tidak memblokir apa pun.

⚠️ Jangan menjalankan dua suite backend bersamaan. Saat suite penuh berjalan, run verifikator paralel menghasilkan 24 pass / 4 fail; dijalankan sendirian, 28 pass / 0 fail tiga kali berturut-turut. Test berbagi satu database, jadi eksekusi paralel saling menimpa — kegagalan seperti itu palsu.


Cek silang — tes manual vs audit kode

Tiga item bertanda [x] di checklist Anda berbenturan dengan kode.

K1 · "Checklist tidak menampilkan Surat Domisili & Surat Pernyataan"

Yang Anda lihat benar, tapi alasannya bukan penghapusan — DocumentChecklist membuang semua docType non-identitas sebelum render (document-checklist.tsx:22-54). Baris checklistnya tetap ada di DB dengan required: true. Setelah revert, ini berbalik jadi cacatB3.

K2 · "Akses /pendirian/:id/extraction dengan ID tidak valid — error handling jelas"

Audit kode menghasilkan FAIL (B2). Jalurnya sudah dirinci di atas — tidak ada cabang isError yang bisa dieksekusi.

Perlu konfirmasi: kalau yang terlihat spinner tak berhenti, itu justru B2. Kalau Anda melihat pesan error nyata, ada jalur yang belum saya temukan dan perlu saya telusuri lagi.

K3 · "Approve per section berfungsi" — ditutup

Dicatat PASS dari tes manual. Untuk arsip: SectionApprovalBar tidak dirender halaman PT Pendirian (hanya Perubahan/Akuisisi/Perbaikan), dan gate submit-nya adalah konfirmasi per-field + nol FAIL belum di-override. Beda penamaan, bukan beda fungsi.

Konfirmasi — tes manual cocok dengan kode

Item checklist Catatan Anda Temuan audit
B5 refresh tiap halaman "reload step 1 ilang, harusnya engga" Persis M3
B1 refresh di tengah upload "hilang bersih" Sumber sama dengan M3
B3 KTP tidak cocok "tidak ditandai" Persis M1, dengan akar yang presisi
B5 akses langsung Step 4 terkonfirmasi terbuka B6 — celah nyata, bukan desain

Sisa item yang hanya bisa ditutup dengan tes manual (4)

Tinggal empat, dan semuanya butuh bahan atau mata manusia — bukan sesuatu yang bisa dibuktikan dari kode:

Item Kenapa tidak bisa ditutup dari sini
NPWP_LAMA vs NPWP baru Tidak ada dataset uji. Pemisahannya sendiri sudah terbukti ada di kode
Negative test: scan berkualitas jelek Butuh dokumen scan buruk yang nyata
Putus koneksi di tengah polling Butuh browser + gangguan jaringan sungguhan. Perilaku kodenya sudah dipetakan (polling terus jalan; tanpa cabang isError, data lama tampil tanpa indikator)
Simulasi flow verifikator — sisi UI Butuh akun ber-peran verifikator. Sisi backend-nya sudah tertutup: 28 test lolos

Yang paling disarankan didahulukan: simulasi flow verifikator di UI — satu-satunya tahap yang belum pernah disentuh, dan punya perilaku mengejutkan (mengembalikan berkas menghapus semua override validasi).


Catatan lingkungan

Audit ini dikerjakan terhadap dev_5 (dd783691). Kalau tes manual Anda dijalankan di worktree lain, sebagian hasil bisa berbeda — dev_6 misalnya tertinggal tiga commit: fix highlight preview (4b42996b), fix Surat Pernyataan Setor yang mengganjal finalize (e4e2de0d), dan fix debug toggle finalize (dd783691).

Pasangan dev_5 yang benar: frontend :47031 → backend :47015 (vite --strictPort, VITE_API_PROXY_TARGET=http://localhost:47015 — terverifikasi).