think
16px
820px

PENDIRIAN_PT smoke wave 2 — implementation report

Worktree: /home/efran/remote-development/poc-ahu-ai/ahu-ocr-tidyup, branch refactor/tidyup-foundation.

Status: DONE

Commits (one per item, oldest → newest)

  • c096b66 fix(pt-pendirian): ground per-entry contact-info confidence instead of literal 0 (Item A)
  • 3837a34 fix(pt-pendirian): ground singkatan_nama against raw OCR text (Item B)
  • 9a439f3 fix(pt-pendirian): trim Item B's added comments back under field-transformer.ts's grandfathered cap (housekeeping, fitness gate)
  • 2b2a273 fix(pt-pendirian): clickable roster cells focus their bbox in the PDF viewer (Item C)
  • 5c0f95a fix(pt-pendirian): prune all-null bukti_setor columns from the roster schema (Item D)
  • 5a83537 fix(pt-pendirian): group + owner-map pdfSources ordering (Item E)
  • 2fea9ac feat(review-engine): groupUnconfirmedBySection helper for the PT footer chip summary (Item F prep)
  • 9296ceb fix(pt-pendirian): finalize-footer grouped chip summary replaces the flat unconfirmed dump (Item F)
  • c4f29a9 fix(pt-pendirian): pull pdfSources ordering into pt-pdf-sources.ts, trim comments to clear the file-size fitness cap (housekeeping, fitness gate)

No pushes were made. Legacy pages/endpoints untouched. contract/review.ts was not modified — cellFieldRefs already existed in the contract as the brief noted.

Per-item summary

Item A — grounded per-entry contact-info confidence. azure-on-prem-contact-info.ts now exports groundedEntryConfidence(rawConf, key, value, rawText): when Azure DI's custom-template model returns a literal 0 (or omits) confidence for a contact_info.N.* table cell, the value is checked against the raw OCR text (case/whitespace/punctuation-insensitive, reusing small-card-cleanup.ts's isGroundedValue) and scored 95 on a verbatim match instead of persisting the misleading 0. Entity-level fields (entity_name/entity_phone/entity_email) were already fine (84-99) per the coordinator's diagnosis and are untouched. Forward-looking only — rows already persisted with confidence 0 keep that value until Efran re-uploads/re-extracts the document. Checked FieldRow/ConfidenceTooltip: a genuine confidence-0 field already renders "0% Rendah" (red), not "Belum ada skor OCR" (that string only fires on a null/undefined confidence) — no frontend display change was needed or made.

Item B — grounded singkatan_nama. transformAktaResult gained an additive optional 4th param rawText?: string. When provided (both Azure on-prem call sites in document-processor.tsprocessAktaViaAzureOnPrem and processAktaPeralihanPpViaAzureOnPrem — now thread ocrResult.rawText through), an extracted singkatan_nama that doesn't appear verbatim (case-insensitive, trimmed) in that text is nulled with confidence reset to 0. The two GPU-server call sites (processAktaViaGpuServer, processAktaPeralihanPpViaGpuServer) have no local OCR text and simply don't pass the param — unchanged behavior, and every pre-existing caller/test that omits it is unaffected. Scope is singkatan_nama only per the brief. Other free-text fields worth a future grounding pass: nama_notaris, kedudukan_notaris, jenis_perseroan, status_perseroan — flagged, not touched.

