think
16px
820px

Blackbox Testing — Fix Log (step-by-step change tracker)

Running record of every change made in response to the Round 1 findings. Each
entry: what was flagged, the verified verdict, the action taken, files touched,
commit, and verification. Branch: dev/4.

Legend — Verdict: REAL bug / NOT-A-BUG (intended) / NOT-AN-ISSUE (accurate) /
DESIGN (needs decision). Action: FIXED / REVERTED / NO-CHANGE / FLAGGED / PENDING.


Summary table

# Finding Verdict Action Commit
B1 Submit gate bypass (perubahan-family) REAL FIXED (+regression test) 45604ff6
B2 POST /api/perbaikan 500 (no OCR) REAL FIXED 45604ff6
B3 Pembubaran APPROVE triggers voting RE-DECIDED (SABH parity) FIXED (working tree)*
Perbaikan per-section approve guard RE-DECIDED FIXED (working tree)*
Missing data-testid on submit buttons REAL FIXED 2cda1561
Apostille outOfType "dead-code" NOT-AN-ISSUE NO-CHANGE
Pembubaran "misleading" text NOT-AN-ISSUE NO-CHANGE
A1 Required-docs overridable (Pendirian) DESIGN → intended NO-CHANGE
A2 Akta date window (Pembubaran) REAL (SABH parity) FIXED 237cfc31
A2b Akta date window (Berakhirnya) REAL (SABH parity, 30d) FIXED b766e30b
A4 Peralihan finalize confirm-all parity REAL FIXED 44221772
A3.1 Autofill Modal from Bukti Setor (Perubahan) REAL (build) FIXED a29a32c0
A3.2 Autofill Alamat from Surat Domisili (Perubahan) REAL (build) FIXED 4d6d2c90
A3.3 Mirror autofill into PP Perbaikan DESIGN DEFERRED

* B3 and the Perbaikan-guard were REVERTED during the fix session (see entries
below), then subsequently RE-IMPLEMENTED with rewritten tests. Those re-done
changes are uncommitted in the working tree and are NOT authored under this
tracked session — confirm the SABH-parity rationale before including them in the
PR. See "Working-tree reconciliation" at the bottom.


Detailed entries

B1 — Submit gate bypass for the perubahan-family — FIXED

  • Flagged: Akuisisi/Penggabungan submit lets an un-overridden validation FAIL reach COMPLETED.
  • Verified: REAL, and broader — Perubahan PT leaks identically (all three types never called checkFailGate; FAIL rules map to approvable:false sections, so section-approval never catches them).
  • Fix: backend/src/routes/perubahan.ts — add checkFailGate to both submit arms (Perubahan + Akuisisi/Penggabungan), respecting the debug bypass.
  • Test: backend/src/routes/__tests__/akuisisi-routes.test.ts — new regression test; proven to FAIL without the fix, PASS with it.
  • Commit: 45604ff6.

B2 — POST /api/perbaikan multipart 500 — FIXED

  • Flagged: Uploading a Surat Pernyataan via the multipart endpoint 500s; submission stuck EXTRACTING.
  • Verified: REAL — handler created docs UPLOADED but never triggered OCR; runSuratPernyataanExtraction throws on empty rawText. (UI uses the SK-generator path, so not user-facing → effective severity Medium.)
  • Fix: backend/src/services/perbaikan-processor.ts — OCR-on-demand when rawText is empty (no-op when text already exists).
  • Commit: 45604ff6.

B3 — Pembubaran APPROVE triggers shareholder voting — REVERTED (not a bug)

  • Flagged: Dissolution should not go through shareholder voting.
  • Verified: NOT-A-BUG — dedicated tests assert dissolution voting is intended ("ROSTER-SOURCE PIN"); UUPT requires shareholder approval for dissolution. The initial fix broke intended behavior.
  • Action: backend/src/routes/verifikator.ts change REVERTED. No code change ships.

Perbaikan per-section approve guard — REVERTED (not a clear bug)

  • Flagged: Section-approve guard reads details.correctionId (top-level) but data lives in details.instances[].correctionId, so the guard never fires.
  • Verified: The read mismatch is real, BUT making the guard fire breaks the intended flow (Task 38 test: approve section → override FAIL at submit). Whether a document-presence FAIL should block section-approval vs only a value-invalid FAIL is a PRODUCT decision.
  • Action: Fix REVERTED. Submit gate (UNRESOLVED_FAIL_VALIDATION) still catches it → no bypass. Needs PO decision.

