Adendum Audit — Alamat & Kontak Perseroan (Pendirian PT)
Lanjutan dari hasil-audit.md dan pembagian-perbaikan-pendirian-pt.md · Repo: ahu-ocr-dev/dev_5 (branch dev/5, HEAD cd2dff46) · 2026-08-05
Ditemukan saat menelusuri N6. Kode temuan melanjutkan penomoran yang ada (terpakai: B1–B6, M1–M9, M12–M15, N1–N10) — M16, M17, M18, N11.
Semuanya berakar pada satu hal yang sama: Submission.alamat* dan Submission.teleponPerseroan/emailPerseroan adalah salinan, bukan turunan. Ditulis sekali saat ekstraksi pertama, lalu tidak pernah disegarkan oleh apa pun — tidak oleh edit notaris, tidak oleh rematch, tidak oleh penghapusan dokumen sumbernya.
Ringkasan
| Kode | Temuan | Severity | Terjadi saat |
|---|---|---|---|
| M16 | Koreksi alamat/kontak di Step 4 tidak pernah sampai ke kolom Submission → satelit audit & ringkasan AI menyimpan nilai OCR yang salah |
🟠 Major | Jalur normal — cukup mengoreksi satu field |
| M17 | Nilai kontak yatim (dokumennya sudah dilepas) tampil sebagai USER_CONFIRMED, confidence 100, dan tidak bisa diedit |
🟠 Major | Setelah detach DATA_KONTAK |
| M18 | DATA_KONTAK pengganti diabaikan diam-diam — guard write-once tidak pernah bisa false lagi |
🟠 Major | Ganti dokumen kontak |
| N11 | PtAddressForm di /status membuang setiap suntingan tanpa indikasi apa pun |
🟡 Minor | Bagian dari keputusan N6 |
M16 yang paling mendesak: ia tidak butuh skenario aneh. Notaris mengoreksi alamat yang salah — persis hal yang fitur review ini ada untuk melakukannya — dan rekaman auditnya diam-diam menyimpan yang salah.
Kenapa bisa begitu: dua sumber kebenaran yang tidak pernah bertemu
dokumen DOMISILI / DATA_KONTAK] --> DE[(DomisiliExtraction
ContactInfoExtraction)] OCR --> EF[(ExtractedField
baris per fieldKey)] DE -->|"copyDomisiliAddressToSubmission
applyContactInfoToSubmission
SEKALI, di hooks.beforeValidation"| COL[(Submission.alamat*
teleponPerseroan / emailPerseroan)] EF -->|join lewat SubmissionDocument| TINJAUAN["Step 4 — Tinjauan
section domisili"] COL --> SAT[(SubmissionAddress / PtSubmission
satelit dual-write)] COL --> AI["Ringkasan AI
POST /summarize"] COL -->|"sub.teleponPerseroan"| KONTAK["Step 4 — section kontak"] NOTARIS([Notaris mengoreksi di Step 4]) -->|PATCH field| EF NOTARIS -.->|"tidak pernah sampai"| COL style COL fill:#fee,stroke:#c00 style NOTARIS fill:#efe,stroke:#0a0
Garis putus-putus merah itulah M16. Rantai buktinya:
- Route edit hanya menyentuh
ExtractedField, lalu memanggilqueueReMatchAndValidate— submissions.ts:1355-1423 copyDomisiliAddressToSubmissionhidup dirunPendirianDeriveStage(pendirian-pt.ts:43-76), yang terdaftar sebagaihooks.beforeValidation(pendirian-pt.ts:102) — hook itu dijalankan pipeline ekstraksi (processor.ts Phase 4b), bukan rematch- Loop matcher standar di rematch menangani KTP, NPWP, BUKTI_SETOR, checklist domisili, dan DATA_KONTAK — rematch.ts:52-59. Tidak ada penyalinan alamat di sana
- Seandainya pun ada, fungsinya membaca
DomisiliExtraction(submission-processor.ts:136-166), sedangkan edit notaris menulis keExtractedField— dua tabel berbeda
Konsekuensinya menyebar ke dua konsumen yang keduanya hidup:
- Dual-write memirror kolom-kolom itu ke satelit
SubmissionAddressdanPtSubmission— dual-write.ts:41-54. Ini justru lapisan audit, yang seharusnya paling jujur - Ringkasan AI menyuapkan
Jalan: ${submission.alamatJalan}ke LLM — submissions-summarize.ts:207. Endpoint ini dipakaiAiSummaryCarddi Peleburan, Pembubaran, Berakhirnya, Akuisisi, dan Perubahan
Section domisili di Tinjauan tidak ikut salah — ia dibangun dari ExtractedField (dijelaskan di pt-pendirian-review.ts:51-58). Justru itu yang membuat M16 tidak terlihat: layar menampilkan yang benar, penyimpanan memegang yang salah.
M17 · Nilai yatim tampil sebagai "dikonfirmasi notaris"
Berbeda dari alamat, section kontak membaca kolom Submission langsung — pt-pendirian-review.ts:725-726:
addContactScalarField("entity_phone", "teleponPerseroan", sub.teleponPerseroan);
Lepaskan dokumen DATA_KONTAK, dan di :711-722 noCarrier menjadi true sementara value masih memegang nilai lama:
const noCarrier = !contactDoc;
confirmed: noCarrier ? value != null : overlay.confirmed, // → true
provenance: (noCarrier && value != null) ? "USER_CONFIRMED" : ...,
editable: !noCarrier, // → false
Hasilnya nilai dari dokumen yang sudah tidak ada dirender sebagai kebenaran yang dikonfirmasi notaris, confidence 100, dan tidak bisa diperbaiki dari layar.
Logikanya tidak salah niat: komentar K.36 di situ mengasumsikan "tanpa DATA_KONTAK, nilai ini pasti ketikan pemohon sendiri di step 1". Asumsi itu benar sebelum dokumen bisa dilepas dari submission; sekarang tidak, dan projector tidak punya cara membedakannya karena kolomnya tidak menyimpan provenance apa pun.
M18 · Dokumen kontak pengganti diabaikan diam-diam
Penyalinan kontak bersifat write-once di dua tempat — processor.ts:216-218 dan submission-processor.ts:738-742:
if (!submission.teleponPerseroan && extraction.entityPhone) updates.teleponPerseroan = ...
Karena nilai lama tidak pernah dikosongkan, guard !submission.teleponPerseroan tidak akan pernah true lagi. Hapus DATA_KONTAK lalu unggah yang baru → nomor lama bertahan, nomor baru dibuang tanpa log, dan M17 memberinya label USER_CONFIRMED.
Bandingkan dengan alamat: copyDomisiliAddressToSubmission melakukan update tanpa guard, jadi penggantian domisili berfungsi pada pass pertama. Yang rusak di sisi alamat hanya penghapusan dan pengeditan.
Catatan: guard write-once yang serupa di rups-attestation-extract.ts:228-232 benar dan jangan ikut diubah — di sana field-nya memang milik user ("never clobber manual input"). Yang salah adalah memakai pola itu untuk kolom yang tidak punya permukaan input sama sekali.
Perbaikan yang disarankan
Satu perubahan menutup M16, M18, dan sisi data dari M17 sekaligus: turunkan ulang kolom-kolom itu dari ExtractedField pada setiap rematch.
Rematch berjalan setelah setiap edit field dan setiap detach dokumen, jadi pola "kosongkan lalu isi ulang dari dokumen yang bertahan" — persis pola checklistResetDocTypes yang sudah ada di rematch.ts:19-22 — otomatis benar untuk ketiga skenario: koreksi, penggantian, dan penghapusan. Guard write-once di kedua tempat ikut dibuang, karena tidak ada input manual yang perlu dilindungi (lihat keputusan N6 di bawah).
M17 tetap butuh satu sentuhan tersendiri di projector: begitu kolomnya jujur, noCarrier harus berhenti mengklaim USER_CONFIRMED untuk nilai yang tidak punya carrier. Nilai tanpa dokumen dan tanpa jejak edit bukan "dikonfirmasi" — ia tidak diketahui asalnya.
Ganjalan yang perlu diketahui di muka: kolom alamatProvinsiId / alamatKabupatenId / alamatKecamatanId / alamatKelurahanId tidak punya padanan di ExtractedField — DOMISILI_KEYS hanya memuat nama-namanya (pt-shape.ts:69-79); id-nya berasal dari resolusi region saat ekstraksi. Kalau notaris mengedit nama kelurahan, id-nya harus di-resolve ulang lewat sabh-region-lookup — wilayah file Dev B, dan bergantung pada M6 (pool keepalive) yang sedang mereka kerjakan.
Test yang pasti tersentuh: backend/src/review/__tests__/pt-pendirian-review.test.ts sudah punya assertion untuk perilaku noCarrier/entity_phone. Ekspektasinya memang perlu berubah — bukan test-nya yang salah, tapi perilaku yang di-assert-nya yang salah.
Keputusan N6: hapus
N6 menanyakan nasib kode mati alamat manual. Penelusuran menunjukkan /api/regions/* sebenarnya masih dipanggil, tapi fitur yang dilayaninya sudah pensiun:
routes.tsx:159-161(/statusuntuk Pendirian, Perubahan, Akuisisi) → SubmissionStatusPage.tsx:502 →PtAddressForm→ keempat hookuse-regions→ keempat endpoint- Tapi form itu dipasang dengan
onSave={NOOP_SAVE}(SubmissionStatusPage.tsx:384) - Fitur input alamat manual dicabut saat rewrite V2 — commit
76010bcb "retire the legacy hand-built review page";PtPendirianReviewPageV2menyebut "alamat" 0 kali useUpdateAddress(use-submission.ts:378) danPATCH /api/submissions/:id/address(submissions.ts:1618) — nol pemanggil- Satu-satunya konsumen
use-regionsadalahpt-address-form.tsx; satu-satunya konsumenPtAddressFormadalahSubmissionStatusPage
N11: PtAddressForm tidak pernah membaca useReviewMode — readOnly dari provider tidak menyentuhnya. Field-nya benar-benar bisa diketik, dan penyimpanannya bukan lewat tombol melainkan on-change (pt-address-form.tsx:600-620): ganti dropdown provinsi → onSave langsung dipanggil → dibuang. Jadi ini bukan tampilan read-only yang kebetulan tidak menyimpan; ini formulir yang berbohong.
Alasan menghapus, bukan menyambungkan: Step 4 sudah menjadi permukaan koreksi alamat yang sah — dengan provenance, audit event NOTARIS_FIELD_EDIT, dan diff "apa kata OCR vs apa kata notaris". Menghidupkan kembali PtAddressForm berarti dua permukaan koreksi untuk data yang sama dengan model confirm yang berbeda. Itu justru masalah yang dihindari rewrite V2.
Urutan aman:
- Hapus
useUpdateAddress+PATCH /:id/address— nol pemanggil, nol perubahan perilaku - Ubah pemakaian di
/statusjadi render teks murni. KolomalamatProvinsi/alamatKotaKabupaten/alamatKecamatan/alamatKelurahanmenyimpan nama, bukan hanya id, jadi menampilkannya tidak butuh region API sama sekali - Setelah itu
/api/regions/*benar-benar kehilangan pemanggil terakhirnya
Untuk Dev B: kalau rute ini diambil, M7 dan N10 turun prioritas seperti dicatat dokumen pembagian. M6 (pool keepalive sabh-db.ts) tetap berdiri — dipakai rule PP29 & KBLI. Dan perhatikan ironinya: endpoint HTTP-nya boleh hilang, tapi sabh-region-lookup di backend justru jadi lebih penting karena perbaikan M16 membutuhkannya untuk me-resolve ulang region id. Sepakati ini sebelum salah satu pihak menghapus apa pun.
Editabilitas field: konteks
Pertanyaan sampingan yang muncul saat tes manual — kenapa hanya sebagian field yang bisa diedit. Jawabannya confidence-driven, pt-field-meta.ts:18-23:
if (conf >= 0.95) return "LOCKED"; // → editable: false
if (conf < 0.7) return "FLAGGED";
return "EDITABLE";
Projector memakainya sebagai editable: f.status !== "LOCKED" (pt-pendirian-review.ts:221).
Efek sampingnya layak dicatat sebagai bahan diskusi terpisah, bukan temuan: OCR yang salah tapi percaya diri (confidence 0.97) terkunci total — tidak ada jalan koreksi sama sekali, dan tidak ada permukaan yang memberi tahu notaris kenapa field itu tidak bisa disentuh.