think
16px
820px

Berakhirnya Status PT — Final Consolidated Fix Wave (2026-07-03)

Branch feat/pt-flows-registry. Commits: b0252fe (backend) + d93951b (frontend). Status: COMPLETE, all suites green.

C1 — registry-gate scan window anchored at the pendirian year (live-SABH false FAIL)

backend/src/flow-engine/context/berakhirnya-context.ts passed selectedTahun ?? currentYear as the
loadPembubaranRegistryState start year — but on the auto-select path selectedTahun is the pendirian
transaction's year (Phase-2 SK-exact match), and the helper scans shards downward. The approved pembubaran
row (id_aksi 12, filed years after pendirian) sat above the window, so approvedPembubaran came back
false and BSBHP_REGISTRY_STATE false-FAILed every legitimate live filing (the "sudah berakhir" guard never
fired either).

Fix: exported registryScanStartYear(selectedTahun, currentYear?) = Math.max(selectedTahun ?? 0, currentYear).

  • Rationale (in the code comment): pembubaran/berakhirnya rows are recent by construction — the filing is
    happening NOW, and its prerequisite pembubaran was approved shortly before.
  • Manual path: the operator-typed year is correct under the same recency argument — a future-dated year
    wins the max, a past one degrades to the current year. Traced: no other consumer of selectedTahun feeds
    this scan (perubahan/akuisisi/peleburan use it only for their own lookups).
  • MAX_YEAR_SCAN coverage: it is 5 → the window reaches currentYear-5. Liquidations longer than that
    degrade to a FAIL the operator overrides — acceptable, documented in the anchor comment.
  • Test seam: loadPembubaranRegistryState gained an optional injectable { available, queryShard } deps
    parameter (defaults = real SABH driver; zero behavior change).
  • Regression test (berakhirnya-context.test.ts): mocked shards put the approved row in the CURRENT-year
    shard with pendirian = currentYear-9. Old anchor → {approvedPembubaran:false} (the invisible case);
    fixed anchor → true; registryStateRule then PASSes on that snapshot.
  • Plan erratum follow-on appended to the header erratum block of
    docs/superpowers/plans/2026-07-03-berakhirnya-status-pt.md.

C2 — review-page inline components lose keystrokes

frontend/src/pages/BerakhirnyaReviewPage.tsx defined ManualEditor/ClipCard (input-bearing) plus
FieldList/ApproveButton inside the page component — new function identity every render → React
remounted the subtree → uncontrolled defaultValue inputs reset + dropped focus on every keystroke.

  • All four hoisted to module scope with explicit props (drafts/handlers); identity now stable across renders.
  • Drafts clear on successful save (setSpDraft(null) / setLalDraft(null) / per-doc clipDrafts prune in
    onSuccess) — the stale-resend nit.
  • ClipCard payload → touched-keys-only: ClipDraft is now partial; verified against
    routes/berakhirnya.ts's pengumuman handler, which accepts partial bodies via "key" in body guards —
    untouched NOT_FOUND nulls are no longer rewritten as MANUAL empty strings.
  • Keystroke-discriminator test: two sequential change events into the Surat Permohonan editor; asserts the
    first input's DOM value survives the re-render and the final PUT body carries BOTH edits. Plus a
    touched-keys-only ClipCard test. Red-checked: with the page stashed back to the inline-component
    version, both tests FAIL (input resets to ""; payload carries all three keys); on the fixed page both PASS.

I3 — stale status-3 UI hint

(3 = pembubaran tercatat) (debunked — approved rows sit at status 1 regardless of aksi, per the 2026-07-03
erratum) replaced with (prasyarat pembubaran diverifikasi pada Hasil Validasi) + a code comment.

Minor fold — api-client known keys

frontend/src/lib/api-client.ts no longer does blanket Object.assign(err, body); it copies only the
envelope keys the backend routes actually emit alongside errorcode, failing, missingSections,
status, detail (grepped across routes/*.ts). A hypothetical body.message can no longer override the
constructed message. New test covers the message-override + unknown-key-drop case; existing tests unchanged.

Verification

  • Backend: bunx tsc --noEmit clean; bun test src/flow-engine src/routes/__tests__/berakhirnya-routes.test.ts
    189 pass / 0 fail (includes the context + rules suites).
  • Frontend: bunx tsc --noEmit clean; the four review-page suites (Berakhirnya/Pembubaran/Peleburan/
    LaporanRups) + api-client + pembubaran-detect + 3 detect-banner suites → 40 pass / 0 fail
    (33 + 7 across the two vitest runs).

Concerns

  • The status envelope key shadows nothing today, but copying it onto an Error object is unconventional;
    kept because two routes emit it and a consumer may read it.
  • ApproveButton/FieldList call sites got two extra props each — slightly noisier JSX, the price of stable
    identity without inventing a context.