Missing data-testid on submit buttons — FIXED

  • Fix: added stable testids to the Kirim/Finalisasi buttons that lacked them: pembubaran/peleburan/berakhirnya/laporan-rups review pages (open + confirm) and the shared ReviewProgressHeader finalize CTA (covers Perubahan/Akuisisi/Pendirian V2).
  • Files: 5 frontend files. Commit: 2cda1561. Purely additive; typecheck + 209 review-engine tests green.

Apostille outOfType "dead-code" — NO-CHANGE (not an issue)

  • Verified: outOfType is an ACTIVE guard (ApostilleClassificationPage.tsx:903 disables proceed when out-of-service docs exist). Not dead. Removing would break a real check.

Pembubaran "misleading" text — NO-CHANGE (accurate)

  • Verified: legally accurate — Ps.89(1) >=3/4 is the real dissolution quorum; the "30 hari" text is Ps.147(1) liquidator duty, correctly marked "boleh menyusul (tidak menghalangi submit)".

A1 — Required-docs overridable (Pendirian) — NO-CHANGE (intended)

  • Decision (Efran): overridable is correct by design — lets a notaris override a falsely-classified required doc. Working as intended.

A2 — Akta date window (Pembubaran) — FIXED (SABH parity)

  • SABH cross-check: FormDataPerseroan.validateTglAkta = 60 days (Pendirian), FormDataPerseroanPerubahanAkta = 30 days (Perubahan) — our values are CORRECT, untouched. FormDataPerseroanPembubaran.validateTglAkta enforces NO window (only non-empty date). Our pembubaran ruleset wrongly applied the 60-day window.
  • Fix: backend/src/flow-engine/rules/pembubaran/index.ts — drop AKTA_DATE_WINDOW from PEMBUBARAN_SHARED_RULE_CODES (peleburan-precedent exclusion). Guard test updated (rules-pembubaran.test.ts: count 15->14, asserted absent).
  • Commit: 237cfc31.

A2b — Akta date window (Berakhirnya) — FIXED (SABH parity, 30-day)

  • SABH: "Perubahan Status Badan Hukum" (maps to Berakhirnya via tbl_verifikasi_berakhirnya_perseroan) uses 30 days + future-date check. Ours applied AKTA_DATE_WINDOW at 60 days.
  • Decision (Efran): set to 30 days for now; revert to 60 if later found otherwise.
  • Fix: backend/src/services/cross-validator.ts — add aktaDateWindowDays? to ValidationInput; validateAktaDateWindow uses input.aktaDateWindowDays ?? (isPerubahan ? 30 : 60). backend/src/flow-engine/context/berakhirnya-context.ts — set base.input.aktaDateWindowDays = 30.
  • Test: backend/src/services/__tests__/cross-validator.test.ts — new block: passes at 30 days, fails at 45.
  • Commit: b766e30b.

A4 — Peralihan PP->PT finalize confirm-all parity — FIXED

  • Verified: REAL — the generic engine finalize (routes/review-data.ts /:id/review/finalize, used by Pendirian) enforces collectUnconfirmed() server-side; peralihan's own /finalize did not (frontend-gated only → a direct API call could finalize unconfirmed).
  • Fix: backend/src/routes/pp-peralihan-pt.ts — add collectUnconfirmed() gate (bypassable via the same debug flag). Mirrors the tested generic gate.
  • Commit: 44221772. Typecheck + existing peralihan suite green.

A3 — Autofill Domisili/Bukti Setor (PP Perubahan) — FIXED (A3.1 + A3.2)

  • Decision (Efran): if OCR already reads the file, it should autofill.
  • Scoping (2026-08-19). The two doc types persist differently:
  • Bukti SetorBuktiSetorExtraction TABLE (jumlahSetor), NOT ExtractedField. Target carrier key modal.
  • DomisiliDomisiliExtraction TABLE (jalan, rt, rw, kelurahan, kecamatan, kotaKabupaten, provinsi, kodePos). Target carrier keys camelCase (alamat, rt, rw, kelurahan, kecamatan, kabupatenKota, provinsi, kodePos) → key map.

A3.1 — Modal from Bukti Setor — FIXED

  • Fix: services/pp-perubahan-processor.ts fillModalFromBuktiSetor(submissionId) — active when jenis MODAL (5); read BuktiSetorExtraction.jumlahSetor, upsertField(carrier, "modal", ...). Wired into POST /:id/document.
  • Test: services/__tests__/pp-perubahan-modal-autofill.test.ts (3 tests).
  • Commit: a29a32c0. Typecheck + tests green.

