think
16px
820px

How legacy Apostille and the Rebuild handle spesimen matching

2026-07-15 · research for Efran ("check and report back") · read-only survey of both codebases
Legacy: ahu-codebase/ahu-apostille (apostille-service + ap-fe + ap-fe-admin) · Rebuild: ahu-codebase/ahu-rebuild/ahu-rebuild-apostille (ahu-apostille-api + module-apostille)


TL;DR

Neither system matches signatures. Both match records, and a human eyeballs the ink.

LEGACY REBUILD tidyup (ours, today)
Who picks the pejabat Applicant, at registration (autocomplete) Applicant, at registration (cascading select) System (OCR name/NIP → registry auto-match) + human correction
Auto name/NIP matching at verification None — the doc already carries the applicant's pick None — same Yes (resolveSigner, contamination-aware since K.28)
Specimen sheet shown to verifikator Yes — true 50/50 side-by-side (document left, "Spesimen Jabatan" right, one viewport) Yes, but stacked vertically — verifikator scrolls between document and specimen Yes — signature crops side-by-side + full-sheet link + AI heatmaps
Automated image comparison / scoring None anywhere None anywhere Advisory AI score (dino+SigNet ensemble), verifikator-only
Signature-level comparison unit Whole sheet vs whole document Whole sheet vs whole document Detected/manual signature crops
Verdict Human Approve/Reject Human Terima/Tolak + keterangan Human verdict (authoritative) + advisory score

The only "score" that exists in either old system is Elasticsearch text relevance for the officer-name autocomplete in legacy (apostille-service/src/service/spesimen-service.ts:54-78). A full grep for opencv|tensorflow|onnx|siamese|cosine|ssim|phash|imagehash|biometric across all four apps returns nothing real.


LEGACY (ahu-apostille)

Matching = the applicant's own selection, made at registration. The pemohon searches the registry by officer name (Elasticsearch wildcard on officer_name.normalize), picks the pejabat and position, and that spesimenPositionId is persisted onto the request (apostille-service/src/service/apostille-service.ts:748-760). If no specimen exists for an electronic document, status flips to "Menunggu Spesimen Terbaru" and a Surat Pengantar is generated instead (:733-738).

Verification = a genuine side-by-side, fully manual. ap-fe-admin/.../VerificationDetail/DocumentDataFile.component.tsx:11-20 renders two preview boxes in one viewport — left the uploaded document, right the linked specimen sheet ("Spesimen Jabatan"), each 50% wide (style.tsx:40-46). The verifikator eyeballs the two PDFs and hits Approve/Reject (VerificatorForm.template.tsx:73-113). No comparison result exists anywhere — the decision is the comparison.

The one officer-data entry a verifikator can do: for documents still awaiting a specimen, an "update specimen" sub-form captures penyampaian/keabsahan type + free-text officer name/position (ValiditySpecimen.component.tsx) — manual data entry, not matching.

Storage: MySQL spesimen + spesimen_position (the same tables our registry reads), specimen file as a MinIO key on spesimen_position.spesimen_file, presigned at detail time (apostille-service.ts:323-325). FORMULIR 01/02 PDFs are generated by lib/pdf.ts:1209,1281 — the same forms whose indexing produces the contamination we measured (K.23).

REBUILD (ahu-rebuild-apostille)

Matching = the same applicant-selected cascade, on Postgres. PejabatSelect.jsx walks name → jabatan → instansi (ILIKE, DISTINCT ON, LIMIT 20 — spesimen-pejabat.repository.ts:83-157) and sets id_spesimen_pejabat. New idea worth noting: if the applicant's officer isn't in the registry, they can upload their own specimen sheet — a spesimen_tangguhan ("deferred specimen"), status "Menunggu Spesimen Pejabat" (permohonan.service.ts:303-355). No NIP matching, no image logic.

Verification = same page but stacked, still fully manual. detailPermohonan returns presigned URLs for the document files and url_spesimen (verifikasi.service.ts:356-525); DetailValid.jsx renders the document viewer up top and the specimen sheet in separate rows further down (:567-603) — the verifikator scrolls to compare rather than seeing both at once. Decision is Terima/Tolak + keterangan (Verif.jsx:59-149). A per-file "Sudah sesuai / Tidak sesuai" radio component exists in the code (CheckVerif.jsx) but is not wired into the live screen.

Storage: Prisma/Postgres (tr_apl_spesimen_pejabat, tr_apl_berkas_spesimen_pejabat.s3_key, tr_apl_spesimen_tangguhan), files in S3/MinIO under spesimen-pejabat/berkas-…, presigned via s3Service.getFileUrl.


What this means for tidyup

  1. Our auto-matching is net-new. Neither predecessor ever matched the OCR'd signer against the registry — they pushed that burden onto the applicant at form-fill time. Our resolveSigner (name+NIP+jabatan composite, contamination-aware since K.28) automates a step that used to be a human's form field, which is exactly the OCR-replaces-form-filling thesis of this project.
  2. The side-by-side has a precedent — the legacy one is the better of the two. Legacy's single-viewport 50/50 beats the rebuild's scroll-to-compare. Ours goes a level deeper (signature crops, not whole sheets, plus the full-sheet link), which is the right direction — the crop is what the eyeball actually needs.
  3. The AI advisory score is also net-new. Both predecessors are 100% eyeball. Our score is an accelerator on top of the same human-decides model, so the "advisory, verifikator decides" framing matches how this process has always legally worked.
  4. The rebuild's spesimen_tangguhan (applicant-uploaded specimen for unregistered officers) is worth stealing — today our unregistered-officer path is acknowledge-and-proceed with nothing to compare against; letting the pemohon attach the officer's specimen sheet would give the verifikator something to eyeball, mirroring a flow the rebuild already designed.
  5. The contamination we measured (K.23) predates us and affected them silently. Legacy/rebuild show the whole sheet with zero attribution checking — a verifikator comparing against a FORMULIR 02 bundle had no warning the ink belongs to the successor. Our sheet-attribution layer is the first thing in this lineage that reads the sheet.

Full file:line evidence gathered read-only from both trees; no engine repos touched.