02 — Assessment Flow: bringing the AHU AI pattern to Koperasi
Status: proposal for review · Date: 2026-07-31
Companion to: koperasi.md (01 — System Map, the read-only survey this is built on)
Precedents it copies: tasks/prd-ptp-ai-transformation.md, tasks/prd-apostille-ai-transformation.md, docs/specs/apostille-build-plan.md
0. What this document is — and what it is not
This is the assessment flow, not the assessment. It defines the stages, the probes to run in each, the artifact each stage must produce, and the gate that must be passed before spending money on the next one. Running it produces the PRD + build plan; running it badly produces the two failure modes we have already paid for once each:
- PTP: we designed an akta-extraction flow before discovering PTP has no akta uploads anywhere (
prd-ptp-ai-transformation.md§2.1). The document layer was assumed, not surveyed. - Apostille: the extraction catalog over-claimed code reuse and had to be corrected adversarially before build ("Authoritative corrections" in
docs/specs/apostille-extraction-catalog.md). Reuse was assumed, not inventoried.
Both failures are stages in this flow now: A1 (document reality) and A4 (reuse inventory).
The one-line framing. The system map answers "what is the koperasi system?" extremely well. It does not answer the only question our product depends on: which paper documents does a notary actually upload, and which typed form fields could those documents have filled? That is the assessment's centre of gravity. Everything else is sequencing.
1. The flow in one picture
koperasi.md"]) --> A0 A0["A0 — Access & feasibility probes
DB read grant · allowed-table check
staging env · sample corpus request"] A0 --> G0{"G0
Can we read the data
and see real documents?"} G0 -->|no| Park["PARK — escalate to AHU.
No further spend."] G0 -->|yes| A1 A1["A1 — Document reality survey ⭐
doc-type taxonomy · required bundles per flow
50–100 real files per type · form-field ↔ doc mapping"] A1 --> G1{"G1
Do uploaded docs actually
contain the typed fields?"} G1 -->|"no / thin"| Rescope["RESCOPE to validation-only
or name-search assist"] G1 -->|yes| A2 A2["A2 — Flow inventory & AI-value scoring
6 flows × volume × manual-field count
× extractability → ranked backlog"] A2 --> A3["A3 — Integration & write-back decision
read path · write path · auth · who owns the front door"] A3 --> A4["A4 — Reuse vs net-new inventory
against flow-engine, ocr/, schema/, lib/*-db.ts
adversarially verified"] A4 --> A5["A5 — Data-quality & risk register
koperasi.md §7 → concrete validator/matcher impact"] A5 --> G2{"G2
Effort vs value defensible
against PP / Apostille / PT queue?"} G2 -->|no| Defer["DEFER with a dated
re-open trigger"] G2 -->|yes| A6["A6 — Scope lock
PRD + phased build plan + effort
(apostille-build-plan.md shape)"] A6 --> Build(["Build starts — Phase 0 scaffold"]) style A1 fill:#fff3cd,stroke:#d39e00,stroke-width:2px style G0 fill:#f8d7da,stroke:#b02a37 style G1 fill:#f8d7da,stroke:#b02a37 style G2 fill:#f8d7da,stroke:#b02a37
Effort legend (one engineer; S ≤ 1 day · M 2–4 days · L 1–2 weeks): A0 S–M · A1 L · A2 M · A3 M · A4 M · A5 S · A6 M. Total assessment ≈ 3–4 weeks, of which A1 is half. A0–A1 are mostly waiting on AHU, so start them on day one and run A4 (pure in-repo work) in parallel.
2. Knowledge ledger — what the map gives us vs what the product needs
| Product question | Answered by the map? | Where / what is missing |
|---|---|---|
| What flows exist? | ✅ | koperasi.md §2, §3 |
| What is the data model + versioning? | ✅ clone-per-transaction | §4.1, §4.2 |
| Who are the actors/roles? | ⚠️ partial — only roles in code branches | §5.2; full list lives in AHU_KOPERASI_DCMS.group |
| Is there a read API? | ✅ /service/get-koperasi-v2 |
§6.1 |
| Is there a write API we may use? | ⚠️ ODS-only XML endpoints | §6.1 — semantics are a trap, see §6 below |
| Are the API routes even reachable from outside? | ❌ unknown | §Open questions #1 — allowed-table gate |
| Which documents are uploaded, per flow? | ❌ only that kop_t_pendirian_dokumen exists |
§6.4 file drops — the A1 gap |
| Which form fields does a notary type by hand? | ❌ | not surveyed — the A1 gap |
| Do we have sample documents? | ❌ | web/uploads gitignored (§6.4) |
| What is the actual volume per flow per month? | ❌ inferred ~440k rows lifetime | §7 / Open questions #9 |
| How dirty is the data? | ✅ unusually well documented | §7.1–§7.4 |
| Cron/ops topology | ❌ | Open questions #4 |
Read this table as the work order. Every ❌ is a probe in A0/A1; every ⚠️ is a confirmation task.
3. Stage-by-stage
A0 — Access & feasibility probes (effort S–M, mostly lead time)
Purpose: find out in week one whether this project is even executable, before anyone designs anything.
| # | Probe | How | Why it matters |
|---|---|---|---|
| A0.1 | Read-only MySQL grant on AHU_KOPERASI (+ AHU_BADAN_HUKUM for m_kbli, WILAYAH_SABH) |
DBA request; no code change per §6.2 | This is the map's own #1 recommendation (§6.5). Every other stage needs to count rows. |
| A0.2 | Dump information_schema for kop_t_koperasi, kop_data_anggota, kop_pemilik_manfaat, kop_t_aktifitas_usaha, kop_t_pendirian_dokumen |
SQL | Resolves Open question #3 — the model docblock is stale (§7.4); we cannot map fields against a docblock we know is wrong. |
| A0.3 | SELECT the allowed / group / auth_assignment tables |
SQL | Open question #1. This decides whether any API integration is possible at all. If /service/* is not whitelisted for a service account, A3 collapses to "DB read + our own front door". |
| A0.4 | SELECT * FROM kop_setting |
SQL | Open question #5 — every integration URL and feature flag resolves through it. |
| A0.5 | Request a document corpus: ≥50 files per document type, spread across years and provinces, from uploads/<transaction_id>/ |
AHU ops ask | Without this A1 cannot run. Longest lead time — file this on day 1. |
| A0.6 | Ask for a staging/sandbox koperasi instance | AHU ops ask | Needed later for A3 contract tests; the map notes errors page an ops group via Telegram (§6.1), so probing production is antisocial. |
| A0.7 | Row counts + monthly rates per flow (kop_t_koperasi grouped by jenis_transaksi_id, created_date month) |
SQL | Feeds the A2 value score. Open question #9. |
Artifact: koperasi-a0-access-report.md — grants obtained, real schemas, allow-list findings, volume table, corpus status.
Gate G0: DB read is live AND a document corpus is committed with a date. Anything less → park and escalate; do not proceed on assumptions.
A1 — Document reality survey ⭐ (effort L — the stage that decides the product)
Purpose: establish, per flow, the mapping uploaded document → form field it could auto-fill. This is the entire value proposition; everything else is plumbing.
form fields"] --> T2["Uploads documents
kop_t_pendirian_dokumen"] T2 --> T3["Preview → approve"] end subgraph target["Target (the AI pattern)"] N1["Upload bundle"] --> N2["Classify"] --> N3["Extract"] N3 --> N4["Review: LOCKED / EDITABLE / FLAGGED"] N4 --> N5["Cross-validate"] --> N6["Finalize"] end today -.->|"A1 measures the
overlap between
T1 and T2"| target
Probes:
- Doc-type taxonomy.
SELECT DISTINCTwhatever type columnkop_t_pendirian_dokumencarries, perjenis_transaksi_id. Cross-check with the UI's upload screen labels. Output: the koperasi analogue of ourDocumentTypeenum (backend/prisma/schema.prisma:20-60). - Required-bundle matrix. flow × doc-type × required/optional. Drives
requiredDocsin a futuredefineFlow(backend/src/flow-engine/types.ts). - Is the akta on the critical path? The map's API response shape includes an
akta_notarissection plusrapat,pengawas_pengurus,modal_koperasi,periode_pengurus,koperasi_pembentuk(§6.1) — strong evidence a real notarial deed (NPAK) is in the loop, unlike PTP. Confirm it, don't assume it. If confirmed, our akta extractor stack is directly on the critical path and this project is materially cheaper than Apostille was. - Field-by-field manual-entry census. For pendirian, list every field the notary types and tag each:
from-akta/from-KTP/from-NPWP/from-bukti-setor/from-domisili/master-data-pick/genuinely-typed. The ratio of the first five to the last is the headline number of the whole assessment. (PTP's equivalent: ~40 fields, most extractable.) - Roster shapes.
kop_data_anggota(members/officers) andkop_pemilik_manfaat(beneficial owners) — cardinality distribution, and which document each row is transcribed from. These two tables have norules()at all (§7.3), i.e. nothing validates them today; that is an unusually clean value story for us, and it mirrors work we have already done for PP BO. - Corpus quality read. Take the A0.5 corpus and answer: scan vs native PDF? photo-of-screen? multi-doc-per-file? Determines whether our existing OCR config transfers or needs re-tuning.
- Sample run. Push 20–30 corpus files through the existing pipeline unchanged (classifier + KTP/NPWP/domisili/bukti-setor extractors) and score field-level accuracy. Cheap, and it converts A4's reuse claims from theory to measurement.
Artifact: koperasi-extraction-catalog.md — the per-document-type extraction & validation catalog, same shape as docs/specs/apostille-extraction-catalog.md, with [CERTAIN] / [INFER] confidence tags per claim.
Gate G1: ≥60% of manually typed pendirian fields are traceable to an uploaded document, on real samples. Below that, the honest move is to rescope to validation-assist + name-similarity rather than sell an extraction product.
A2 — Flow inventory & AI-value scoring (effort M)
Purpose: rank the six flows so Phase 1 ships against the highest-value one instead of the alphabetically first one.
Score each flow: annual volume (A0.7) × manual fields eliminated (A1.4) × extractability (A1.7) ÷ build cost (A4), with a hard modifier for whether a back-office reviewer is in the loop (verifier time saved counts double — it is the scarce resource).
First-pass hypothesis, to be validated in A2, not treated as findings:
| Flow | Map ref | Why it might score high | Why it might score low |
|---|---|---|---|
| Pendirian | §3.1 | Most fields, most documents, akta + KTP + BO + KBLI + rapat data all at once; the archetype we have built 4× | Highest build cost |
| Perubahan | §3.2 | Checklist-driven → maps cleanly onto our change-type/jenis machinery; diff-review UI already exists (PerubahanDeltaReviewPage.tsx) |
Needs reliable "current state" reads — complicated by clone-per-transaction (§4.2) |
| Perbaikan Data | §3.3 | Two-stage verifikator queue — the only flow with a captive back-office audience; we already ship a verifikator persona (VerifikatorPerbaikanReviewPage.tsx) |
Newest module (Aug 2025) → low volume, and its own model/DB enum already disagree (§7.3) |
| Pesan Nama | §2 | Elasticsearch fuzzy index already populated; name-similarity is a solved shape for us (SIMPADHU on PT) | Small field surface — assist, not transformation |
| Pembubaran | §3 | Cheap; we have pembubaran-pt and pp-pembubaran rule sets |
Thin document layer (PTP precedent: dissolution is list-driven, low AI value) |
| Penggabungan / Pembagian | kop_bh_* tables (§4.1) |
We have penggabungan/peleburan rule sets built for PT |
Volume unknown; may be rare enough to ignore |
Artifact: ranked backlog table with the score inputs shown, so the ranking can be re-run when numbers change.
A3 — Integration & write-back decision (effort M)
Purpose: decide the read path, the write path, and — the political question — who owns the front door.
richest, no code change, inherits §7 quality issues"] R2["Option R2: POST /service/get-koperasi-v2
clean, deduped by id_master, needs oss/ods account + allowed row"] R3["Option R3: Elasticsearch kop_pesan_nama
name/dedup work only"] end subgraph write["WRITE (the real decision)"] W1["Option W1: STANDALONE — no write-back.
We finalize in our own system.
Apostille precedent."] W2["Option W2: new REST endpoint built by the koperasi team"] W3["Option W3: masquerade as ODS via /api/transaksi/*"] end read --> D{"Front door?"} write --> D D --> D1["AI app is primary,
koperasi wizard = fallback
(PTP/SABH pattern)"] D --> D2["Koperasi stays primary,
AI app is an assist panel"] style W3 fill:#f8d7da,stroke:#b02a37
Recommendation going in (to be confirmed, not assumed): R1 + R2 for read (R1 for analysis/backfill, R2 as the supported runtime interface), and W1 standalone for Phase 1 — exactly the 2026-06-23 Apostille scope decision, which de-risked that project by removing an integration dependency from the critical path.
W3 is flagged red and should stay red. The map shows why: rows marked is_ods/is_migrasi skip nomor_transaksi generation entirely (§7.2, TransaksiKoperasi.php:381), the ODS clone path explicitly writes nomor_transaksi = '' (§7.2), and the notary identity check is skipped when is_migrasi = 1 (§7.4). Writing through that door means our submissions inherit the exact defect class that required eight repair runs and a rollback script in Feb 2026 (§7.1).
Probes: confirm A0.3's allow-list result end-to-end with a real service account; confirm whether /service/* errors still page an ops Telegram group (§6.1) and get a non-paging path for our traffic; get a written answer on who owns the front door — this is a product decision, not an engineering one, and it silently determines half the frontend scope.
Artifact: koperasi-integration-decision.md — one page, options, chosen path, signed off by product.
A4 — Reuse vs net-new inventory (effort M — run in parallel with A0/A1)
Purpose: produce an honest component ledger. The Apostille lesson is that the first draft of this always over-claims reuse, so this stage ends with an adversarial review pass.
Provisional ledger against the current engine:
| Need | Reuse? | Where |
|---|---|---|
| Flow definition, rules, validation runner, rematch | ♻️ high | defineFlow + FLOW_REGISTRY (backend/src/flow-engine/registry.ts). Adding a SubmissionType makes the registry a missing-key compile error — the adoption forcing mechanism; a koperasi flow slots in as another tenant. |
| KTP / NPWP / domisili / bukti-setor extraction | ♻️ high | backend/src/ocr/ — same Indonesian documents, same layouts |
| Akta notaris extraction | ♻️ medium, pending A1.3 | backend/src/schema/akta-notaris.ts; an akta koperasi has different parties (pendiri/pengurus/pengawas, rapat pendirian) → new schema, same machinery |
| Review UI: 3-state confidence, PDF viewer, field cards, validation panel | ♻️ high | frontend/src/components/*, page shells under frontend/src/pages/ |
| Verifikator persona + queue | ♻️ high | VerifikatorPerbaikanReviewPage.tsx et al — maps onto the two-stage bakum verification (§3.3) |
| Beneficial-owner roster + document handling | ♻️ high | PP BO stack (incl. the shared-document reference-set guard we learned the hard way) |
| KBLI catalogue | ♻️ high | backend/src/lib/kbli-description.ts — and koperasi reads the same AHU_BADAN_HUKUM.m_kbli (§4.3) |
| Name similarity / duplicate check | ♻️ medium | concept proven via SIMPADHU (backend/src/lib/simpadhu-db.ts); koperasi's is an Elasticsearch index → new client |
| Dukcapil NIK verification | ♻️ medium | docs/research/2026-06-29-dukcapil-nik-verification-recon.md; koperasi already calls Dukcapil itself (§6.4) — do not duplicate the call, cross-check against it |
| Koperasi registry read client | 🆕 net-new | Every domain gets its own: sabh-db.ts, pp-registry-db.ts, apostille-spesimen-db.ts → koperasi-db.ts |
| NPAK notary matcher | 🆕 net-new (probably) | The NPAK list lives in koperasi Admin (§2); our notary matching targets SABH PT transactions — same trap the Apostille catalog fell into |
| Akta koperasi + rapat-pendirian schemas | 🆕 | modal koperasi, simpanan pokok/wajib, periode pengurus, koperasi pembentuk |
| Classifier classes for koperasi doc types | 🆕 | retraining on the A1 corpus; in-repo classifier.ts is a thin remote client |
SubmissionType.PENDIRIAN_KOPERASI (+ siblings) |
🆕 | schema + registry entries |
Exit: an adversarial pass (a second reader whose job is to refute each ♻️) converts the ledger into build-ready truth. Every downgraded row is effort we would otherwise have discovered mid-build.
A5 — Data-quality & risk register (effort S)
Purpose: turn the map's §7 — unusually candid and the most valuable section for us — into concrete engineering consequences.
| Finding (map ref) | Consequence for us |
|---|---|
nomor_transaksi values mangled to <nomor>_DUP_<id> and still present (§7.1) |
Never key on nomor_transaksi; any lookup must tolerate the suffix |
| Bulk NIK correction ran in prod Feb 2026 (§7.1) | Historical NIKs are untrustworthy → NIK cross-validation must be advisory-first, exactly the PP-NIK precedent |
nik frequently NULL, with a backfill job (§7.2) |
Cannot use NIK as a join key for identity matching |
Free-text full_nama / nama_koperasi used as join keys (§7.4) |
Our matcher must normalise; expect ambiguity, surface candidates, never auto-fill on a name-only multi-hit |
sanitizeHtmlInput strips & / < > " ' from stored values (§7.4) |
Stored names differ from the akta text — a silent extraction "mismatch" that is not a mismatch. Normalise before comparing. |
| Clone-per-amendment (§4.2) | "Current state" for a Perubahan diff = follow master.id_transaksi, and expect it to have drifted (findExistTransactionWithSK self-heals, §7.4) |
No rules() on DataAnggota / AktifitasUsaha; PemilikManfaat has no required (§7.3) |
Opportunity, not risk — we become the first validation layer these rosters have ever had |
| Hardcoded 550-case regency→Kemendagri switch (§7.4) | Wilayah normalisation is ours to own; do not depend on their mapping |
YII_DEBUG on, credentials in the working tree (§1) |
Treat any koperasi checkout/dump as credential-bearing; secrets go through the OpenBao pattern, never into our repo |
Artifact: a risk table carried forward into the PRD, each row tagged design constraint / validator severity / ops ask.
A6 — Scope lock (effort M)
Purpose: produce the two documents that end the assessment: a PRD in the prd-ptp-ai-transformation.md shape (context, codebase findings that changed the design, revamped process per flow, out-of-scope) and a phased build plan in the apostille-build-plan.md shape (Phase 0 scaffold → … with per-task files, deps, effort, and an explicit exit criterion per phase).
Non-negotiables to carry into the plan:
- Phase 0 is always scaffold-only — enum + registry entry + route + page shell + rematch dispatch wiring. It exits when a submission can be created and the shell renders, with no extraction.
- The first value phase is identity (KTP → identity block + cross-validation), because it is the cheapest ship-able win and the #1 typo field. Apostille Phase 2, PTP step 5 — same shape both times.
- All model/OCR egress goes through the gateway with the standard headers and queue semantics (ahu-gpu-manager/docs/CONVENTIONS.md); engine changes stay dormant until the env flip.
- Read-only means read-only — Phase 1 writes nothing back to AHU_KOPERASI.
4. Gate summary
| Gate | Question | Pass | Fail action |
|---|---|---|---|
| G0 | Can we read the data and see real documents? | DB grant live + corpus committed with a date | Park; escalate. No design spend. |
| G1 | Do uploaded documents actually contain the typed fields? | ≥60% of pendirian manual fields traceable to a document, measured on samples | Rescope to validation-assist + name search, or stop |
| G2 | Is effort-vs-value defensible against the PP → Apostille → PT queue? | Ranked backlog beats the marginal next item in the existing queue | Defer with a dated re-open trigger |
5. Where this sits in the queue — say it out loud
The standing priority is PP → Apostille/Legalisasi → PT. Koperasi is a fourth domain, and this assessment is ~3–4 engineer-weeks before any build starts. Two honest options:
- Assess now, build later. A0/A1 have long external lead times (DB grants, document corpora) that do not consume engineering capacity while waiting. Filing those requests now costs almost nothing and means the build can start the moment the queue clears. Recommended.
- Defer entirely until PP + Apostille are shipped, and accept a cold start later.
What is not on the table is starting the build in parallel: koperasi introduces net-new registry clients, a net-new classifier taxonomy, and new akta schemas — it is not a configuration of an existing flow.
6. Asks for AHU (file these on day 1)
- Read-only MySQL grant on
AHU_KOPERASI+AHU_BADAN_HUKUM(analyst-level, our IP range). - A document corpus — ≥50 files per document type, across years and provinces, with the matching
kop_t_pendirian_dokumenrows. information_schemadump for the corekop_*tables (the repo has noCREATE TABLE— §4.1).SELECTonAHU_KOPERASI_DCMS.allowed/group/auth_assignment, and onkop_setting.- A staging/sandbox koperasi instance, plus an
oss/ods-class service account if API integration survives A3. - A named product owner who can answer the front-door question in §A3.
- Confirmation of who owns the NPAK notary registry and whether it is queryable.
Appendix — open questions this flow inherits from the map
The map's nine open questions are not loose ends; they are assigned:
| Map open question | Owned by |
|---|---|
#1 allowed-table routes |
A0.3 → gates A3 |
| #2 full role/permission matrix | A0.3 |
#3 actual kop_t_koperasi schema |
A0.2 |
| #4 cron schedule | A3 (ops topology) |
#5 kop_setting contents |
A0.4 |
#6 web/koperasi-dashboard/ SPA |
A3 (may already be a front door) |
| #7 which config files are committed | A5 (credential hygiene) |
#8 is_finish semantics |
A1 (state machine for our status mapping) |
| #9 historical volume | A0.7 → feeds A2 scoring |