Item C — clickable roster cells → bbox focus. Projector: pengurus (direksi/dewan_komisaris)/pemegang_saham/kbli rows fill RosterRowVM.cellFieldRefs with ${aktaDocId}:${bboxKey}, keyed by each row's own orderIndex in its SOURCE table (verified against staging submission 2aff6383…: pemegang_saham.0 has a nama bbox but no persentase bbox, confirming the existence check has to be per-cell). A ref is only emitted when the matching BoundingBox row actually exists. RosterSection.tsx: a cell whose column key is in cellFieldRefs renders cursor-pointer + hover affordance and, on click, splits the ref on the first colon (fieldKey is dot-notation, never contains one) and calls actions.focusField(documentId, fieldKey) — never gated by readOnly (mirrors FieldRow's own always-live focus-on-click). ReviewActions.focusField gained an additive optional 2nd param for this direct (documentId, fieldKey) form; the existing single-arg focusField(ref) form is unchanged. Only PtPendirianReviewPageV2 implements the 2-arg branch (other pages' rosters never populate cellFieldRefs). Verified the PDF viewer re-derives its active document from activeDocumentId on every render — no extra wiring needed for "switch documents on click."

Item D — prune all-null bukti_setor columns. filename/jenis always survive; every other column is dropped from the roster's columns schema when it is null across every row of the submission. A column present in even one row's cells stays for the whole roster (schema stays stable, cells never shift between rows).

Item E — grouped + owner-mapped pdfSources. New pure unit backend/src/review/pt-pdf-sources.ts (buildOwnerNameByDocId, orderPdfSourceDocs) orders AKTA first, then BUKTI_SETOR/BUKTI_TRANSFER-* docs, DOMISILI, DATA_KONTAK, then per-person KTP+NPWP pairs (identityMatches/people order, KTP before NPWP); a safety net appends anything left over so no submission document is ever dropped. An identity doc the identitas section actually linked to a person (via IdentityMatch for KTP, the existing fuzzy-name-prefix match for NPWP — there is no npwpDocumentId column on IdentityMatch, confirmed in prisma/schema.prisma, so the existing resolver logic is reused rather than invented) gets an owner-suffixed label ("KTP — Budi Santoso"); an unmatched identity doc keeps its plain classifiedType label. Extracted to its own file (not inlined) specifically to keep pt-pendirian-review.ts under the file-size fitness cap (see below).

Item F — finalize-footer grouped chip summary. submit-gate.ts gained groupUnconfirmedBySection(data), grouping every unconfirmed field/roster-row by the SectionSchema it belongs to (field-list fieldRefs, identity-section people's KTP/NPWP fieldRefs + phone contact ref, or roster rosterKey), ordered per data.sections. PtPendirianReviewPageV2.tsx's footer now shows a single headline ("Belum dikonfirmasi: N item di M bagian") plus per-section chips ("Data Perseroan · 1", "Pengurus · 2", …), each clickable via the existing scrollToSectionAndExpand helper. The "Aturan validasi gagal" alert is untouched (already one rule per <li>).

Fitness-gate note (file-size ratchet)

pt-pendirian-review.ts is not in the grandfathered-monster list (cap 800 lines) and grew past it once Items C/D/E landed (804→845 lines across the work). Fixed by extracting Item E's pure ordering/labeling logic into pt-pdf-sources.ts and condensing a few Item C/D/E comment blocks without losing their content. Final size: 788 lines. fitness: no new monster files is green.

Gates

  • Backend ./node_modules/.bin/tsc --noEmit: clean.
  • Backend full suite (TEST_DATABASE_URL=…sandbox_test bun run test): all of this task's own test files are 100% green every run (azure-on-prem-contact-info.test.ts, field-transformer.test.ts, pt-pendirian-review.test.ts, file-size.test.ts). The full-suite pass/fail count varied across 3 consecutive runs (2096/7, 2093/10, 2082/21 — baseline was 2092/1) with a different set of failing tests each time, none of which touch any file this task changed (PP/pembubaran/perubahan/peleburan routes, RUPS-tahunan/peleburan context builders, etc.). Spot-checked several of the "failing" tests (pp-perubahan-jenis.test.ts, rups-tahunan-context.test.ts) in isolation — 100% green alone. This is pre-existing test-isolation/ordering flakiness in the shared integration suite (a large number of route tests share one live Postgres test DB with no per-file isolation), not a regression introduced here.
  • Frontend tsc --noEmit: clean.
  • Frontend full suite (npx vitest run): 834/834 tests pass across 115/115 files (baseline 827/115) — zero failures, zero flakiness observed.

Concerns

  • The full backend suite's pre-existing flakiness (see above) means the literal "2092/1 baseline, zero regressions" gate can't be verified as a single clean number the way the frontend one can — I'm reporting per-file isolation results plus 3 repeated full-suite runs as the strongest evidence available that nothing in this diff regressed anything.
  • Item A's fix is forward-looking only (documented in the brief and above) — existing 0-confidence contact rows need a re-upload to pick up the new grounding.
  • Item B flagged (not changed) other hallucination-prone free-text fields for a future wave, per the brief's scope note.