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:36→APOSTILLE: { engine: "v1-fork" }— the only non-defineFlowentry.review/review-registry.ts:46→APOSTILLE: 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, 15AP_*rules) + hand-built 655-lineApostilleReviewPage.tsx. - The landing zone already exists:
contract/review.tsalready defines a"document-set"SectionVMkind withsigner/candidates/translationfields — purpose-built for Apostille, unused by any projector today. The Prisma enums (ApostilleCategory13×,ApostilleDocLayout16×) 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 onSignerMatch. 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: falseat every call site), quota check is a hardcodedfalsewith 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 likespesimen-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
PDFViewerpanes) 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.pyclient 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 toahu.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
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). v2runFlowis single-shot today. Add an optional two-phase capability toFlowConfig(e.g.classification?: { classify(ctx), gate(ctx) }— dispatch runs classify → pauses atAWAITING_CLASSIFICATION; the existing/startroute resumesrunFlowin extract phase). Built generically so future multi-doc flows can use it. - A2. Flow definition + rules port.
defineFlowfor APOSTILLE: context builder (applicant + document records + extracted fields + signer matches + destination country/service), port the 15AP_*rules toRule<Ctx>[](mechanical — theRuleResultshapes are already near-identical),validationScopefor 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 throughExtractionPlaninstead of hand-branching inapostille-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.APOSTILLEgoes live; the genericGET /reviewserves it. - A5. Frontend on the review engine. Replace
ApostilleReviewPage.tsxwith a thin shell around<ReviewEngine>(PP pages are the template): new genericDocumentSetSectionrenderer incomponents/review-engine/(reusing the internals of today'sapostille-documents-section.tsx+SignerMatchBlock+ signer modal as the custom renderer),submitGate()replaces the inlinesubmitBlockedReason. 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 --noEmitboth 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:
jalurPermohonanenum (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)
- 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-verifyon 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 newSignerMatchfields (signatureScore,signatureDocCrop,signatureSpecimenCrop,signatureStatusincl.NO_SIGNATURE_DETECTED/SPECIMEN_UNAVAILABLEsoft states). New v2 ruleSPESIMEN_TTD_MATCH— WARNING-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
- 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.
- 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. - MinIO access is outside my control (Q3). Everything else in C is buildable without it, but real specimen data is what makes C5 meaningful.
mr_signature_templatein 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.