think
16px
820px

Apostille/Legalisasi Full Recode — Plan (2026-07-10)

Scope requested: (1) migrate Apostille/Legalisasi to the v2 flow engine + v2 review engine, (2) carry over the fast-track feature, (3) build a Spesimen signature-verification system (AI signature recognition + comparison, match % advisory to the verifikator).


0. What research found (facts the plan is built on)

0.1 Current state in this repo (ahu-ocr-tidyup)

Apostille is the last v1-fork flow. Everything else (all PT + PP) is on v2-generic:

  • flow-engine/registry.ts:36APOSTILLE: { engine: "v1-fork" } — the only non-defineFlow entry.
  • review/review-registry.ts:46APOSTILLE: null — the only unmigrated review projection (404 REVIEW_NOT_MIGRATED).
  • Bespoke pipeline: routes/apostille.ts (948 lines) + apostille-processor.ts + own validation runner (apostille-cross-validator.ts, 15 AP_* rules) + hand-built 655-line ApostilleReviewPage.tsx.
  • The landing zone already exists: contract/review.ts already defines a "document-set" SectionVM kind with signer/candidates/translation fields — purpose-built for Apostille, unused by any projector today. The Prisma enums (ApostilleCategory 13×, ApostilleDocLayout 16×) are already shared.
  • Signer "spesimen" matching today is text-only: name-token + NIP matching against the legacy registry MariaDB (v1_appapos.spesimen / spesimen_position, 132K officers), tri-state MATCHED/AMBIGUOUS/UNMATCHED persisted on SignerMatch. Zero signature-image comparison exists anywhere in the workspace.

0.2 Fast-track ("Jalur Percepatan") — important honest finding

The feature you remember is from the rebuild (ahu-rebuild-apostille), and it was never actually live anywhere:

  • Legacy ahu-apostille: zero hits for "percepatan" — the feature doesn't exist there at all.
  • Rebuild: the field exists (Permohonan.jalur_permohonan Int @default(0), 0=Reguler 1=Percepatan) and the UI copy exists, but the applicant-facing dropdown is commented out, the admin list column is hard-disabled (jalur_percepatan: false at every call site), quota check is a hardcoded false with a TODO, the Rp500k surcharge is never billed (voucher tariff ignores the field), and the verifikator worklist ordering never uses it. It's a paused feature branch.

