PR: fix/berakhirnya-status-badan-hukum → refactor/tidyup-foundation
Review brief for an AI coding agent. Scope = the 22 non-merge commits unique to this
branch on top ofrefactor/tidyup-foundation. 25 files, +1291 / −36. No commits
outside the Berakhirnya (BSBHP) flow are touched.
1. What this branch is
Berakhirnya Status Badan Hukum is the SABH flow that records a PT's legal-entity
termination after liquidation. The operator (notaris) uploads a paket: the akta plus
supporting documents — the liquidator's final report, the applicant letter, and the
newspaper (koran) announcements required by UUPT Pasal 149 (creditor call) and
Pasal 152 (final liquidation result).
Two hard problems drove this branch:
- The coarse GPU classifier has no Berakhirnya-supporting classes. It lands
confidently wrong on these docs (a koran clipping →OTHER/SURAT_KETERANGAN_BANK
at ≥0.95; a laporan likuidasi →BERITA_ACARA_RUPS@0.9996; a surat permohonan →
AKTA_BERAKHIRNYA). Corrective tiers were gated on low confidence, so the safety
net never fired. - Koran is the weakest OCR surface — a graphic ad box on a full newspaper page,
read by PaddleOCR. Deciding whether a clipping satisfies Ps.149 vs Ps.152, and
recovering its announcement number, is genuinely hard.
The branch answers both with a hybrid direction (design: TEMPLATE-BSBHP-design.md,
2026-07-16): four standard BSBHP forms become the primary, deterministic path for
operator-authored documents, and the free-format classifier is hardened as the
fallback for documents that don't use the forms.
2. Architecture at a glance
routes/klasifikasi.ts} C -->|AKTA_* label| A[extractPtNameFromTitle
→ namaPerseroan] A -.threads PT name.-> C C -->|non-AKTA label| R[refineSupportingLabel
supporting-doc-classifier.ts] R --> F{matchBsbhpFormCode
FORMULIR SABH BSBHP-0N?} F -->|yes, conf 0.98| L1[Auto-adopt correct lane] F -->|no| K[suggestByKeyword
head-anchored shape rules] K --> H{head = laporan/permohonan?} H -->|yes| L2[LAPORAN_LIKUIDASI / SURAT_PERMOHONAN] H -->|no, koran| X[matchPengumumanAxis
Ps.149 vs Ps.152 suggestion] X --> L3[BUKTI_PENGUMUMAN _149 / _152 chip
suggest-only] subgraph Templates T[GET /api/bsbhp-form/:code/blank → DOCX] --> U G[POST /api/bsbhp-form/:code/generate → PDF/DOCX] end subgraph Extraction L3 --> E[runBuktiPengumumanPass
per-pasal prompt + nomor recovery] end subgraph Validation V1[BSBHP_PENGUMUMAN_ORDER — Ps.152 3 upper bound] V2[BSBHP_REQUIRED_DOCS — dual-function hint] end
Golden rule to check against every change: the BSBHP recognition path is gated on the
printed BSBHP-0N code (which cannot appear in any other document), and the koran
pasal-axis fires only inside the BUKTI_PENGUMUMAN rule which is suggestion-only.
Nothing here should change behavior for any other flow (pendirian / perubahan / akuisisi /
merger / peleburan / pembubaran). Several commits add explicit regression guards for that.
3. The changes, grouped
A. BSBHP standard-form template path — the new primary route
(commits 9a986f89, f3725ec8, 90ee6ea7, c273ede2, 5e160447, 5c0aeae3)
- Four DOCX templates built by
backend/scripts/build-bsbhp-templates.ts: bsbhp-surat-permohonan.docx(BSBHP-03)bsbhp-laporan-akhir-likuidasi.docx(BSBHP-04)bsbhp-bukti-pengumuman-pasal-149.docx(BSBHP-05)bsbhp-bukti-pengumuman-pasal-152.docx(BSBHP-06)
The koran cover sheets (05/06) are the key idea: they turn the hardest OCR case into an
applicant declaration on a clean form instead of a scanned newspaper ad.
- services/bsbhp-form-generator.ts + routes/bsbhp-form.ts — GET /:code/blank
(defaults to DOCX so the notaris can fill it; ?format=pdf for a printable
handwrite copy) and POST /:code/generate (defaults PDF; ?format=docx to correct
extracted values). Mirrors sp-pendirian-pp-generator (docxtemplater + libreoffice),
stateless (no model in v1).
- Recognition — matchBsbhpFormCode maps the constant title line
FORMULIR SABH BSBHP-0N … (with a footer fallback Kode Formulir: BSBHP-0N for
OCR that eats the head) straight to the lane at conf 0.98, autoAcceptable.
- c273ede2 lampiran-proofs the footer: BSBHP-03/04 cite other forms
(BSBHP-04/05/06) in their lampiran list, so a title-damaged scan could grab the
wrong code from the tail window. A higher-priority probe anchored on the distinctive
Kode Formulir: stamp (which citations never carry) fixes it; the generic tail probe
stays for legacy-footer PDFs.
- 5e160447 moves the form-code probe to the very top of refineSupportingLabel,
unconditional for every non-AKTA label at any confidence — a printed+scanned
BSBHP-05 whose neraca table drew a HIGH-confidence BUKTI_TRANSFER-NERACA was hitting
the confidence >= min early-return before the probe ran (repro 2026-07-23).
- FE: an "Unduh template" link on empty berakhirnya lanes only (gated on labelType).
- Roundtrip proven end-to-end via src/scripts/bsbhp-roundtrip-diag.ts: all four forms
generate → PaddleOCR → recognise → auto-adopt onto the right lane (4/4).
Review focus: confirm the BSBHP code regex cannot match on a non-BSBHP doc; confirm the
Kode Formulir: vs lampiran-citation disambiguation; confirm no extra OCR read is added on
the happy path (recognition reuses the branch's existing OCR text).
B. Supporting-doc classifier hardening (free-format fallback)
(commits 41eab6d4, 7d0fa876, f76e4f67, e0f0271f)
41eab6d4—refineSupportingLabelre-examines by document shape (head-anchored
keyword) forCONFIDENTLY_WRONG_SUPPORTING_LABELS(OTHER/SURAT_KETERANGAN_BANK)
regardless of confidence; adopts onlyautoAcceptablehits. A GPUBERITA_ACARA_RUPS
flips toLAPORAN_LIKUIDASIonly on a head-anchoredautoAcceptablelaporan read —
a genuine BA RUPS / keputusan sirkuler is untouched (regression-guarded, because BA RUPS
handling is shared with every other flow).7d0fa876/f76e4f67— head-anchored laporan/permohonan detection before the
shared koran rule. The koran rule fired on any doc with "PENGUMUMAN" + a liquidation
keyword, so a laporan/permohonan that recites its newspaper announcement in the body
was mis-suggested as a clipping. A clipping leads with a masthead, never with these
titles → checking the head first leaves clippings untouched (10/10). Permohonan also
matches on letter structure (Perihal + Badan Hukum + Menteri/Ditjen AHU) to survive
OCR mangling ("Permohonan"→"Pemmohonan").e0f0271f— re-scoped fallback trim (Tahap 1/3/4): adds a legacy setor-pernyataan
label to the confidently-wrong set; the FE chip lane guard suppresses a suggestion
whose lane isn't in the active flow's label set (a bareBUKTI_PENGUMUMANis real in
pembubaran/peleburan but not berakhirnya — applying it there sent the file to "Tidak
Dikenali"); permohonan head rule widened for the real title "Permohonan Pemberitahuan
Berakhirnya Status Badan Hukum".
C. Koran pasal-axis (Ps.149 vs Ps.152) suggestion
(commits 3cf52554, c1a39d18, be0eb411, 6c00e7d2)
3cf52554— sample-calibrated (30-sample baseline, 2026-07-15): majority-of-hits
over distinctive terms, not first-match._152= HASIL AKHIR|HASIL
LIKUIDASI|PERTANGGUNGJAWABAN|PEMBERESAN;_149= KREDITOR|TAGIHAN|KEBERATAN|PAILIT|
KURATOR. Generic PEMBUBARAN/PUTUSAN dropped (they recite in both pasals).be0eb411— PT-name anchoring (pendirian pattern): the classify route returns
the kepala-akta PT name forAKTA_*labels and accepts?namaPerseroan, scanning the
pasal-axis within the applicant's own box so a stray term from a neighbouring
newsprint box can't flip the axis. FE accumulates the deed's PT name and threads it to
subsequent clippings. Best-effort; no name → whole-page (no regression).6c00e7d2— rebuiltmatchPengumumanAxisfrom the runtime PaddleOCR matrix (30
verification PDFs), not the text-layer hypothesis:- Tier 1: literal
Pasal 149/Pasal 152citation wins by majority — most reliable
signal in a scanned koran; resolves 7/10 alone, zero wrong. - Tier 2: recalibrated distinctive-keyword majority (drop HASIL LIKUIDASI from _152 —
it's the Ps.149 "sisa hasil likuidasi" call; add RENCANA PEMBAGIAN to _149). - Signal-bearing tie →
_152default; total absence of signal →undefined. (Note:
e0f0271flater changed tie →undefinedon ANY tie, since the BSBHP-05/06 cover
sheet is now the primary koran path.) → anchored 10/10.
Review focus: the tie-break rule shifted across 6c00e7d2 → e0f0271f. Confirm the
final behavior is tie → undefined and that this is consistent with the tests. Suggest-only
throughout — never auto-accepted; the board operator decides.
D. Bukti-pengumuman extractor
(commits b9172ac3, 4d1ade0a)
- Parametrize the clipping prompt by UUPT pasal (127 peleburan default / 149 pembubaran
/ 152 hasil akhir);runBuktiPengumumanPassderives the pasal from the operator-assigned
SubmissionDocument.classifiedTypefine label. - Wire
recoverDocNumberViaLlmas thenomor_pengumumanfallback (PaddleOCR's "No.AL"
line-split loss on newsprint), gated to Ps.149/152 — peleburan (127) keeps nomor
null, so its behavior is byte-identical (no extra call). Verify this gate: peleburan
must not pay for a berakhirnya-only feature.
E. Validation rules
(commits c78e89b3, 1e04bbe8, 82a61e11)
c78e89b3BSBHP_PENGUMUMAN_ORDERenforces Ps.152(3) upper bound
(t152 ≤ tRUPS + 30d) server-side (SABH only did it client-side via a datepicker
maxDate). Stays WARNING-only (severityFloor) — koran is the weakest surface and
the 149/152 axis is operator-assigned. Item 1 (BSBHP_AKTA_RECENCY) was intentionally
not duplicated — already covered by the wrappedAKTA_DATE_WINDOWrule at
windowDays=30.82a61e11BSBHP_REQUIRED_DOCS— message-only: whenLAPORAN_LIKUIDASI(or
BERITA_ACARA_RUPS) is missing while its counterpart is present, the FAIL now hints one
document may satisfy both slots (a Keputusan Sirkuler RUPS memuat pertanggungjawaban
likuidator is also the Laporan Likuidasi). The actual "one file, two slots" handling is a
reviewer decision and is NOT implemented — validation logic unchanged.
F. Robustness & security follow-ups
(commits 13973309, f9ad4e90, db5af2d5)
13973309— register/api/bsbhp-forminROUTE_POLICIESasAUTHED(the new
mount failed the exhaustiveness test).f9ad4e90— the unconditional form-code probe (B/A) added an OCR read for every
non-AKTA label, breaking the no-OCR fast path for high-confidence KTP/NPWP. Skip the
probe for a HIGH-confidenceCONFIDENT_ID_CARD_LABELShit (a full-page BSBHP form can
never be confidently classified as a compact ID card). Document-shaped and low-confidence
labels are still probed.db5af2d5—be0eb411addedextractPtNameFromTitle(await ocrText())outside any
try/catch, so a PaddleOCR outage on an akta upload threw past the handler → 503 instead
of the decided label. Wrap it: degradeaktaPtNameto null and still return 200.
4. Files touched
| Area | Files |
|---|---|
| Templates | backend/scripts/build-bsbhp-templates.ts, backend/templates/bsbhp-*.docx (4) |
| Form generator/route | backend/src/services/bsbhp-form-generator.ts, backend/src/routes/bsbhp-form.ts, backend/src/scripts/bsbhp-roundtrip-diag.ts, backend/src/index.ts, backend/src/security/route-policies.ts |
| Classifier | backend/src/routes/klasifikasi.ts, backend/src/services/supporting-doc-classifier.ts (+ tests) |
| Extractor | backend/src/services/bukti-pengumuman-extract.ts (+ tests) |
| Rules | backend/src/flow-engine/rules/berakhirnya/index.ts (+ tests) |
| Frontend | KlasifikasiPage.tsx, klasifikasi/classify-button.tsx, file-card.tsx, kelengkapan-checklist.tsx (+ tests) |
5. Review checklist for the agent
- [ ] No cross-flow regression. Trace every branch in
refineSupportingLabel/
matchPengumumanAxis/runBuktiPengumumanPassand confirm non-berakhirnya inputs
keep byte-identical behavior. The claim is that BSBHP recognition is gated on a code
that can't appear elsewhere, the koran axis is suggestion-only, and the peleburan
(127) nomor path adds no extra call. Verify each. - [ ] Tie-break consistency. The koran pasal-axis tie rule changed
_152→undefined
across commits C→B. Confirm the shipped code and tests agree. - [ ] BSBHP code disambiguation. Can
matchBsbhpFormCodegrab a cited form code from
a lampiran list instead of the document's own code? Confirm theKode Formulir:
anchor + priority ordering closes this on title-damaged scans. - [ ] No-OCR fast path. Confirm high-confidence KTP/NPWP still skip OCR after the
unconditional form-code probe was added. - [ ] Outage handling. Confirm every best-effort OCR read (esp. the akta PT-name) is
inside a try/catch and degrades to a decided label + 200, never 503. - [ ] Security policy exhaustiveness.
/api/bsbhp-formisAUTHEDand matches sibling
document routes. - [ ] Tests. ~5 test files touched (rules, klasifikasi-suggest, supporting-doc,
bukti-pengumuman, classification-board, kelengkapan-checklist). Confirm they cover the
confidently-wrong-label overrides, BA-RUPS-untouched guards, and the axis fixtures.
6. How to run
# Backend typecheck + tests
cd backend && node node_modules/.bin/tsc --noEmit
cd backend && bun test src/services/__tests__/supporting-doc-classifier.test.ts \
src/services/__tests__/bukti-pengumuman-extract.test.ts \
src/flow-engine/rules/berakhirnya/__tests__/rules-berakhirnya.test.ts \
src/routes/__tests__/klasifikasi-berakhirnya-suggest.test.ts
# Frontend typecheck + tests
cd frontend && node node_modules/.bin/tsc --noEmit
cd frontend && bun test src/components/klasifikasi
# Regenerate templates / prove the roundtrip
cd backend && bun run scripts/build-bsbhp-templates.ts
cd backend && bun run src/scripts/bsbhp-roundtrip-diag.ts # expects 4/4 lanes