think
16px
820px

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

flowchart TD OCR[Ekstraksi pertama
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:

  1. Route edit hanya menyentuh ExtractedField, lalu memanggil queueReMatchAndValidatesubmissions.ts:1355-1423
  2. copyDomisiliAddressToSubmission hidup di runPendirianDeriveStage (pendirian-pt.ts:43-76), yang terdaftar sebagai hooks.beforeValidation (pendirian-pt.ts:102) — hook itu dijalankan pipeline ekstraksi (processor.ts Phase 4b), bukan rematch
  3. 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
  4. Seandainya pun ada, fungsinya membaca DomisiliExtraction (submission-processor.ts:136-166), sedangkan edit notaris menulis ke ExtractedField — dua tabel berbeda

Konsekuensinya menyebar ke dua konsumen yang keduanya hidup:

  • Dual-write memirror kolom-kolom itu ke satelit SubmissionAddress dan PtSubmissiondual-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 dipakai AiSummaryCard di 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 ExtractedFieldDOMISILI_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-lookupwilayah 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 (/status untuk Pendirian, Perubahan, Akuisisi) → SubmissionStatusPage.tsx:502PtAddressForm → keempat hook use-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"; PtPendirianReviewPageV2 menyebut "alamat" 0 kali
  • useUpdateAddress (use-submission.ts:378) dan PATCH /api/submissions/:id/address (submissions.ts:1618) — nol pemanggil
  • Satu-satunya konsumen use-regions adalah pt-address-form.tsx; satu-satunya konsumen PtAddressForm adalah SubmissionStatusPage

N11: PtAddressForm tidak pernah membaca useReviewModereadOnly 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:

  1. Hapus useUpdateAddress + PATCH /:id/address — nol pemanggil, nol perubahan perilaku
  2. Ubah pemakaian di /status jadi render teks murni. Kolom alamatProvinsi/alamatKotaKabupaten/alamatKecamatan/alamatKelurahan menyimpan nama, bukan hanya id, jadi menampilkannya tidak butuh region API sama sekali
  3. 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.