Intended semantics (from the rebuild's UI copy, JalurPercepatan.jsx):

Fast-track = estimasi selesai 1 hari kerja, biaya Rp500.000, tersedia 08.00–12.00 WIB (kuota terbatas), khusus dokumen elektronik atau dokumen konvensional dengan pejabat yang sudah terdaftar di Pangkalan Data AHU. Apostille only (never wired for Legalisasi).

So "carry that feature here" = implement the documented intent properly, with real enforcement — not port dead code. Our system can even gate eligibility better than the rebuild planned to: we already know per-document whether the signer is registered (our SignerMatch pre-match), so the "pejabat terdaftar" requirement can be checked automatically instead of trusting the applicant.

0.3 Spesimen — reference signatures exist and are (probably) reachable

  • The registry DB we already query has spesimen_position.spesimen_file — populated for 135,881 of 135,882 officers. Values are object keys like spesimen-file/202604/<hash>.pdf.
  • Files live in AHU's MinIO (legacy service config: bucket apostille, host like *.apps.*.dc.ahu.go.id). We have DB creds but no MinIO creds yet → the one real access dependency for Phase C.
  • In the rebuild, verification of the signature is fully manual: the verifikator sees the uploaded document PDF and the specimen PDF side-by-side (two PDFViewer panes) and eyeballs it. No scoring, no overlay, nothing. That manual comparison is exactly what the AI match % will assist.

0.4 Verification flow in the real system (for UX parity)

Rebuild lifecycle: draft → dikirim → (ditangguhkan if pejabat unregistered) → verifikasi_verifikator (Tahap 1) → verifikasi_kasi (Tahap 2) → pencetakan | ditolak. Rejection reason is free text; re-verification override exists for admins. Legalisasi differs from Apostille only in: country list per service, required kanwil (print office), output name (Stiker vs Sertifikat), a Legalisasi-only applicant block list, and PNBP tariff. One model, one flow, jenis_layanan flag — which validates our current single-APOSTILLE-type design.

0.5 Signature-AI platform fit

  • Nothing exists to reuse (no siamese/embedding/cosine image code anywhere in the workspace).
  • No vision model is currently deployed — the gpu-server's vlm.py client exists but points at the text-only Qwen3.6-35B. Vision path is dead code.
  • The right template is ahu-doc-classifier: FastAPI + local-CUDA torch model, own port (classifier=5004), Redis cache keyed by content hash, audit events to ahu.ai.audit (fire-and-forget, env-gated), heartbeat to the manager, dormant-until-env-flip. A new in-process GPU tenant is cheaper and better-fitting than reviving a 7B+ VLM for a bbox-and-embedding task.
  • Platform rules that bind us (CONVENTIONS.md v1.0): new stable tenant slug, deterministic idempotency keys, audit schema v1, on-prem weights only, all engine touch points env-gated (unset = byte-identical behavior).

1. Phases

flowchart LR A[Phase A\nv2 migration] --> B[Phase B\nFast-track] A --> C2[Phase C2-C5\nSignature AI build] C1[Phase C1\nMinIO access + discovery] -.parallel, needs Efran.-> C2 B --> D[Phase D\nPolish + docs + deploy] C2 --> D

Phase A — Migrate Apostille/Legalisasi to v2 flow engine + v2 review engine

The recode proper. Target: APOSTILLE becomes a normal v2-generic flow; the bespoke pipeline dies.

  • A1. Engine extension — interactive classification phase. Apostille is the only flow with a human-in-the-middle classification board (AWAITING_CLASSIFICATION + drag-drop reclassify + "start extraction" gate). v2 runFlow is single-shot today. Add an optional two-phase capability to FlowConfig (e.g. classification?: { classify(ctx), gate(ctx) } — dispatch runs classify → pauses at AWAITING_CLASSIFICATION; the existing /start route resumes runFlow in extract phase). Built generically so future multi-doc flows can use it.
  • A2. Flow definition + rules port. defineFlow for APOSTILLE: context builder (applicant + document records + extracted fields + signer matches + destination country/service), port the 15 AP_* rules to Rule<Ctx>[] (mechanical — the RuleResult shapes are already near-identical), validationScope for delete-stale, keep the classifier seam (APOSTILLE_CLASSIFIER=llm|model) intact.
  • A3. Extraction via the engine. Register the generic OCR-assist extractor (apostille-generic-extract.ts — ported as-is; its grounding guards are high-value) as the extraction pass for apostille docs; wire through ExtractionPlan instead of hand-branching in apostille-processor.ts. Signer resolve runs as a post-extract step per document (same as today, new home).
  • A4. Review projector. First real consumer of the "document-set" SectionVM: identity section (KTP), one document-set section (items = per-document card with category/layout/fields/signer VM/translation axis), validation results, section approvals. REVIEW_REGISTRY.APOSTILLE goes live; the generic GET /review serves it.
  • A5. Frontend on the review engine. Replace ApostilleReviewPage.tsx with a thin shell around <ReviewEngine> (PP pages are the template): new generic DocumentSetSection renderer in components/review-engine/ (reusing the internals of today's apostille-documents-section.tsx + SignerMatchBlock + signer modal as the custom renderer), submitGate() replaces the inline submitBlockedReason. Classification board page stays (it's already good), advisory synthesis moves server-side (normalize with PT/PP).
  • A6. Nuke v1 + verify. Delete the bespoke code paths (no backwards compatibility — bukti-setor precedent; demo apostille submissions get reprocessed or nuked), update tests (route tests, rule tests, projector tests), tsc --noEmit both sides, deploy to staging, screenshot-verify the full journey (upload → board → review → verifikator decide).

Phase B — Fast-track / Jalur Percepatan (built working, not ported dead)

  • B1. Schema: jalurPermohonan enum (REGULER | PERCEPATAN) on Submission + apostille satellite; migration.
  • B2. Applicant opt-in on the upload page (Apostille service only): option card with the official copy (1 hari kerja / Rp500.000 / 08.00–12.00 WIB / kuota). Server-side eligibility at submit: within office hours (WIB), quota not exhausted (env FASTTRACK_DAILY_QUOTA, 0 = unlimited), service = Apostille.
  • B3. Registered-signer eligibility as a real check: after signer pre-match, a v2 rule JALUR_PERCEPATAN_ELIGIBLE — FAIL/WARNING when a percepatan submission has a conventional document whose signer is UNMATCHED (i.e. pejabat not in the registry — the rebuild's stated restriction, actually enforced here).
  • B4. Verifikator side: worklist sorts percepatan first + "⚡ Percepatan" badge + SLA deadline chip (submit + 1 business day, WIB) with overdue state. List filter by jalur.
  • B5. Fee: display-only (Rp500.000 shown as informasi biaya). We are the audit/verification side — no PNBP billing here (SIMPADHU voucher stuff stays in the source system).

Phase C — Spesimen signature verification (AI match %, verifikator decides)

sequenceDiagram participant BE as OCR backend (Bun) participant MinIO as AHU MinIO (spesimen-file/*) participant SV as ahu-signature-verify (new GPU tenant) participant V as Verifikator UI Note over BE: signer pre-match → MATCHED (spesimenId known) BE->>MinIO: fetch specimen PDF (cached per spesimenId) BE->>SV: /verify { document page(s), specimen PDF } SV->>SV: rasterize → detect signature bboxes (both sides) SV->>SV: crop → embed → cosine similarity SV-->>BE: { matchPercent, docCrop, specimenCrop, confidence } BE->>BE: persist on SignerMatch (score + crop refs) BE->>V: review-data: crops side-by-side + match % badge Note over V: verifikator still decides accept/reject — score is advisory only
  • C1. Access + discovery (needs you — Q3): MinIO read credentials for the specimen bucket. With them: sample ~50 specimen PDFs, study layout variance (signature + stamp sheets), build the fetch client (env-gated, fail-soft like the registry client). Fallback if creds are impossible: admin "unggah spesimen" UI so verifikators can attach a reference PDF per pejabat manually — the AI pipeline is identical from that point on.
  • C2. New service ahu-signature-verify on the GPU host, cloned from the doc-classifier template: FastAPI + torch, own port (5006), Redis cache (sha256-keyed), audit events (ahu.ai.audit), heartbeat, env-gated/dormant. Endpoints: POST /detect (page image → signature bboxes), POST /verify (doc pages + specimen file → {match_percent, crops}). Models: open-weights only, running fully on-prem — a YOLO-family signature detector + an embedding verifier; baseline with a general visual encoder (e.g. DINOv2 embeddings + cosine) and evaluate 1–2 signature-specific metric models against it. C5's eval decides which ships.
  • C3. Backend integration (env SIGNATURE_VERIFY_URL, unset = current behavior): after MATCHED signer resolve → fetch specimen → call verify → persist new SignerMatch fields (signatureScore, signatureDocCrop, signatureSpecimenCrop, signatureStatus incl. NO_SIGNATURE_DETECTED / SPECIMEN_UNAVAILABLE soft states). New v2 rule SPESIMEN_TTD_MATCHWARNING-severity ceiling, never FAIL: the score can never block; only the verifikator accepts/rejects.
  • C4. Verifikator UI: in the document-set signer block — side-by-side signature crops (document vs spesimen), match % with advisory tier (≥80% hijau "sangat mirip", 50–79% kuning "periksa manual", <50% merah "kemungkinan berbeda"), plus links to open both full PDFs. Decision panel summarizes lowest signature score across documents.
  • C5. Eval + calibration before it ships to the verifikator: labeled eval set from real specimens (same-officer pairs = genuine, cross-officer pairs = impostor), report ROC/threshold table, calibrate the tier cutoffs from data instead of the defaults above. If accuracy is poor (noisy stamps, cursive Indonesian signatures), ship crops side-by-side without the % first (still a big verifikator UX win) and iterate the model.

Phase D — Polish, docs, deploy

Per-phase staging deploys as usual (Server 2 :3520, migrations auto-applied, screenshot verification), report per phase, tidyup/tasks.md phase tracking, memory updates. Dev-environment worktrees (dev_1/2/3) rebase guidance noted in the report so your devs aren't surprised by the engine extension in A1.

Sequencing on autopilot: A → B → C3–C5, with C1+C2 interleaved early (C2 scaffolding doesn't depend on A). Rough effort: A is the biggest chunk (engine extension + projector + page), B is small, C is medium-large plus model iteration.


2. Questions — ANSWERED by Efran 2026-07-10, these are now the binding decisions

# Decision
Q1 Full fast-track semantics (office-hours gate, quota env, eligibility rule, SLA badge, priority sort), fee display-only
Q2 Apostille-only
Q3 Efran will request MinIO creds. Build the env-gated MinIO client stubs + consumers now (so QC is fast once creds arrive) and the manual specimen-upload fallback
Q4 Yes to new service on GPU host; prune unused docker images; also kill and delete the fakeshield container
Q5 SOTA models allowed, deploy on GPU 1. If SOTA doesn't fit current GPU, use a smaller model now and reserve SOTA for prod when the B200 arrives
Q6 Keep single APOSTILLE SubmissionType
Q7 Nuke v1 apostille code, no back-compat, reprocess/wipe demo submissions
Q8 Single verification stage (our current APPROVE/REJECT/REVISE)

Original questions (for the record):

# Question My default if you don't say otherwise
Q1 Fast-track depth: full semantics (office-hours gate, quota env, eligibility rule, SLA badge, priority sort) or minimal (flag + priority sort + badge)? Full, fee display-only
Q2 Fast-track for Apostille only (rebuild behavior) or also Legalisasi? Apostille-only
Q3 The real blocker for C: can you get MinIO read creds for the specimen bucket (apostille bucket, keys spesimen-file/…, same infra as the registry DB)? If yes, drop endpoint+creds in .env style; if no/later, I build the manual specimen-upload fallback first. Build both: MinIO client env-gated + manual upload fallback; C4/C5 use whichever has data
Q4 New ahu-signature-verify container on the GPU host ok? Disk there is ~92% full — am I authorized to docker image prune the ~176 GB of reclaimable images first? Yes to service; prune only dangling/unused images, nothing running
Q5 Signature models: open-weights from HF (YOLO-family detector + embedding verifier), fully on-prem — ok to download to the GPU host? Yes
Q6 Keep single APOSTILLE SubmissionType covering both services (current design, mirrors the real system's single-flow + jenis_layanan flag) vs splitting LEGALISASI into its own flow entry? Keep single
Q7 Nuke all v1 apostille code + reprocess/wipe existing demo apostille submissions after migration (no back-compat, bukti-setor precedent)? Yes
Q8 Verification stages: keep our current single verifikator decision (APPROVE/REJECT/REVISE), or mirror the real system's two stages (Verifikator → Kasi)? Keep single stage — we're the audit/assist side, not the system of record

3. Risks / honest caveats

  1. Signature-verification accuracy is unproven on this data. Specimens are scanned sheets with stamps; documents are photocopies/scans. C5's eval decides whether the % is trustworthy enough to show — the fallback (auto-cropped side-by-side, no %) is still a major step up from today's nothing.
  2. A1 touches the shared engine. All PT/PP flows run through runFlow; the classification-phase extension will be additive + covered by regression tests on existing flows before the apostille flow lands on it.
  3. MinIO access is outside my control (Q3). Everything else in C is buildable without it, but real specimen data is what makes C5 meaningful.
  4. mr_signature_template in the registry DB (AHU's own certificate-signing officials) is distinct from spesimen and out of scope — noted so we don't confuse the two later.