think
16px
820px

PENDIRIAN_PT tenant — T1 (backend projector) report

Status: DONE_WITH_CONCERNS

Commits (worktree ahu-ocr-tidyup, branch refactor/tidyup-foundation)

  1. 42779d1refactor(review): extract normalizeConfidence/PROVENANCE_MAP/reconcileExtractionStatus to projector-utils
    Mechanical extraction of the duplicated helpers out of pp-pendirian-review.ts / pp-pembubaran-review.ts into backend/src/review/projector-utils.ts (+ a small direct unit test file). No behavior change — existing PP/Pembubaran suites stayed green throughout.
  2. c31688ffeat(review): PENDIRIAN_PT ReviewData projector (tenant #3) + registry entry
    New backend/src/review/pt-shape.ts (field-key inventory + labels, ported from the legacy page's own countUnconfirmedInSection switch), new backend/src/review/projectors/pt-pendirian-review.ts, its test file, and the REVIEW_REGISTRY.PENDIRIAN_PT registration.

Test summary

Full backend suite: 2076 pass / 1 skip / 0 fail (baseline was 2067 pass/1 skip + this tenant's 10 new tests = 2077 total, matches exactly). tsc --noEmit clean. Strict TDD followed throughout (each new test file was watched failing — missing module — before the corresponding implementation was written).

New tests:
- backend/src/review/__tests__/projector-utils.test.ts — 5 tests for the extracted helpers.
- backend/src/review/__tests__/pt-pendirian-review.test.ts — 5 tests: a full-seed projection test (section order/kinds/confirmMode, atomic modal fields+bbox, merged roster rows incl. a WNA komisaris row and a pemegangSaham↔buktiSetor cross-ref, an orphan bukti-setor row, KTP overlay of an edited alamat.jalan proving USER_EDITED provenance, identity person shape, domisili/kontak sections, STRICT ReviewDataSchema.parse), a null-for-unknown-id test, a null-for-wrong-type test, a reconcileExtractionStatus regression test, and an e2e GET /api/submissions/:id/review 200+parse test via the real route module.

Design decisions made (not fully pinned down by the brief — flagging for review)

  1. Section granularity: kept 3 separate field-list sections (data_perseroan/akta_notaris/notaris_pengganti) rather than merging into one "akta" section, matching the PP-pendirian precedent (one SectionSchema per legacy card) and the brief's bullet-5 instruction to "respect the survey's inventory."
  2. pengurus_pemegang_saham is ONE merged roster (direksi + komisaris + pemegangSaham rows in a single table, detail.role tags the type). This is the most literal reading of the brief's roster-key list (pengurus_pemegang_saham + kbli + bukti_setor, not 5 separate roster keys), but it's a real design call — RosterVM has one shared columns schema, so all type-specific data (WNA flag, saham %, bukti-setor cross-ref, kehadiranRups, …) lives in row.detail. Task 2 (frontend) will need to decide how to render/group this — worth a second look before committing to it long-term.
  3. modal_saham fields are keyed directly off the AktaModal row's 6 scalar columns (fieldKey = the same modal.* strings the legacy page uses for its handleFieldFocus bbox lookups). Verified (not assumed) via field-transformer.ts/document-processor.ts: these are BoundingBox keys only — there is no per-sub-field ExtractedField row, so there's no overlay concern here (unlike KTP/NPWP). The whole row is one atomic entity; confirmMode: "atomic" reflects that literally.
  4. kontak section scope narrowed: only telepon/email perseroan (contactInfo-linked, entity_phone/entity_email) + PMA currency/kurs. The address-line submission columns (alamatJalan/rt/rw/…) are not re-projected as separate "manual" fields — traced the legacy PtAddressForm's own wiring and found its confirm/confidence state for those cells is actually keyed off the domisili ExtractedField rows (ADDRESS_TO_DOMISILI_KEY), not an independent per-submission-column confirm state. Projecting them again would have fabricated a second, disconnected confirm state that doesn't exist in the legacy app. The domisili field-list section is the source of truth for those cells.
  5. akta.notarisPenggantiExtraction (the raw display-only JSON blob) is not ported — no contract slot exists for an ad-hoc display blob outside fields/rosters/registryEntities, and the brief's own enumeration for notaris_pengganti only asked for the ExtractedField rows (which ARE ported).
  6. Penghadap is not projected at all, per the brief's explicit instruction (confirmed dead: legacy fetches it but never renders it).
  7. aiSummary is null: confirmed the legacy GET /review-data response has no such field — /summarize is a separate, non-persisted, transient POST endpoint (not read back on GET).
  8. NPWP startsWith fuzzy-linking ported AS-IS (not the stronger tokenScore match used elsewhere), per the brief's explicit parity-first instruction.

Concerns / things worth a second look

  • Decision #2 above (the merged pengurus/pemegang-saham roster) is the single biggest interpretive call in this projector — the brief text is genuinely ambiguous between "3 separate rosters" and "1 merged roster," and I went with the literal reading. If Task 2's frontend design assumed separate rosters per entity type, this will need to change (mechanical — the roster-building code is isolated in one section of the projector).
  • kontak's telepon/email fields use a best-effort confidence/status derivation from ContactInfoExtraction.confidence's entity_phone/entity_email JSON keys; I could not find a definitive legacy bbox-key precedent for these two fields specifically (best-effort boundingBoxes lookup included but unverified against real data).
  • evidenceCheckAvailable and the exact docTypes metadata on SectionSchema entries follow the PP precedent by analogy rather than a PT-specific requirement — low risk, but not independently verified against a spec line.

Not done (explicitly out of scope per the brief)

  • Legacy endpoint/route/frontend untouched.
  • No git push.