PENDIRIAN_PT tenant — T2 report: engine renderers (identity kind + atomic confirmMode)
Status: DONE
Commits (worktree ahu-ocr-tidyup, branch refactor/tidyup-foundation):
- c78e9b7 feat(review-engine): atomic confirmMode for field-list sections (PT T2 D1)
- b10f975 feat(review-engine): IdentitySection — engine DEFAULT renderer for kind identity (PT T2 D2)
Test summary: frontend npx vitest run 780 passing / 113 files (baseline 761/112 + 19 new: 6 atomic-mode + 1 section-progress + 11 IdentitySection + 1 ReviewEngine registration). tsc --noEmit clean. Backend suite not run (no backend change, per brief).
What changed
Deliverable 1 — atomic confirmMode in FieldListSection
types.ts:ReviewActionsgains optionalconfirmAtomic?(sectionKey, documentId, confirmed): void.FieldListSection.tsx: whensection.confirmMode === "atomic"it renders ONE section-level toggle button ("Konfirmasi Modal & Saham" / "Terkonfirmasi") above the field rows, invokingactions.confirmAtomic(section.key, section.documentId, next). State derives live fromsection.fieldRefs.every(ref => fields[ref]?.confirmed)(all, not just one, per brief's "for safety" instruction). No per-field confirm buttons render in this mode; the Edit affordance is untouched. Disabled whenreadOnly. Per-field mode (absent/"per-field") is byte-identical — verified by the full pre-existing FieldListSection suite passing unchanged.- Extracted
FieldRow.tsxout ofFieldListSection.tsx's per-ref JSX (identical markup/testids/behavior, parameterized byfield/manualConfirmAll/actions/readOnly/suppressConfirm) so Deliverable 2 could reuse it instead of copy-pasting ~100 lines. section-progress.tsneeded no change — added a unit test insection-progress.test.tsproving an atomic field-list's progress counts identically to per-field mode (the projector mirrors one DB row onto every ref, so counting each ref lands on the right total either way).
Deliverable 2 — IdentitySection (new default renderer for kind "identity")
- New
IdentitySection.tsx: one base-novaCardperIdentityPersonVM— header = personName + role badges + a matchStatus badge (AUTO/MANUAL→ emerald "matched",AMBIGUOUS→ amber,UNMATCHED→ red-ish). Body = labeled "KTP" block (always rendered: field rows via the sharedFieldRow, or a muted "Tidak ada dokumen" marker whendocs.ktpis null), labeled "NPWP" block (rendered only whendocs.npwpis present), and a "Kontak" row forcontact.phoneFieldRefwhen non-null. Rows reuseFieldRowso confirm/edit/focus behave identically to every other field-list row. Per-person done-glow (CARD_DONE_GLOWring) once every fieldRef the card can show is confirmed (empty person → never glows). - Registered in
ReviewEngine.tsx'sDEFAULT_RENDERERS["identity"]. Not added toSELF_CHROMED_KINDS— content-only, standard shell (h2/border/badge) still wraps it, confirmed via a newReviewEngine.test.tsxcase.
Self-review notes / concerns
- matchStatus → color mapping is an interpretive call. The contract enum (
contract/enums.gen.ts) isAUTO | AMBIGUOUS | MANUAL | UNMATCHED— the brief's own language used a different vocabulary ("matched/full → emerald, partial → amber, none/missing → red-ish") and the survey only documented AUTO (green) / AMBIGUOUS (amber) explicitly, calling everything else "neutral gray". I mappedMANUALto emerald alongsideAUTO(both mean "a person IS matched", just auto vs. human-picked) andUNMATCHEDto red (survey's implicit "none"). Worth a product-owner glance since it diverges from the survey's literal "else → gray" default. - NPWP-block-hidden-when-null is also an inference, not explicit in the brief (which said "for docs.ktp and docs.npwp (when present)" then only spelled out the absent-doc marker for
docs.ktp). I treated KTP as always-rendered-with-a-marker and NPWP as rendered-only-if-present, matching the survey's note that legacy NPWP linkage is optional/best-effort while KTP is not. Flagging in case the intended behavior was an explicit "Tidak ada dokumen" marker for NPWP too. section-progress.tsstill returns{total:0, confirmed:0, done:false}for kind"identity"(unmeasured, same as"diff"/"document-set"today) — this is unchanged/out-of-scope per the brief, but it means the outer shell's confirmed/total badge and emerald done-border never activate for an identity section; only the per-person Card's own ring glows. Not a regression, just noting the visible asymmetry vs. field-list/roster sections.- Neither deliverable wires into any actual page/registry — this is the frontend engine-renderer layer only, per the brief's explicit "Do NOT touch pages, backend, contract" constraint.
No blockers encountered. Did not push (per instructions).