Pembagian Perbaikan — Flow Pendirian PT
Sumber temuan: hasil-audit.md · Repo: ahu-ocr-dev/dev_5 (branch dev/5, HEAD dd783691) · 2026-08-04
Dibagi untuk 2 developer. Prinsipnya bukan membelah daftar jadi dua, tapi memberi tiap orang satu area yang koheren supaya file yang disentuh minim bertabrakan dan konteksnya tidak bolak-balik.
- Dev A — Alur dokumen (Step 1 → 3). Dokumen masuk, terklasifikasi, lengkap, sampai halaman ekstraksi.
- Dev B — Review, validasi & integrasi SABH (Step 4 + data). Tinjauan, aturan validasi, dan seluruh jalur ke database SABH.
Kode temuan (B blocker, M major, N minor) mengikuti penomoran di hasil-audit.md — jangan diubah supaya rujukannya tetap nyambung.
Ringkasan beban
| Blocker | Major | Minor | Catatan | |
|---|---|---|---|---|
| Dev A | 3 (2 berat) | 4 | 3 | Pekerjaan berat terkonsentrasi — B4 dan B5 keduanya menyentuh backend + frontend sekaligus |
| Dev B | 2 (1 sedang) | 7 | 7 | Lebih banyak item, rata-rata lebih ringan dan lebih terpisah |
Seimbang dalam jam kerja, bukan dalam jumlah baris tabel.
Dev A — Alur dokumen (Step 1 → 3)
Wilayah file: KlasifikasiPage · classification-validation · store/klasifikasi · submissions-klasifikasi-board · submission-document-detach · submission-checklist · flows/pendirian-pt · PendirianExtractionPage · document-checklist · identity-cards · identity-matcher · use-submission
🔴 B2 · Jalur error di Step 3 — mulai dari sini
Paling ringan, paling terlihat, dan berdiri sendiri.
Halaman Step 3 tidak pernah mengambil isError dari hook-nya (PendirianExtractionPage.tsx:37). Satu-satunya penjaga adalah :158-164 yang merender spinner. Akibatnya ID tidak valid, backend mati, atau koneksi putus sebelum data pertama → spinner abadi.
Kerjakan:
- Tambahkan cabang isError meniru pola yang sudah benar di Step 4 — PtPendirianReviewPageV2.tsx:146-154 (ExtractionErrorState + tombol kembali)
- Bungkus rute Pendirian dengan ErrorBoundary — routes.tsx:114 dan :117 masih telanjang, padahal enam rute verifikator sudah dibungkus (:255–:294)
Selesai bila: membuka /pendirian/<uuid-ngawur>/extraction menampilkan pesan error yang bisa dibaca, bukan spinner.
🔴 B5 · Cannot remove Akta Pendirian — Akta tidak bisa diganti
Repro: Step 2 → back ke Step 1 → hapus Akta (berhasil) → tambah ulang Akta → "Lanjut" → 400 Cannot remove Akta Pendirian.
Akarnya konseptual: board mengekspresikan "ganti Akta" sebagai remove + add, tapi penjaganya hanya melihat remove-nya. Backend tidak punya konsep "replace".
Rantainya:
1. Hapus baris Akta di board hanya mengubah atom — tidak ada panggilan server, jadi terasa berhasil
2. boardDocumentDiff menaruh Akta lama di remove, Akta baru di add — store/klasifikasi.ts:178-189
3. sync-documents memproses remove dulu — submissions-klasifikasi-board.ts:178-183
4. detachSubmissionDocument menolak tanpa syarat — submission-document-detach.ts:49-51 (UNREMOVABLE_TYPE = "AKTA_PENDIRIAN" di :21)
5. Handler langsung return → seluruh sync batal, termasuk Akta baru
Kerjakan:
- Kenali replacement: kalau remove memuat Akta dan add memuat baris berklasifikasi AKTA_PENDIRIAN, izinkan detach-nya — atau proses add lebih dulu baru remove
- Jadikan loop remove transaksional. Cacat turunan: loop itu tidak atomik, jadi kalau ada dokumen lain di urutan sebelum Akta, dokumen itu sudah terlanjur ter-detach saat Akta memicu 400 — submission tertinggal setengah-jadi
- Selaraskan UI: kalau Akta memang tidak boleh dilepas sendirian, jangan biarkan barisnya dihapus tanpa peringatan — sediakan "Ganti Akta" sebagai satu aksi
🔴 B4 · Kewajiban 5 sub-tipe Bukti Setor hanya ditegakkan di Step 2
Kelimanya wajib — klasifikasi-doc-types.ts:9-13: Slip Bank, Rekening Koran, Neraca, Surat Keterangan Bank, Surat Pernyataan Setor.
| Gerbang | Menegakkan? |
|---|---|
| Step 2 klasifikasi | ✅ tiap label wajib yang kosong → severity: "error" yang memblokir |
| Step 3 | ❌ hanya satu baris generik "Bukti Setor Modal" (submission-checklist.ts:116-122), terpenuhi oleh sub-tipe apa pun (pendirian-pt.ts:66-67) |
| Step 4 finalize | ❌ gerbangnya hanya field belum terkonfirmasi + FAIL belum di-override |
Rule yang seharusnya menjaga ini tidak pernah jalan. REQUIRED_SUPPORTING_DOCS terdaftar di rule set Pendirian (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. Dan karena SKIPPED disaring dari tampilan, tidak ada petunjuk bahwa pemeriksaan itu absen.
Skenario yang lolos sekarang: unggah kelimanya di Step 2 → lolos → di Step 3 hapus empat → rematch memenuhi kembali baris generik karena masih ada satu → checklist hijau → finalize lolos dengan empat dokumen wajib hilang.
Kerjakan (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 karena hasIncompleteChecklist sudah memblokir tiap item wajib
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. REQUIRED_SUPPORTING_DOCS tidak bisa dipakai apa adanya karena beranggapan konteks Perubahan
⚠️ Kerjakan bersama M14 di bawah. Tanpa itu, user terkunci oleh lima syarat yang tak satu pun tampil di layar.
🟠 M14 · Pesan blokir sebut dokumen mana yang kurang
Keputusannya: tampilan checklist lengkap tidak diperlukan. Tapi saat "Lanjut ke Tinjauan" mati, pesannya hanya "Dokumen belum lengkap" — PendirianExtractionPage.tsx:203-204 — tanpa menyebut dokumen apa. Karena panel checklist memang sengaja tidak menampilkan dokumen non-identitas, tidak ada permukaan lain yang bisa memberi tahu.
Cukup sebutkan nama dokumennya di pesan/tooltip. Tidak perlu merender daftar checklist.
🟠 M2 · Tombol "Lanjut" tidak memblokir file tanpa jenis
File tak dikenali dan file belum dikonfirmasi dicatat severity: "warning" — classification-validation.tsx:73-77 dan :84-89 — tapi gate hanya melihat "error" (:92, :108).
Perbaikannya praktis satu kata. Pastikan tidak malah memblokir kasus sah yang sekarang lewat.
🟠 M1 · KTP yatim tidak pernah terlihat
KTP yang tidak cocok dengan siapa pun mendapat matchedPersonName: "" — identity-matcher.ts:118-127. Sementara IdentityCards membangun kartunya 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.
Risikonya: berkas salah orang lolos tanpa disadari. Perlu permukaan baru — misalnya seksi "KTP belum cocok" di bawah daftar identitas.
🟠 M3 · Refresh di Step 1/2 menghapus board
Semua atom memakai atom() polos tanpa persistensi — store/klasifikasi.ts:127 dan :191-194. Jalur pemulihan ?resume= tidak menolong pada refresh biasa karena lastSubmissionAtom ikut hilang, dan rebuild otomatis langsung early-return: KlasifikasiPage.tsx:979.
atomWithStorage adalah perubahan kecil. Alternatif minimal: beri peringatan sebelum user kehilangan unggahan.
🟠 M5 · Polling tanpa batas pada status non-terminal
Polling berhenti pada READY/COMPLETED/ERROR (use-submission.ts:45, :80), tapi fallback return 2500 di :81 membuat submission yang tersangkut di EXTRACTING dipoll tiap 2,5 detik selamanya tanpa indikator apa pun ke user.
🟡 N6 · Putuskan nasib 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 task itu di-revert, alamat datang dari dokumen (pendirian-pt.ts:62-64).
Hapus, atau sambungkan sebagai pelengkap. Putuskan lebih dulu dan kabari Dev B — keputusan ini menentukan prioritas M7 dan N10 di sisi B.
🟡 N2 · Wording batas ukuran salah
Tertulis "maks 10MB", batas sebenarnya 100 MB (config.ts:165). Halaman /process masih dirutekan aktif (routes.tsx:289), jadi teksnya benar-benar terlihat.
Lokasi: document-type-selector.tsx:27, :34, :41, :48, ProcessPage.tsx:164.
🟡 N5 · Kontras confidence badge
Badge dirender untuk setiap card (classification-board.tsx:316), tapi tone confidence tinggi terlalu lembut sehingga terkesan hanya muncul untuk yang gagal. Ambangnya di :77.
Dev B — Review, validasi & integrasi SABH
Wilayah file: sabh-db · regions · sabh-region-lookup · cross-validator · review-data · document-processor · PtPendirianReviewPageV2 · review-engine/* · projectors/pt-pendirian-review · format-doc-date · validation-results · main.tsx
🟠 M6 + M15 + N3 · Pool SABH tanpa keepalive — mulai dari sini
Tiga baris konfigurasi, sudah terbukti lewat eksperimen, dan memperbaiki gangguan nyata.
Seluruh endpoint /api/regions/* di backend yang berjalan tidak merespons sama sekali (dites 20 dtk, tiga endpoint, semuanya 000), padahal backend yang sama menjawab /api/health dalam ~0 ms. Query SQL yang persis sama lewat proses baru selesai dalam 168 ms / 40 baris.
Sebabnya: MySQL SABH menutup koneksi idle setelah wait_timeout = 28800 dtk (8 jam), sementara proses backend hidup 21+ jam. Tanpa keepalive, mysql2 tetap membagikan koneksi mati itu. Dengan connectionLimit: 2 dan antrean akuisisi tanpa batas, permintaan menggantung selamanya alih-alih gagal.
Dibuktikan: memicu reload modul (tanpa mengubah kode) mengubah /api/regions/provinsi dari tanpa respons menjadi 200 dalam 0,09 dtk; ketiga endpoint pulih (0,09 / 0,02 / 0,03 dtk). Akan kambuh setiap 8 jam idle.
| Pool | keepalive/idle |
|---|---|
| regions.ts:16-24 | ❌ |
| sabh-db.ts:14-22 | ❌ |
| sabh-region-lookup.ts:21-29 | ❌ |
| apostille-spesimen-db.ts:33-35 | ✅ |
| pp-registry-db.ts:30 | ✅ |
| auth-db.ts:35 | ✅ |
| pp-wilayah-resolve.ts:28 | ✅ |
Kerjakan: tambahkan enableKeepAlive: true + idleTimeout ke tiga pool SABH, tiru pola empat pool yang sudah benar di repo yang sama. Pertimbangkan juga batas antre akuisisi supaya gagal-cepat, bukan menggantung.
M15 adalah bagian paling penting dari ini:
sabh-db.tspunya cacat identik, dan itulah pool yang dipakai rulePP29_MODALdanKBLI_VALID. Artinya cross-validation bisa menggantung — bukan gagal-cepat — di backend yang hidup lama. Logika rule-nya sendiri sudah terbukti benar; yang rapuh transportnya.
🔴 B1 · Tidak ada UI override validasi
Finalize menolak setiap FAIL yang belum di-override — review-data.ts:104-113 → 400 VALIDATION_FAILED. Tapi tidak ada cara meng-override di flow Pendirian PT.
Backend-nya sudah lengkap dan teruji: submissions.ts:1948-2006 — alasan wajib (:1961), audit event VALIDATION_OVERRIDE_SET (:1993-2003), dan daftar rule yang tidak boleh di-override (pp-bo-kriteria.ts:31).
Hook frontend-nya juga sudah ada — use-submission.ts:311 useOverrideValidation — tapi nol pemanggil. Panel validasi Step 3 sepenuhnya read-only (validation-results.tsx, 117 baris, nol kata "override"). Yang memakai endpoint itu cuma flow lain: Berakhirnya, Laporan RUPS, Pembubaran, Peleburan.
Kenapa penting: rule-nya heuristik dan bisa salah menuduh — OCR salah baca 0/O memicu NIK_KTP_AKTA FAIL; MODAL_HIERARCHY bahkan mencetak "kemungkinan salah baca nilai" (cross-validator.ts:463); AKTA_DATE_WINDOW gagal untuk akta lewat 60 hari (:1298) padahal pengajuan terlambat itu sah. Tanpa override, satu salah baca mengunci permohonan secara permanen.
Satu-satunya jalan keluar sekarang adalah debug bypass — dan itu mematikan semua validasi + semua konfirmasi field sekaligus, tanpa alasan tertulis, dan bukan untuk produksi.
Kerjakan: sambungkan useOverrideValidation ke panel validasi dengan dialog alasan wajib. Alternatif lebih murah kalau kepatuhan bukan prioritas: turunkan rule yang bisa FAIL secara sah (terutama AKTA_DATE_WINDOW) menjadi WARNING — tapi jejak siapa yang memutuskan melanjutkan jadi hilang.
🔴 B6 · Tinjauan bisa dibuka saat ekstraksi belum selesai
Mengganti address bar ke /pendirian/:id/review membuka halaman Tinjauan walaupun ekstraksi belum selesai dan validasi masih GAGAL.
Tidak ada penjaga di lapisan mana pun:
- Frontend — routes.tsx hanya punya RoleGuard (berbasis peran), nol step guard
- Backend — review-data.ts:24-40 hanya 404 kalau submission tidak ada; tidak menggerbangi status
- Lebih jauh — membukanya justru menstempel Ekstraksi sebagai selesai: PtPendirianReviewPageV2.tsx:106-108 mem-POST extraction-ack saat mount
Akibatnya notaris bisa mengonfirmasi field dari dokumen yang masih diekstrak — nilainya bisa berubah di belakang layar setelah dikonfirmasi.
Komentar kodenya menyebut stempel itu disengaja ("routing refinement 2026-07-20") untuk mencegah deep-link terlempar balik ke Step 2 selamanya. Niatnya sah — pertahankan niatnya, tutup celahnya.
Kerjakan: gerbangi berdasarkan status. Belum READY/COMPLETED → alihkan ke halaman ekstraksi. extraction-ack hanya dikirim ketika ekstraksi memang sudah selesai.
🟠 M4 · Kegagalan SABH menyamar jadi "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 → user melihat "Tidak ditemukan: 62019", padahal errornya connect ETIMEDOUT.
Bandingkan validatePp29Modal yang menanganinya dengan benar (SKIPPED "Gagal membaca BakumSetting" — :568-575). Tiru pola itu.
🟠 M12 · Koreksi manual NPWP ditimpa classifier saat ekstraksi
Jalur NPWP selalu meng-klasifikasi ulang file dan menulis balik documentType — document-processor.ts:2104-2117. Berbeda dari jalur Akta (:463-466) dan Bukti Setor (:1395) yang menghormati classificationOverride, jalur NPWP mengabaikannya.
Akibatnya koreksi manual "NPWP Lama" → "NPWP baru" di Step 2 berbalik sendiri setelah ekstraksi. Override yang diisi user diam-diam dibuang.
🟠 M13 · Format tanggal tidak seragam
Tanggal akta tampil YYYY-MM-DD, tanggal identitas DD-MM-YYYY. Dua sebab:
tanggal_aktatidak terdaftar sebagai date field — format-doc-date.ts:71DATE_FIELD_KEYShanya memuatdoc_date,tanggal_lahir,tanggal_terbit,berlaku_hinggaformatDateDisplay(:37) hanya dipakaifield-correction.tsxdanDocumentSetSection.tsx— bukan olehFieldRow/FieldListSectionyang dipakai review PT Pendirian
Formatter seragam "D MMMM YYYY" sudah ada dan sudah benar. Yang kurang: permukaan review PT tidak memanggilnya. Hati-hati membedakan bentuk tampil dari bentuk tersimpan — tanggal identitas sengaja disimpan DD-MM-YYYY karena dipakai validator (:64-69).
🟠 M9 · Urutan review bukan by confidence
Checklist meminta confidence terendah lebih dulu. Yang ada: urutan statis order: 0..10 per seksi — pt-pendirian-review.ts:131-143. Nol pengurutan by confidence di seluruh review-engine.
Berpasangan dengan permintaan "Smart Review satu per satu" yang juga belum ada — FieldListSection merender semua field sekaligus (FieldListSection.tsx:49-53), nol jejak wizard. Kalau review terpandu memang diinginkan, itu permintaan fitur — konfirmasikan dulu ruang lingkupnya sebelum mengerjakan.
🟠 M7 · Wilayah dijodohkan LIKE %nama% lintas dua database
Kelurahan dicocokkan ke kecamatan berdasarkan kemiripan nama, bukan foreign key — regions.ts:97-104, sabh-region-lookup.ts:45, :61. Nama kecamatan yang berulang antar kabupaten berisiko salah jodoh.
Prioritasnya bergantung N6 di sisi Dev A. Kalau kode mati alamat manual dihapus,
/api/regions/*kehilangan satu-satunya pemakai dan item ini turun prioritas. Tunggu kabar dari A.
🟠 M8 · Data basi antar-tab
refetchOnWindowFocus: false global (main.tsx:12) tanpa sinkronisasi antar-tab (nol BroadcastChannel/listener storage). Di Step 3 polling 2,5 dtk menutupinya; Step 4 tidak polling sama sekali, jadi data basi bisa bertahan diam-diam.
🟡 N1 · Wording "Nilai" → "Input yang benar"
Satu baris — FieldEditDialog.tsx:60 — tapi menyentuh 7 halaman review yang berbagi dialog itu: PT Pendirian, PT Perubahan, PT Akuisisi, PP Pendirian, PP Perubahan, PP Pembubaran, PP Peralihan. Plus salinan terpisah di ApostilleReviewPage.tsx:758.
Dua test mengunci label lama — PtPendirianReviewPageV2.test.tsx:456 dan PpPeralihanPtReviewPageV2.test.tsx:442 memakai getByLabelText("Nilai"). Perbarui juga, jangan sampai suite berubah merah.
🟡 N4 · isSabhAvailable() mengukur konfigurasi, bukan konektivitas
sabh-db.ts:7-9 hanya mengecek env non-kosong. 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). Namanya menjanjikan lebih dari isinya.
🟡 N7 · Dua token untuk konsep yang sama
SURAT_PERNYATAAN_DOMISILI (flow PT) vs SURAT_DOMISILI (flow PP, dipetakan ke enum DB DOMISILI) — pp-perubahan.ts:53-58, pp-perbaikan-pp.ts:64. Grep satu token akan melewatkan yang lain.
🟡 N9 · Kode KBLI non-5-digit dilaporkan "Tidak ditemukan"
Filter LENGTH(kode) = 5 di query (sabh-db.ts:91) membuat 620 dan 0000000 jatuh ke keranjang yang sama dengan kode yang memang tidak terdaftar. Terbukti saat probe. Notaris tidak bisa membedakan "salah ketik" dari "belum terdaftar di SABH".
🔍 N8 · Investigasi — ambang PP29 untuk PT tertutup = Rp 1
Terbaca langsung dari SABH: min_modal_dasar_tertutup_pp29 = **1** (PT terbuka = 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 dan menyerahkannya ke kesepakatan pendiri. Tapi labelnya "Minimum Modal Dasar (PP29)" menjanjikan pemeriksaan yang efektifnya tidak ada. Konfirmasikan ke tim hukum sebelum mengubah apa pun.
🔍 N10 · Investigasi — 40 provinsi di tabel wilayah
AHU_NOTARIAT.WILAYAH memuat 40 baris TINGKAT_WILAYAH = 1, sementara Indonesia saat ini punya 38 provinsi. Kemungkinan ada entri lama atau duplikat. Berhubungan dengan M7 — dan prioritasnya juga bergantung keputusan N6 di sisi A.
Titik koordinasi
1. PendirianExtractionPage.tsx disentuh keduanya
A untuk B2 dan M14; B untuk B1 yang menambahkan kontrol override ke panel validasi yang dirender halaman itu.
Kesepakatan: B hanya menyentuh validation-results.tsx; A memegang file halamannya. Kalau B butuh prop baru, A yang menambahkannya.
2. N6 menentukan prioritas M7 dan N10
Kalau A memutuskan menghapus kode mati alamat manual, /api/regions/* kehilangan satu-satunya pemakai — M7 dan N10 langsung turun prioritas. A putuskan N6 lebih dulu, lalu kabari B.
Catatan: keputusan itu tidak membatalkan M6. Pool sabh-db.ts tetap harus diperbaiki karena dipakai rule PP29 & KBLI.
3. B4 mengubah bentuk checklist
Kalau B sedang menyentuh projector review, koordinasikan — baris checklist yang dipecah jadi lima bisa memengaruhi apa yang tampil di Tinjauan.
Urutan yang disarankan
Hari pertama — kemenangan cepat, supaya ada yang bisa didemokan segera:
| Item | Kenapa duluan | |
|---|---|---|
| Dev B | M6 (+M15, N3) | Tiga baris konfigurasi, sudah terbukti memperbaiki gangguan nyata |
| Dev A | B2 | Satu cabang isError, meniru pola yang sudah ada di Step 4 |
Setelah itu baru masuk ke yang butuh berpikir panjang: A ke B5 lalu B4 + M14 (kerjakan berpasangan); B ke B1 lalu B6.
Minor dikerjakan menyisip di sela-sela, bukan diblok di akhir — kebanyakan hitungan menit.
Definition of done
Berlaku untuk keduanya, tiap kali sebelum push:
-
Jalankan dua suite, bukan satu:
bash cd backend && bun test cd frontend && bun run test --run
Baseline saat ini: backend 3031 pass · 1 skip · 2 fail, frontend 1092 pass · 0 fail. -
Dua kegagalan backend itu sudah ada sebelumnya dan environmental — bukan hasil kerja Anda:
-config gateway defaults > gpuServerDirectUrl falls back to gpuServerUrl— test membaca env nyata, tidak mengisolasi
-processPendirianPpViaCustomModel — BO persistence— memanggil PaddleOCR sungguhan,400 Image decode failed
Kalau angkanya berubah dari 2, itu Anda.
-
⚠️ Jangan menjalankan dua suite backend bersamaan. Test berbagi satu database. Saat suite penuh berjalan, run verifikator paralel memberi 24 pass / 4 fail; dijalankan sendirian, 28 pass / 0 fail tiga kali berturut-turut. Kegagalan seperti itu palsu.
-
Typecheck bersih:
bash cd backend && bunx tsc --noEmit cd frontend && bunx tsc --noEmit
Catatan lingkungan
- Pasangan dev_5 yang benar: frontend
:47031→ backend:47015(vite--strictPort,VITE_API_PROXY_TARGET=http://localhost:47015) - Worktree lain punya port sendiri dan HEAD berbeda — dev_6 misalnya tertinggal tiga commit dan tidak punya fix debug-toggle finalize. Pastikan menguji di dev_5
- SABH:
AHU_BADAN_HUKUM@:3306, userbht-read(read-only di level kredensial). Hidup dan terjangkau — balas ~750 ms dari proses baru - Cache GPU: kunci =
ocr:{tipe}:{model}:{sha256 isi file}, Redis db 2, TTL 7 hari. Ganti nama file tidak memaksa jalur dingin. Untuk benchmark jujur: pakai berkas yang isinya belum pernah diproses, atau setOCR_CACHE_ENABLED=false