A3.2 — Alamat from Surat Domisili — FIXED

  • Fix: services/pp-perubahan-processor.ts fillAddressFromDomisili(submissionId) — active when jenis ALAMAT_PERSEROAN (3); read DomisiliExtraction, key-map, upsertField per column. Only non-null/non-empty values written (partial scan never clobbers with null). Wired into POST /:id/document.
  • Test: services/__tests__/pp-perubahan-address-autofill.test.ts (3 tests: maps columns, skips null, no-op when jenis inactive).
  • Commit: 4d6d2c90. Typecheck + 6 autofill tests (A3.1+A3.2) green.

A3.3 — Mirror into PP Perbaikan — DEFERRED (design, not a clean mirror)

  • Finding: PP Perbaikan already autofills owner identity from KTP/NPWP (fillOwnerIdentityFromPerbaikanDocs, called post-processDocument in pp-perbaikan-pp.ts). But it is a CORRECTION flow: values live as PerbaikanCorrection.baruValue keyed by fieldKey, NOT as carrier ExtractedField rows — so the A3.1/A3.2 upsertField-onto-carrier pattern does not map 1:1. Autofilling modal/address into a correction's baruValue is a distinct design (which correction fields, and does an OCR value pre-fill a value the notary is meant to assert?).
  • Recommendation: treat as its own scoped pass with a product decision, not a mechanical mirror. Not blocking the PR — the A3 intent (Perubahan autofill) is delivered.

Round 2 regression (2026-08-19)

Re-ran each fixed area in isolation (this suite has known cross-test DB contention,
so per-area isolation is the reliable signal). All green.

Area Fix Round 1 (baseline) Round 2 (after fix)
B1 gate (akuisisi-routes) checkFailGate on submit FAIL slipped to COMPLETED 14 pass / 0 fail (regression test: FAIL now blocks, override unblocks)
B1 area (perubahan submit) 13 pass / 0 fail (no regression)
B2 area (perbaikan) OCR-on-demand 500 / stuck EXTRACTING 9 pass / 0 fail
B3 revert (verifikator-decide) reverted 12 pass / 0 fail (intended voting preserved)
A2 (pembubaran rules + routes) drop AKTA_DATE_WINDOW 60-day window applied 15 pass / 0 fail (window absent, SABH parity)
A4 (peralihan) confirm-all gate server skipped confirm gate 15 pass / 0 fail
Frontend testids (review-engine) added testids 209 pass / 0 fail
  • Backend typecheck: clean. Frontend typecheck: clean.
  • Net: 4 fixes verified holding (B1, B2, A2, A4); 2 reverts confirmed (B3, perbaikan-guard); testids in place.

Still open (bigger work / product)

  • A3.3 Autofill into PP Perbaikan — deferred design (see A3.3 above).
  • Category C (OCR/extraction, high impact): Laporan RUPS empty fields on real scans; classifier mislabels (PP Surat Pernyataan → BUKTI_TRANSFER, Apostille KTP-PDF).

Working-tree reconciliation (2026-08-21)

Snapshot check before the PR. Everything the blackbox session owns is COMMITTED
and test-backed (B1, B2, testids, A2, A2b, A4, A3.1, A3.2). Beyond those, the
working tree carries hanging changes that were present at session start and are
NOT authored by this tracked session
— the user asked to include them for now.
Inventory:

Modified (uncommitted) — PT / Pembubaran / Perbaikan (NOT yayasan/perkumpulan):
- routes/verifikator.ts + routes/__tests__/verifikator-pembubaran.test.ts — Pembubaran now SKIPS shareholder e-voting: initial APPROVE → SELESAI, no ShareholderVote rows (rationale in code: UUPT Ps.142, RUPS already decided at the akta). This REVERSES the B3 "not-a-bug / voting is intended" verdict above. Tests rewritten to assert the new behavior. 8/8 green.
- routes/perbaikan.ts + perbaikan-approve.test.ts + perbaikan-submit-override.test.ts — section-approve guard now reads details.instances[].correctionId (so it actually fires). This REVERSES the earlier "reverted" Perbaikan-guard verdict. Tests adapted. Green.
- frontend/PtAkuisisiReviewPageV2.tsx, PtPerubahanReviewPageV2.tsx — add data-testid to the two remaining submit buttons.
- frontend/VerifikatorPembubaranReviewPage.tsx — RUPS subtitle drops the "kuorum Ps.89(1) ≥3/4" claim (pembubaran doesn't validate kuorum, SABH parity).

Untracked — the actual yayasan/perkumpulan/apostille work:
- backend/scripts/peek-akta-perkumpulan.ts, docs/perubahan-perkumpulan-sabh-parity-spec.md, docs/samples/perkumpulan/ (perkumpulan; sample PDFs contain PII — keep OUT of git), docs/apostille-workflow-sections.md (apostille).

Action needed before PR: confirm the two reversals (verifikator voting, perbaikan
guard) are the intended final decisions — they contradict verdicts recorded earlier
in this log. Tests pass either way; the question is product intent, not correctness.


PR-prep: post-merge fitness fixes (2026-08-21)

After merging dev/1 + dev/2 + master into dev/4, the full suite was green on all
3721 functional backend tests + 1230 frontend, but 5 architecture "fitness"
guards went red (merged feature code past grandfathered baselines). All 5
resolved so the branch opens green. Backend typecheck clean; architecture suite
25/25; affected functional tests (pp-perubahan + nirlaba) 182/0.

# Guard Verdict Fix Commit
F1 document-delete-unlinks (4 nirlaba routes leak file on akta re-upload) REAL bug (PII/leak) Route deletes through purgeDocuments() (rows + unlink) 7d7ffa49
F2 file-size: pp-perubahan.ts 1562 > 1556 mine (A3.1/A3.2) Extract runPerubahanDocAutofill; route back to 1547 7e89c01c
F3 model-deps: NIRLABA_ENTITY_EARLY_URL undocumented REAL (undeclared upstream) Declare in GATEWAY_MODEL_DEPENDENCIES 0f794621
F4 file-size: submissions.ts 2347 > 2270 dev/2 (legit growth) Ceiling 2270 -> 2347 with cleanup note 0f794621
F5 body-validation: nirlaba-pembubaran.ts raw c.req.json() dev/2 (tolerant blob) Budget 1, sanitized by readPembubaranNirlabaForm 0f794621
F6 copy-forks: 4 nirlaba route pairs dev/2 (sibling flows) Grandfather with shared-helper cleanup note 0f794621

F1 is the only substantive one — a real file leak (unowned PDFs left in
uploads/). F2 is a clean refactor. F3 documents a real upstream. F4/F5/F6 are
honest baseline records for legitimately-new dev/2 code, each with a cleanup
pointer (not dodges).


Final review dev/4 vs master (2026-08-21)

Compared the full branch against origin/master (56 commits / 164 files) across
three risk clusters with parallel reviewers, then verified findings by hand.

  • Merge integration (files both dev/1 + dev/2 touched: schema/migrations,
    registry, index.ts, type-inference, review-registry, domains/policies,
    klasifikasi): 0 bugs. Auto-merge was semantically clean; every new
    SubmissionType/DocumentType exists once in schema + once in migrations, all
    flows registered once, no route collisions, exhaustiveness holds.
  • Nirlaba routes + document lifecycle: 0 bugs. The purge fix (F1) is
    SAFE — nirlaba akta docs are single-owner (fresh UUID per upload, no
    dedup-by-hash), so purgeDocuments never touches a shared file.
  • My session's fixes: 7 of 8 correct; 1 real finding (below).

R1 — Peralihan A4 gate was incomplete — FIXED

  • Finding: the A4 finalize gate (collectUnconfirmed) only scans flat
    ExtractedField rows for documentType in {AKTA, DOMISILI}. The peralihan deed is
    AKTA_PERALIHAN_PP_PT, so its five surfaced field-lists (data_perseroan,
    akta_notaris, notaris_pengganti, peralihan_recital) were skipped server-side
    while the UI submitGate required them — a direct API /finalize could complete
    with unconfirmed flat akta fields (the exact bypass A4 meant to close). Med,
    API-only (UI enforces), lowest-traffic flow.
  • Structure check that shaped the fix: the gate's modal (block 2) and roster
    (block 3) scans already run for every doc, so ONLY the flat-field block was the
    gap; modal is the atomic aktaModal entity, already gated (so it is NOT in the
    reviewable-field set).
  • Fix: field-section-mapping.ts adds isReviewablePeralihanField (shared
    akta keys + 8 recital keys); submission-confirmation-gate.ts scans
    AKTA_PERALIHAN_PP_PT with the same meta-field exclusion as AKTA (no wedge). New
    peralihan-confirmation-gate.test.ts: unit + a drift-guard pinning the gate's
    set to the projector's surfaced keys + a DB integration test. seedPeralihan
    updated to seed fields notaris-confirmed (models the real pre-finalize state).
  • Commit: 6afb40cb. Verified: gate test 3/0, peralihan routes 15/0,
    architecture 25/0, typecheck clean.