think
16px
820px

Fix report — BSBHP-05 classification gate (berakhirnya flow)

Branch: fix/berakhirnya-status-badan-hukum
Date: 2026-07-23
Scope: the BSBHP-05 auto-classification gate bug found via manual UI testing, plus
template filename/footer cleanup and full e2e + unit coverage.

TL;DR

A printed-and-scanned BSBHP-05 ("Bukti Pengumuman Koran — Pasal 149") failed to
auto-classify on full-package upload, while Pasal-152, Laporan Likuidasi and Surat Permohonan
landed normally. Root cause: a reachability gate, not the form-code regex.
refineSupportingLabel only ran matchBsbhpFormCode when the GPU label was low-confidence
or in a hard-coded allow-list (CONFIDENTLY_WRONG_SUPPORTING_LABELS). A scanned BSBHP-05
carries a neraca table, so the GPU landed it on BUKTI_TRANSFER-NERACA at scan-noise high
confidence
— a label not in that allow-list — so the function returned early and the
printed FORMULIR SABH BSBHP-05 code was never checked.

Fixed by checking the form code first, for every label at any confidence (the printed code
is ~100 % precise), replacing the fragile allow-list. The one deliberate exception: a
high-confidence compact ID card (KTP/NPWP/…) is skipped, since a full-page form can never
be one — this preserves those flows' no-OCR fast path.

1 · Diagnosis evidence (verified, not assumed)

Reproduced against the real GPU classifier + PaddleOCR on the actual bsbhp-05.pdf in
uploads/klasifikasi-temp/.

(a) The form-code anchor itself works. OCR rawText of bsbhp-05.pdf:

HEAD(0..300): "FORMULIR SABH BSBHP-05 — BUKTI PENGUMUMAN SURAT KABAR PASAL 149 UUPT
              (PENGUMUMAN KEPADA KREDITOR / RENCANA PEMBAGIAN SISA KEKAYAAN) …"
FOOT(-400):   "… Formulir BSBHP-05 v1 — dihasilkan oleh SABH AI Platform. Jangan mengubah …"
contains BSBHP-05 token: true
matchBsbhpFormCode(rawText) → BUKTI_PENGUMUMAN_149        ✅ recognised correctly

(b) The gate skipped it. GPU rawClassification + confidence for the file, and the gate
behaviour over that same OCR text:

Input to refineSupportingLabel Before fix After fix
clean digital bsbhp-05.pdf → GPU BUKTI_TRANSFER-NERACA@0.0001 (low) BUKTI_PENGUMUMAN_149 (passes — low-conf gate opens) BUKTI_PENGUMUMAN_149
scanned form → GPU BUKTI_TRANSFER-NERACA@0.95 (high) BUKTI_TRANSFER-NERACA (form code skipped) BUKTI_PENGUMUMAN_149
scanned form → GPU SURAT_PERNYATAAN_DOMISILI@0.99 (high) SURAT_PERNYATAAN_DOMISILI BUKTI_PENGUMUMAN_149

The clean digital PDF only escaped the bug because it scored NERACA@0.0001 (below the 0.6
min-confidence, so the re-examine gate opened anyway). The physical scan draws the same label
at high confidence — confirmed live in the e2e below, where a rasterised BSBHP-06 scan drew
a 0.8937 GPU confidence and still auto-landed after the fix.

This is purely a gate bug — there is no OCR problem in the scan; matchBsbhpFormCode
recognises the text every time.

2 · The fix

