PENDIRIAN_PT tenant — T1 (backend projector) report
Status: DONE_WITH_CONCERNS
Commits (worktree ahu-ocr-tidyup, branch refactor/tidyup-foundation)
42779d1—refactor(review): extract normalizeConfidence/PROVENANCE_MAP/reconcileExtractionStatus to projector-utils
Mechanical extraction of the duplicated helpers out ofpp-pendirian-review.ts/pp-pembubaran-review.tsintobackend/src/review/projector-utils.ts(+ a small direct unit test file). No behavior change — existing PP/Pembubaran suites stayed green throughout.c31688f—feat(review): PENDIRIAN_PT ReviewData projector (tenant #3) + registry entry
Newbackend/src/review/pt-shape.ts(field-key inventory + labels, ported from the legacy page's owncountUnconfirmedInSectionswitch), newbackend/src/review/projectors/pt-pendirian-review.ts, its test file, and theREVIEW_REGISTRY.PENDIRIAN_PTregistration.
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)
- 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 (oneSectionSchemaper legacy card) and the brief's bullet-5 instruction to "respect the survey's inventory." pengurus_pemegang_sahamis ONE merged roster (direksi + komisaris + pemegangSaham rows in a single table,detail.roletags 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 —RosterVMhas one sharedcolumnsschema, so all type-specific data (WNA flag, saham %, bukti-setor cross-ref, kehadiranRups, …) lives inrow.detail. Task 2 (frontend) will need to decide how to render/group this — worth a second look before committing to it long-term.modal_sahamfields are keyed directly off theAktaModalrow's 6 scalar columns (fieldKey = the samemodal.*strings the legacy page uses for itshandleFieldFocusbbox lookups). Verified (not assumed) viafield-transformer.ts/document-processor.ts: these are BoundingBox keys only — there is no per-sub-fieldExtractedFieldrow, so there's no overlay concern here (unlike KTP/NPWP). The whole row is one atomic entity;confirmMode: "atomic"reflects that literally.kontaksection scope narrowed: only telepon/email perseroan (contactInfo-linked,entity_phone/entity_email) + PMAcurrency/kurs. The address-line submission columns (alamatJalan/rt/rw/…) are not re-projected as separate "manual" fields — traced the legacyPtAddressForm's own wiring and found its confirm/confidence state for those cells is actually keyed off the domisiliExtractedFieldrows (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. Thedomisilifield-list section is the source of truth for those cells.akta.notarisPenggantiExtraction(the raw display-only JSON blob) is not ported — no contract slot exists for an ad-hoc display blob outsidefields/rosters/registryEntities, and the brief's own enumeration fornotaris_penggantionly asked for the ExtractedField rows (which ARE ported).- Penghadap is not projected at all, per the brief's explicit instruction (confirmed dead: legacy fetches it but never renders it).
aiSummaryisnull: confirmed the legacyGET /review-dataresponse has no such field —/summarizeis a separate, non-persisted, transient POST endpoint (not read back on GET).- NPWP
startsWithfuzzy-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 fromContactInfoExtraction.confidence'sentity_phone/entity_emailJSON keys; I could not find a definitive legacy bbox-key precedent for these two fields specifically (best-effortboundingBoxeslookup included but unverified against real data).evidenceCheckAvailableand the exactdocTypesmetadata onSectionSchemaentries 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.