flowchart TD A["refineSupportingLabel(label, confidence, ocrText)"] --> B{"label starts with AKTA_?"} B -- yes --> R1["return label"] B -- no --> C{"HIGH-confidence
compact ID card?
(KTP/NPWP/…)"} C -- yes --> G["confidence / allow-list gate
(unchanged fast path)"] C -- no --> D["matchBsbhpFormCode(ocrText)"] D -- "form code found" --> R2["return BSBHP lane
(auto-adopt)"] D -- "null" --> E{"label == BERITA_ACARA_RUPS?"} E -- yes --> F["narrow laporan rescue"] E -- no --> G G --> R3["confidence gate / re-examine
(free korans, etc.)"]

Before: the form code was only reachable via the BERITA_ACARA_RUPS branch or the
CONFIDENTLY_WRONG_SUPPORTING_LABELS allow-list (OTHER, SURAT_KETERANGAN_BANK,
BUKTI_TRANSFER-*). Any high-confidence label outside that set returned early.

After: the form code is checked first, above every gate, for all non-AKTA labels — with
one exception (high-confidence ID cards, which a full-page form can never be, preserving their
no-OCR fast path). The allow-list stays only for free koran clippings (no printed code),
which remain suggest-only by design.

Template cleanup (steps 4–5)

  • Renamed the four templates to descriptive names (the BSBHP-0N code stays inside the
    document as the OCR anchor):
    bsbhp-surat-permohonan.docx, bsbhp-laporan-akhir-likuidasi.docx,
    bsbhp-bukti-pengumuman-pasal-149.docx, bsbhp-bukti-pengumuman-pasal-152.docx.
  • Replaced the AI-generated-looking footer
    (Formulir BSBHP-0N v1 — dihasilkan oleh SABH AI Platform. Jangan mengubah baris judul…)
    with a plain formal stamp Kode Formulir: BSBHP-0N — no "AI Platform", no do-not-change
    instruction. The code is retained as the footer OCR-fallback anchor.
  • Hardened matchBsbhpFormCode with a distinctive "Kode Formulir: BSBHP-0N" probe so a
    title-damaged BSBHP-03/04 scan resolves its own code and never grabs a lampiran-cited
    code from the tail window (the shorter footer could otherwise let the 400-char window reach
    the lampiran list). Legacy-footer PDFs still recognised.

3 · New / updated tests

File Added
klasifikasi-berakhirnya-suggest.test.ts BSBHP-03/04/05/06 each land their lane for any HIGH-confidence document-shaped GPU label outside the allow-list (DOMISILI/NERACA/BANK/BA-RUPS/LAPORAN_KEUANGAN) + autoAcceptable; HIGH-confidence ID-card labels keep the no-OCR fast path; LOW-confidence ID-card falls through to the probe
supporting-doc-classifier.test.ts new "Kode Formulir:" footer stamp; title-damaged BSBHP-03 disambiguation vs lampiran codes; legacy-footer backward-compat

4 · Regressions fixed (branch-related, surfaced by the full suite)

  • /api/bsbhp-form security policy — the mount (added earlier on the branch) had no
    ROUTE_POLICIES entry → exhaustiveness test failed. Added as AUTHED.
  • aktaPtName OCR-outage 503be0eb411 awaited ocrText() for AKTA_* labels outside
    any try/catch, so an OCR outage on an akta upload returned 503 instead of the decided label.
    Now soft-fails to null and returns 200.

5 · e2e (Playwright, real UI → real backend → real GPU)

Existing .pw-scratch scripts were run; a scanned-BSBHP case was added (rasterises the
generator's filled form to an image-only PDF to simulate print→scan).

Transaction Result Notes
sample-1 6/6 ✅ free korans land as correct _149/_152 chips (suggest-only by design)
sample-2 5/6 1 free koran → coarse BUKTI_PENGUMUMAN (axis tie → operator assigns) — pre-existing
sample-3 4/6 an akta the GPU calls AKTA_BERAKHIRNYA; an English koran the GPU calls BA-RUPS — pre-existing GPU/keyword limits
sample-4 5/5 ✅ incl. the dual-function BAR = LAPORAN_LIKUIDASI
sample-5 6/6 ✅
synthetic BSBHP (4 forms + akta + BA RUPS) 6/6 AUTO ✅ template path through the UI
scanned BSBHP-05/06 (image-only) 2/2 AUTO ✅, chip=none bsbhp-06 scan drew conf 0.8937 (HIGH) and still auto-landed — the exact bug scenario
tambah-dokumen (post-board add) koran chip after board render

No BSBHP form landed in a wrong box; every BSBHP form (8/8 across synthetic + scanned)
auto-adopted its correct lane.
The sample free-document misses are proven not regressions:
matchBsbhpFormCode returns null for every sample XX file (they carry no printed code), so
refineSupportingLabel falls through to byte-identical behaviour — the misses are the documented
best-effort free-koran / coarse-GPU limitations (TAHAP0 baseline), untouched by this change.
Screenshots: .pw-scratch/{sample-1..5,bsbhp-synthetic,bsbhp-scan,tambah-dokumen}.png.

6 · Test suites — final

Suite Result
backend bunx tsc --noEmit 0 errors
backend bun run test 2620 pass · 1 skip · 2 fail
frontend bunx tsc --noEmit 0 errors
frontend vitest run 992 pass (140 files)
Playwright e2e BSBHP forms 8/8 AUTO; no wrong-box landing

The 2 backend failures are pre-existing and unrelated to this change, proven at the
refactor/tidyup-foundation baseline (06c85777):

  1. config gateway defaults > gpuServerDirectUrl falls back … when GPU_SERVER_DIRECT_URL is unsetfails identically at baseline; the dev .env sets GPU_SERVER_DIRECT_URL,
    so the "when unset" assertion can't hold. Pure test-vs-env; config.ts untouched by this
    work.
  2. processPendirianPpViaCustomModel — BO persistencepasses standalone (9/9) at baseline
    and at HEAD
    ; only fails under full-suite shared-DB test-ordering contention. PP flow,
    untouched by this work.

Commits (this session)

fix(berakhirnya): check BSBHP form code first for ALL labels in refineSupportingLabel
test(berakhirnya): BSBHP form code wins for any high-confidence GPU label
chore(berakhirnya): descriptive BSBHP template filenames + formal footer
fix(berakhirnya): lampiran-proof "Kode Formulir:" footer anchor
chore(berakhirnya): register /api/bsbhp-form security policy
fix(berakhirnya): keep no-OCR fast path for confident ID cards in form-code probe
fix(berakhirnya): soft-fail akta PT-name read on OCR outage