# PRD — Bank Soal UtiPlay untuk Duel Rasi: Word Pull

**Status:** Implementasi berlangsung — Fase 0–2 selesai, Fase 3–4 menunggu browser acceptance
**Tanggal:** 25 September 2026
**Produk:** UtiPlay + UtiClass
**Fitur:** Bank Soal UtiPlay untuk Duel Rasi: Word Pull
**Repo frontend runtime:** `C:\laragon\www\UtiPlay`
**Repo backend dan portal guru:** `C:\laragon\www\UtiClass`

---

## 0. Cara Menggunakan PRD Ini

Dokumen ini adalah kontrak produk dan implementasi Bank Soal UtiPlay untuk game **Duel Rasi: Word Pull**. IDE/agent yang mengerjakannya wajib:

1. Membaca seluruh bagian keputusan arsitektur, compatibility contract, model data, dan authorization sebelum mengubah kode.
2. Memeriksa ulang `git status`, routes, models, migrations, policies, services, tests, serta kontrak frontend aktual pada kedua repo karena kode dapat berkembang setelah tanggal PRD ini.
3. Mengerjakan fase secara berurutan dan membuat checkpoint setelah setiap fase.
4. Tidak mengubah migration lama yang sudah pernah dijalankan; setiap perubahan schema wajib memakai migration baru.
5. Tidak membuat backend, database, atau sistem akun UtiPlay kedua pada MVP ini.
6. Tidak menyatukan paksa struktur pertanyaan Word Pull dengan Pick A Side.
7. Tidak membocorkan `canonical_answer`, aliases, answer snapshot, atau kunci jawaban melalui endpoint katalog maupun snapshot sebelum fase resolution.
8. Menjaga URL, autentikasi one-time handoff, match lifecycle, controller tim, dan laporan Word Pull yang sudah ada.
9. Menjalankan verification commands dan memenuhi acceptance fase aktif sebelum pindah fase.
10. Menyebutkan secara eksplisit pada handoff: perubahan aktual, test yang dijalankan, test yang belum dijalankan, status Git, deployment, dan risiko tersisa.

Jika implementasi memerlukan perubahan terhadap lifecycle pertandingan, buat regression test terhadap perilaku existing lebih dahulu.

### Status implementasi

- [x] Fase 0 — Baseline, kontrak, dan safety net.
- [x] Fase 1 — Schema tambahan, lifecycle paket, dan policy.
- [x] Fase 2 — Backend CRUD, media, dan publication workflow.
- [ ] Fase 3 — Portal Guru UtiClass.
- [ ] Fase 4 — Integrasi katalog/runtime UtiPlay dan deck soal.
- [ ] Fase 5 — Hardening, browser QA, dan regression.
- [ ] Fase 6 — Rollout terbatas dan verifikasi produksi.

Status hanya boleh dicentang berdasarkan bukti pada checkpoint fase. Push Git, build lokal, deployment, dan verifikasi produksi adalah empat klaim yang berbeda.

---

## 1. Ringkasan Keputusan Produk

Bank Soal UtiPlay memungkinkan guru membuat paket Word Pull sendiri melalui Portal Guru UtiClass. Paket dapat memakai gambar upload guru atau salah satu ilustrasi preset bawaan. Paket milik guru bersifat privat; paket sistem tersedia untuk seluruh guru tetapi hanya dapat dikelola admin.

Keputusan arsitektur utama:

1. **UtiPlay tidak memperoleh backend aplikasi terpisah pada MVP ini.**
2. **UtiClass tetap menjadi sumber identitas, authorization, konten, state pertandingan, answer validation, dan laporan resmi.**
3. UtiPlay tetap menjadi frontend/runtime permainan: host, controller, animasi, audio, dan recovery UI.
4. API resmi tetap berada di namespace `/api/utiplay/*` pada UtiClass.
5. Portal pengelolaan bank soal berada di UtiClass karena guru sudah login dan role tersedia di sana.
6. MVP hanya membuat CRUD konten untuk **Word Pull**.
7. UI boleh memakai payung nama **Bank Soal UtiPlay**, tetapi model pertanyaan tetap game-specific.
8. Pick A Side tetap memakai `utiplay_side_packs` dan `utiplay_side_questions`; penyatuan generic question table bukan bagian MVP.

### Batas tanggung jawab

| Area | UtiClass | UtiPlay |
|---|---|---|
| Akun dan role | Otoritas | Mengonsumsi session/handoff |
| CRUD paket dan soal | Otoritas | Tidak menyediakan editor |
| Upload dan metadata media | Otoritas | Menampilkan media |
| Katalog paket yang boleh dimainkan | Otoritas | Mengonsumsi katalog |
| Match, timer, jawaban, laporan | Otoritas | Mengirim aksi dan merender snapshot |
| Host/controller UI | Tidak | Otoritas presentasi |
| Animasi Phaser | Tidak | Otoritas presentasi |
| Paket Pick A Side | Model game-specific existing | Runtime Pick A Side existing |

Backend/service terpisah baru boleh dievaluasi kembali jika UtiPlay memiliki pengguna di luar UtiClass, kebutuhan deployment independen, beban realtime yang tidak dapat ditangani UtiClass, atau menjadi produk multi-sekolah dengan tenancy sendiri. Evaluasi tersebut bukan bagian PRD ini dan tidak boleh menghasilkan akun kedua tanpa kontrak identitas yang eksplisit.

---

## 2. Baseline Repo yang Diverifikasi pada 24 September 2026

### 2.1 UtiClass

Fondasi yang sudah tersedia:

- `utiplay_packs`, `utiplay_prompts`, dan pivot `utiplay_pack_prompts`;
- `utiplay_matches`, teams, rounds, dan attempts;
- `UtiplayPack`, `UtiplayPrompt`, `UtiplayMatch`, dan model runtime terkait;
- `UtiplayMatchController`, `UtiplayMatchService`, dan `UtiplayMatchPresenter`;
- policy match guru/admin;
- endpoint session handoff, packs, create match, lifecycle, snapshot, attempts, rematch, dan report;
- starter pack 20 prompt;
- encrypted canonical answer dan aliases;
- CORS serta security headers untuk origin UtiPlay;
- feature tests `UtiplayApiTest`;
- tabel dan runtime Pick A Side yang terpisah.

Gap aktual yang menjadi alasan PRD ini:

1. Katalog `GET /api/utiplay/packs` masih mengembalikan seluruh paket aktif tanpa scope pemilik.
2. `POST /api/utiplay/matches` mengambil paket berdasarkan slug tanpa mengulang scope kepemilikan.
3. `pilot_ready` Word Pull dihitung dengan ambang 20 prompt, sedangkan kebutuhan produk baru menetapkan minimal 10.
4. Belum ada lifecycle eksplisit `draft`, `published`, dan `archived`.
5. Prompt belum mempunyai metadata kepemilikan dan sumber media yang cukup untuk CRUD guru.
6. Round hanya menyimpan snapshot jawaban/aliases; visual prompt masih dibaca dari record live.
7. Pemilihan ronde berikutnya hanya mengecualikan prompt terakhir sehingga dapat berulang antara prompt awal walaupun paket memiliki banyak soal.
8. Foreign key match memakai `restrictOnDelete`; paket yang pernah dipakai tidak cocok dengan hard delete biasa.
9. Belum ada Portal Guru untuk mengelola paket Word Pull.

### 2.2 UtiPlay

Fondasi yang sudah tersedia:

- launcher dan registry game;
- Word Pull online Host dan Controller;
- `utiplay-api.ts` dengan session bootstrap, katalog packs, match lifecycle, polling, attempt, rematch, dan report;
- URL Word Pull existing `?view=host&online=1`;
- query `game=duel-rasi-word-pull` tetap dikenali;
- fallback visual bila gambar gagal dimuat;
- lint, typecheck, Vitest, dan build scripts;
- CSP pada `vercel.json` dan `public/.htaccess`.

Gap aktual:

- Host masih menampilkan fallback paket palsu ketika katalog kosong;
- belum ada empty state yang mengarahkan guru ke Bank Soal UtiPlay;
- belum ada dukungan preselect `pack` dari URL;
- CSP perlu mengizinkan media upload dari origin UtiClass;
- tipe katalog belum menyatakan source/ownership paket.

### 2.3 Kondisi Git saat PRD disusun

- UtiClass berada pada `main` yang sinkron dengan `origin/main`.
- Dokumen `PLAN-utiplay-bank-soal.md` adalah perubahan lokal yang belum dilacak.
- PRD ini tidak membuktikan status deployment atau production.

---

## 3. Latar Belakang Masalah

Word Pull saat ini bergantung pada starter pack sistem. Guru belum dapat:

- membuat materi yang sesuai bab atau kelasnya;
- mengunggah visual sendiri;
- menyusun jawaban alternatif;
- menyimpan paket sebagai draft;
- menerbitkan atau mengarsipkan paket;
- melihat apakah paket sudah layak dimainkan;
- membuka UtiPlay dengan paket tertentu dari Portal Guru.

Selain kebutuhan produk tersebut, penambahan CRUD langsung ke schema existing tanpa kontrak baru menimbulkan risiko:

- paket guru lain dapat dipakai melalui direct request walaupun disembunyikan dari dropdown;
- gambar dapat berubah saat match aktif sementara jawaban memakai snapshot lama;
- penghapusan paket merusak atau tertolak oleh histori match;
- file lama dapat terhapus walaupun masih dipakai histori;
- preset relatif dapat diarahkan ke domain yang salah;
- minimal 10 soal tidak otomatis mencegah pengulangan karena algoritma deck belum benar.

PRD ini menyelesaikan kebutuhan guru sekaligus menutup risiko tersebut.

---

## 4. Tujuan, Indikator Keberhasilan, dan Non-Goals

### 4.1 Tujuan MVP

- Guru dapat membuat dan mengelola paket Word Pull miliknya sendiri.
- Guru dapat memakai gambar preset atau upload JPEG/PNG/WebP.
- Guru dapat menyimpan draft sebelum mencapai 10 prompt.
- Paket hanya dapat dipublikasikan jika seluruh readiness rule terpenuhi.
- Guru hanya melihat dan memainkan paket sistem serta paket miliknya sendiri.
- Admin dapat mengelola seluruh paket dan paket sistem.
- UtiPlay menampilkan empty/error/loading state yang jujur tanpa fallback data palsu.
- Match tidak mengulang prompt sebelum deck aktif habis.
- Edit paket tidak mengubah konten match yang sedang atau sudah dimainkan.
- Histori match dan laporan tetap valid setelah paket diarsipkan atau diubah.

### 4.2 Indikator keberhasilan produk

- Guru dapat membuat draft paket pertama tanpa bantuan teknis.
- Paket 10 prompt valid dapat diterbitkan dan muncul di UtiPlay.
- Dari daftar paket, guru dapat membuka Host Word Pull dengan paket yang sudah dipilih.
- Tidak ada paket privat guru lain pada katalog, direct create match, atau rematch.
- Empty state UtiPlay memberi jalan keluar menuju Portal Guru.
- Dalam satu siklus deck, prompt tidak berulang.
- Gambar dan jawaban pada ronde selalu berasal dari versi snapshot yang sama.

### 4.3 Indikator keberhasilan teknis

- Seluruh perubahan schema memakai migration baru.
- Direct endpoint authorization diuji, bukan hanya navigasi/UI.
- Kunci jawaban tidak muncul pada katalog atau snapshot sebelum resolution.
- File upload divalidasi berdasarkan MIME aktual, ukuran, dan dimensi.
- URL media production memakai origin yang benar dan lolos CSP.
- UtiClass targeted/full tests serta UtiPlay lint/typecheck/test/build lulus.
- Regression Word Pull existing dan Pick A Side tetap lulus.

### 4.4 Non-goals MVP

- Backend aplikasi, database, atau sistem akun UtiPlay terpisah.
- Marketplace paket antar guru.
- Berbagi paket antar sekolah.
- Kolaborasi edit realtime.
- Import massal dari Word/Excel/Google Forms.
- AI generator soal atau AI image generation.
- Moderasi konten otomatis.
- Generic question schema yang dipakai seluruh game.
- CRUD paket Pick A Side dalam fase yang sama.
- Penilaian akademik otomatis dari hasil Word Pull.
- CDN/object storage baru; storage UtiClass tetap baseline MVP.

---

## 5. Terminologi

- **Bank Soal UtiPlay:** shell pengelolaan konten game pada Portal Guru UtiClass.
- **Pack/Paket:** kumpulan prompt Word Pull yang dimiliki guru atau sistem.
- **Prompt:** satu unit gambar, alt text, canonical answer, dan accepted aliases.
- **Paket sistem:** paket dengan `created_by = null`; dapat dimainkan semua guru dan dikelola admin.
- **Paket guru:** paket dengan `created_by = user.id`; privat bagi pemilik dan admin.
- **Draft:** paket yang masih disusun dan belum dapat dipakai membuat match.
- **Published:** paket yang lolos readiness dan dapat dipakai membuat match.
- **Archived:** paket yang tidak dapat dipakai untuk match baru tetapi histori dipertahankan.
- **Preset:** ilustrasi bawaan UtiPlay yang diidentifikasi oleh key stabil.
- **Upload:** file gambar milik guru yang disimpan pada public disk UtiClass dengan path immutable.
- **Deck:** urutan prompt untuk satu match; seluruh prompt aktif dipakai satu kali sebelum reshuffle.
- **Snapshot:** salinan immutable konten prompt/pack yang dipakai oleh match atau round.
- **Readiness:** hasil validasi server apakah paket boleh dipublikasikan.

---

## 6. Pengguna dan Authorization

### 6.1 Matriks akses

| Aksi | Guru pemilik | Guru lain | Admin | Siswa |
|---|---:|---:|---:|---:|
| Melihat paket sistem published | Ya | Ya | Ya | Tidak |
| Memainkan paket sistem published | Ya | Ya | Ya | Tidak sebagai host |
| Melihat paket sendiri | Ya | Tidak | Ya | Tidak |
| Membuat paket | Ya | — | Ya | Tidak |
| Mengedit paket guru | Pemilik | Tidak | Ya | Tidak |
| Publish/archive paket guru | Pemilik | Tidak | Ya | Tidak |
| Hard delete draft tanpa histori | Pemilik | Tidak | Ya | Tidak |
| Mengelola paket sistem | Tidak | Tidak | Ya | Tidak |
| Memainkan paket guru lain | Tidak | Tidak | Ya | Tidak |

### 6.2 Aturan policy

`UtiplayPackPolicy` minimal memiliki:

- `viewAny(User $user)` — guru/admin;
- `view(User $user, UtiplayPack $pack)` — sistem published, pemilik, atau admin;
- `create(User $user)` — guru/admin;
- `update(User $user, UtiplayPack $pack)` — pemilik atau admin; paket sistem hanya admin;
- `publish(User $user, UtiplayPack $pack)` — sama dengan update dan readiness harus lulus;
- `archive(User $user, UtiplayPack $pack)` — sama dengan update;
- `delete(User $user, UtiplayPack $pack)` — hanya draft yang belum pernah dipakai dan tidak memiliki reference runtime;
- `play(User $user, UtiplayPack $pack)` — paket published yang sistem/milik sendiri, atau admin.

Policy harus didaftarkan secara eksplisit dan dipakai pada controller web maupun API.

### 6.3 Query scope wajib

Semua query katalog dan create match menggunakan scope yang sama:

```text
admin:
  seluruh paket published yang aktif

guru:
  status = published
  AND is_active = true
  AND (created_by = auth.id OR created_by IS NULL)
```

Memfilter dropdown saja tidak cukup. `POST /api/utiplay/matches`, rematch, dan setiap lookup berdasarkan slug wajib mengulang authorization server-side.

---

## 7. User Stories

### 7.1 Guru

1. Sebagai guru, saya dapat melihat paket sistem dan paket buatan saya pada satu halaman.
2. Saya dapat membuat draft tanpa harus langsung mengisi 10 prompt.
3. Saya dapat memilih ilustrasi preset atau mengunggah gambar sendiri.
4. Saya dapat memasukkan jawaban utama dan beberapa jawaban alternatif.
5. Saya dapat mengurutkan prompt.
6. Saya melihat alasan spesifik jika paket belum siap dipublikasikan.
7. Saya dapat menerbitkan paket setelah minimal 10 prompt aktif valid.
8. Saya dapat mengarsipkan paket tanpa menghapus histori match.
9. Saya dapat membuka UtiPlay dan mendapatkan paket tersebut sudah terpilih.
10. Saya tidak dapat melihat atau memainkan paket privat guru lain.

### 7.2 Admin

1. Sebagai admin, saya dapat melihat dan mengelola seluruh paket.
2. Saya dapat membuat dan merawat paket sistem.
3. Saya dapat menonaktifkan paket bermasalah tanpa menghapus histori.
4. Saya dapat membedakan paket sistem, paket guru, draft, published, dan archived.

### 7.3 Host UtiPlay

1. Sebagai host, saya hanya melihat paket yang sah untuk akun saya.
2. Saya melihat loading, empty, dan error state yang berbeda.
3. Saya tidak melihat paket palsu ketika API mengembalikan daftar kosong.
4. Saya tidak dapat membuat sesi sebelum paket valid dipilih.
5. Saya tetap mendapatkan recovery dan laporan existing.

---

## 8. Alur Produk End-to-End

### 8.1 Membuka Bank Soal

1. Guru login ke UtiClass.
2. Guru membuka menu **Pembelajaran → Bank Soal UtiPlay**.
3. Halaman menampilkan dua kelompok visual:
   - **Paket Saya**;
   - **Paket Sistem**.
4. Filter minimum: status, pencarian nama, dan sumber paket.
5. Paket sistem tampil read-only bagi guru.

### 8.2 Membuat draft

1. Guru memilih **Buat Paket Word Pull**.
2. Guru mengisi nama dan deskripsi.
3. Server membuat slug unik dan menetapkan `created_by` dari session; client tidak boleh mengirim owner bebas.
4. Paket tersimpan sebagai `draft`.
5. Guru masuk ke editor prompt.

Draft boleh memiliki 0–100 prompt. Draft tidak muncul di katalog UtiPlay.

### 8.3 Menambahkan prompt preset

1. Guru memilih **Pilih Ilustrasi Preset**.
2. UI menampilkan 20 preset yang tersedia dari manifest terkontrol.
3. Guru memilih satu preset.
4. Sistem mengisi alt text bawaan yang tetap dapat diperbaiki guru.
5. Guru mengisi canonical answer dan aliases.
6. Server membuat prompt milik guru dengan `asset_source = builtin`; record starter/system tidak diedit atau dibagi sebagai object mutable.

### 8.4 Menambahkan prompt upload

1. Guru memilih **Upload Gambar**.
2. Client menampilkan preview lokal.
3. Server memvalidasi MIME aktual, ukuran, dan dimensi.
4. File disimpan dengan nama UUID/path immutable pada `public` disk UtiClass.
5. Guru wajib mengisi alt text, canonical answer, dan aliases opsional.
6. Mengganti gambar membuat file/path baru; file lama tidak ditimpa in-place.

### 8.5 Menyusun dan memvalidasi paket

Editor menampilkan:

- nomor urut;
- thumbnail;
- source badge `Preset` atau `Upload`;
- canonical answer;
- jumlah aliases;
- status valid/tidak valid;
- drag handle/reorder;
- edit, nonaktifkan, dan hapus sesuai aturan histori.

Server menjadi sumber readiness. Counter hijau di UI hanya representasi hasil validasi server, bukan otoritas.

### 8.6 Menerbitkan paket

Ketika guru memilih **Terbitkan**:

1. Server lock paket yang diperiksa.
2. Server menghitung prompt aktif valid.
3. Server menolak publish jika syarat readiness gagal.
4. Jika valid, status berubah menjadi `published`, `published_at` diisi, dan compatibility flag diperbarui.
5. Paket mulai muncul pada katalog UtiPlay akun pemilik.

### 8.7 Mengedit paket published

- Metadata dan prompt boleh diedit untuk sesi berikutnya.
- Match existing tetap memakai snapshot yang telah dibuat.
- Jika hasil edit membuat prompt aktif kurang dari 10 atau invalid, paket otomatis kembali ke `draft` atau publish ditolak sesuai implementasi service yang dipilih.
- Keputusan MVP: perubahan yang merusak readiness **menurunkan paket ke draft** secara atomik dan menampilkan alasan kepada guru.

### 8.8 Mengarsipkan dan menghapus

- Paket published dihapus dari katalog melalui **Arsipkan**, bukan hard delete.
- Paket archived dapat dilihat pemilik/admin dan dapat dipulihkan menjadi draft.
- Hard delete hanya tersedia untuk draft yang belum mempunyai match/round reference.
- Paket yang pernah dipakai tidak boleh hard delete pada MVP.
- File lama tidak boleh dihapus selama masih direferensikan prompt atau histori match.

### 8.9 Membuka di UtiPlay

1. Guru memilih **Buka di UtiPlay** pada paket published.
2. UtiClass menggunakan route one-time handoff existing:

```text
/utiplay/launch?return_to={allowed UtiPlay URL}
```

3. Return URL Word Pull membawa parameter:

```text
?game=duel-rasi-word-pull&view=host&online=1&pack={pack_slug}
```

4. UtiPlay memuat katalog terotorisasi.
5. Jika slug masih tersedia, paket dipilih otomatis.
6. Jika slug tidak tersedia, UI menjelaskan bahwa paket tidak dapat digunakan dan tidak diam-diam memilih paket lain.

---

## 9. Kontrak Konten Word Pull

### 9.1 Metadata paket

- Nama: wajib, 3–120 karakter.
- Deskripsi: opsional, maksimal 500 karakter.
- Slug: dibuat server, unik, stabil setelah paket diterbitkan.
- Jumlah prompt draft: 0–100.
- Jumlah prompt aktif untuk publish: minimal 10, maksimal 100.
- Pemilik: berasal dari user session.

### 9.2 Metadata prompt

- `type`: tetap `picture_to_word` pada MVP.
- `asset_key`: identifier unik dan stabil; dibuat server.
- `asset_source`: `builtin` atau `upload`.
- `asset_reference`: key preset atau path storage immutable.
- `alt_text`: wajib, 3–240 karakter.
- `canonical_answer`: wajib, 1–120 karakter; encrypted at rest.
- `accepted_aliases`: array opsional, maksimal 10 item, masing-masing 1–120 karakter; encrypted at rest.
- `category`: opsional, maksimal 64 karakter.
- `difficulty`: integer 1–3.
- `is_active`: boolean.
- `created_by`: nullable untuk sistem, user ID untuk prompt guru.

### 9.3 Normalisasi jawaban

Readiness dan runtime memakai normalizer yang sama dengan match service:

- Unicode normalization bila tersedia;
- trim;
- lowercase UTF-8;
- whitespace berulang menjadi satu spasi.

Aliases setelah normalisasi:

- tidak boleh kosong;
- tidak boleh duplikat;
- tidak perlu menyimpan nilai yang identik dengan canonical answer;
- tidak boleh dikirim dalam katalog atau snapshot aktif.

### 9.4 Readiness rules

Paket dapat dipublikasikan hanya jika:

1. memiliki minimal 10 prompt aktif;
2. seluruh prompt aktif memiliki media resolvable;
3. seluruh prompt aktif memiliki alt text dan canonical answer valid;
4. source preset memakai key allow-list;
5. source upload memiliki file pada disk yang dikonfigurasi;
6. tidak ada posisi duplikat;
7. tidak ada placeholder dengan status `placeholder_pending_original_art`;
8. pemilik paket dan prompt konsisten;
9. tidak ada validasi file/metadata yang gagal.

`pilot_ready` pada payload katalog menjadi hasil readiness yang konsisten dengan ambang 10, bukan perhitungan terpisah ambang 20.

---

## 10. Lifecycle Paket

### 10.1 State

```text
draft ──publish──> published ──archive──> archived
  ▲                    │                     │
  └──invalid edit──────┘                     └──restore──> draft
```

### 10.2 Aturan transisi

| Dari | Aksi | Ke | Syarat |
|---|---|---|---|
| draft | publish | published | Readiness lulus |
| published | edit valid | published | Readiness tetap lulus |
| published | edit invalid | draft | Perubahan disimpan atomik dan alasan tampil |
| published | archive | archived | Pemilik/admin |
| archived | restore | draft | Pemilik/admin |
| draft | delete | deleted | Belum pernah dipakai dan tidak direferensikan |

### 10.3 Compatibility `is_active`

Kolom existing `is_active` dipertahankan pada MVP untuk menghindari breaking migration:

- `published` → `is_active = true`;
- `draft` dan `archived` → `is_active = false`.

Hanya domain service yang boleh mengubah status dan compatibility flag agar keduanya tidak drift. Penghapusan `is_active` dapat direncanakan pada migration/major contract berikutnya, bukan dalam fase awal ini.

---

## 11. Struktur Frontend Portal Guru UtiClass

### 11.1 Navigasi

Tambahkan **Bank Soal UtiPlay** pada kategori **Pembelajaran** untuk guru dan admin. Menu tidak tampil untuk siswa.

### 11.2 Halaman index

Target file:

```text
resources/js/Pages/Utiplay/Packs/Index.tsx
```

Konten minimum:

- header dan deskripsi singkat;
- tombol **Buat Paket Word Pull**;
- kartu ringkasan total draft, published, dan archived;
- filter status dan pencarian;
- tabel/grid compact;
- badge `Milik Saya` atau `Sistem`;
- nama, jumlah prompt aktif, readiness, updated time;
- aksi Edit, Terbitkan, Arsipkan, Pulihkan, Hapus Draft, dan Buka di UtiPlay sesuai policy;
- empty state terpisah untuk belum punya paket dan filter tanpa hasil.

### 11.3 Halaman create/edit

Target file:

```text
resources/js/Pages/Utiplay/Packs/Form.tsx
```

Kontrak UX:

- metadata paket berada di bagian atas;
- prompt memakai list builder yang tetap usable di mobile;
- pilihan `Preset` dan `Upload` jelas per prompt;
- preview memiliki alt text;
- aliases menggunakan chip/list input, bukan string koma mentah;
- drag/reorder memiliki alternatif keyboard/tombol naik-turun;
- error ditampilkan pada baris terkait;
- readiness panel sticky/terlihat dengan jumlah valid dan daftar masalah;
- primary action draft adalah **Simpan Draft**;
- **Terbitkan** terpisah dan disabled berdasarkan readiness response server;
- ada konfirmasi sebelum archive/delete;
- perubahan yang belum disimpan memicu peringatan navigasi.

### 11.4 Mobile dan accessibility contract

- Target sentuh minimal 44×44 px.
- Label form terhubung dengan input.
- Status tidak dibedakan dengan warna saja.
- Reorder dapat dilakukan tanpa drag.
- Modal memiliki focus trap dan tombol tutup yang jelas.
- Error summary fokus ke field pertama yang gagal.
- Thumbnail tidak menggeser layout ketika selesai dimuat.
- Tidak ada horizontal overflow pada 320 px.

---

## 12. Arsitektur Data

Semua perubahan dibuat melalui migration baru. Nama final migration mengikuti timestamp implementasi aktual.

### 12.1 Perluasan `utiplay_packs`

Kolom target:

| Kolom | Tipe | Catatan |
|---|---|---|
| `status` | string(16), indexed | `draft`, `published`, `archived` |
| `published_at` | timestamp nullable | Waktu publish terakhir |
| `archived_at` | timestamp nullable | Waktu archive |

Backfill:

- pack system existing yang `is_active = true` menjadi `published`;
- pack existing nonaktif menjadi `archived` kecuali bukti domain menyatakan draft;
- backfill wajib idempotent pada test/migration path yang digunakan.

### 12.2 Perluasan `utiplay_prompts`

Kolom target:

| Kolom | Tipe | Catatan |
|---|---|---|
| `created_by` | FK users nullable, indexed | Null untuk sistem |
| `asset_source` | string(16), indexed | `builtin` atau `upload` |
| `asset_reference` | string | Preset key atau path storage |
| `mime_type` | string nullable | Upload only |
| `file_size` | unsigned bigint nullable | Upload only |
| `image_width` | unsigned integer nullable | Upload only |
| `image_height` | unsigned integer nullable | Upload only |

`image_url` existing tetap dipertahankan sebagai compatibility/read field selama MVP. Penulisan dan pembacaan URL wajib melalui media service agar source tidak tertukar.

### 12.3 Perluasan `utiplay_matches`

| Kolom | Tipe | Catatan |
|---|---|---|
| `pack_slug_snapshot` | string | Slug saat match dibuat |
| `pack_name_snapshot` | string | Nama saat match dibuat |

Deck state dapat disimpan pada `settings` existing:

```json
{
  "round_seconds": 20,
  "cooldown_seconds": 2,
  "prompt_deck": [12, 31, 8, 19],
  "prompt_cursor": 1,
  "deck_cycle": 1
}
```

Jika implementasi memilih tabel deck tersendiri, perubahan tersebut harus didokumentasikan pada checkpoint dan memberi jaminan recovery/idempotency yang setara.

### 12.4 Perluasan `utiplay_match_rounds`

Tambahkan snapshot minimum:

| Kolom | Tipe | Catatan |
|---|---|---|
| `prompt_type_snapshot` | string(32) | `picture_to_word` |
| `asset_key_snapshot` | string | Identifier saat ronde dibuat |
| `asset_source_snapshot` | string(16) | `builtin` atau `upload` saat ronde dibuat |
| `asset_reference_snapshot` | string | Preset key atau path immutable saat ronde dibuat |
| `image_url_snapshot` | string | URL resolved saat ronde dibuat untuk compatibility |
| `alt_text_snapshot` | string(240) | Alt text saat ronde dibuat |

`answer_snapshot` dan `aliases_snapshot` encrypted existing tetap dipakai.

Presenter ronde tidak boleh membaca visual dari `round->prompt` setelah snapshot tersedia. FK prompt tetap berguna untuk lineage, tetapi payload runtime berasal dari snapshot.

### 12.5 Relasi prompt dan ownership

Pivot existing tetap dipakai untuk compatibility. Aturan MVP:

- prompt guru dibuat khusus untuk paket guru tersebut;
- UI tidak menawarkan share/reuse prompt antar paket;
- preset berarti reuse asset immutable, bukan reuse record prompt mutable;
- server memastikan prompt guru tidak dipasang pada paket guru lain;
- prompt sistem tetap dapat dikelola admin;
- detachment/deactivation tidak langsung menghapus record atau file yang sudah dipakai histori.

### 12.6 Constraint dan index

Minimal:

- index `utiplay_packs(status, created_by)`;
- index `utiplay_prompts(created_by, asset_source, is_active)`;
- unique pivot existing tetap dipertahankan;
- posisi prompt dalam pack tetap unik;
- slug dan asset key dibuat server dan tetap unik;
- FK existing tidak dilonggarkan demi memudahkan hard delete.

---

## 13. Backend Boundaries

### 13.1 Class baru yang direncanakan

```text
app/Http/Controllers/UtiplayPackController.php
app/Http/Requests/Utiplay/StorePackRequest.php
app/Http/Requests/Utiplay/UpdatePackRequest.php
app/Http/Requests/Utiplay/StorePromptRequest.php
app/Http/Requests/Utiplay/UpdatePromptRequest.php
app/Policies/UtiplayPackPolicy.php
app/Services/UtiplayPackService.php
app/Services/UtiplayPromptMediaService.php
```

Nama class dapat disesuaikan jika repo memiliki konvensi yang lebih tepat, tetapi pemisahan responsibility harus dipertahankan.

### 13.2 Perubahan class existing

- `UtiplayPack` — casts, scopes, lifecycle helpers, dan relation ownership.
- `UtiplayPrompt` — asset metadata, ownership, dan media resolver.
- `UtiplayMatchController` — scoped catalog dan scoped pack lookup.
- `CreateMatchRequest` — validasi pack tidak hanya `exists`, tetapi authorization-aware.
- `UtiplayMatchService` — deck, snapshot penuh, dan pack readiness.
- `UtiplayMatchPresenter` — media snapshot dan URL contract.
- `UtiplayStarterPackSeeder` — source metadata/backfill aman dan idempotent.
- `AppServiceProvider` — registration policy jika discovery tidak dipakai.

### 13.3 Aturan domain service

`UtiplayPackService` menjadi satu-satunya jalur untuk:

- membuat draft;
- mengubah metadata;
- menambah/mengubah/mengurutkan prompt;
- menghitung readiness;
- publish;
- demote published pack menjadi draft jika edit membuat invalid;
- archive/restore;
- hard delete draft yang aman.

Controller tidak boleh mengulang business rule yang sama dengan API.

Payload halaman edit boleh mengembalikan canonical answer dan aliases yang sudah didekripsi hanya setelah policy owner/admin lulus. Field tersebut tetap hidden pada serialisasi model umum, dipetakan eksplisit oleh controller, memakai response `Cache-Control: no-store`, dan tidak boleh masuk log, flash message, analytics, atau props halaman lain.

### 13.4 Transaction dan file lifecycle

- Perubahan database paket/prompt dibungkus transaction.
- Upload memakai nama/path baru yang immutable.
- Jika database gagal setelah upload, file baru dibersihkan secara aman.
- File lama baru dapat dihapus setelah database commit dan terbukti tidak memiliki reference aktif/historis.
- Pada MVP, lebih aman mempertahankan file historis daripada menghapus agresif.
- Jangan memakai glob/path dari input user untuk delete.

---

## 14. Route Contract Portal Guru

Seluruh route berada di dalam middleware `auth`, password-change guard existing, dan `can:guru` karena gate `guru` sudah mencakup admin.

| Method | Route | Nama | Fungsi |
|---|---|---|---|
| GET | `/utiplay/packs` | `utiplay.packs.index` | Daftar paket terotorisasi |
| GET | `/utiplay/packs/create` | `utiplay.packs.create` | Form draft baru |
| POST | `/utiplay/packs` | `utiplay.packs.store` | Buat draft |
| GET | `/utiplay/packs/{pack}/edit` | `utiplay.packs.edit` | Editor paket |
| PATCH | `/utiplay/packs/{pack}` | `utiplay.packs.update` | Update metadata |
| POST | `/utiplay/packs/{pack}/prompts` | `utiplay.packs.prompts.store` | Tambah prompt |
| PATCH | `/utiplay/packs/{pack}/prompts/{prompt}` | `utiplay.packs.prompts.update` | Update prompt |
| DELETE | `/utiplay/packs/{pack}/prompts/{prompt}` | `utiplay.packs.prompts.destroy` | Detach/delete aman |
| POST | `/utiplay/packs/{pack}/reorder` | `utiplay.packs.reorder` | Reorder prompt |
| POST | `/utiplay/packs/{pack}/publish` | `utiplay.packs.publish` | Publish jika ready |
| POST | `/utiplay/packs/{pack}/archive` | `utiplay.packs.archive` | Archive |
| POST | `/utiplay/packs/{pack}/restore` | `utiplay.packs.restore` | Kembali ke draft |
| DELETE | `/utiplay/packs/{pack}` | `utiplay.packs.destroy` | Hard delete draft unused |

Route model binding tidak menggantikan policy. Nested prompt harus diverifikasi benar-benar terkait dengan pack pada URL.

---

## 15. Kontrak API UtiPlay

### 15.1 Katalog packs

Endpoint existing:

```text
GET /api/utiplay/packs
```

Authorization:

- guru: paket sistem published + paket sendiri published;
- admin: seluruh paket published aktif;
- siswa: `403`;
- guest: `401`.

Payload minimum:

```json
{
  "packs": [
    {
      "slug": "starter-picture-words",
      "name": "Starter Picture Words",
      "description": "Kosakata gambar dasar",
      "source": "system",
      "prompt_count": 20,
      "visual_ready_count": 20,
      "pilot_ready": true
    }
  ]
}
```

Larangan payload:

- `canonical_answer`;
- `accepted_aliases`;
- answer snapshot;
- storage path internal yang tidak diperlukan client;
- data pemilik guru lain.

### 15.2 Create match

Endpoint existing:

```text
POST /api/utiplay/matches
```

Selain validasi request, server wajib:

1. query paket dengan scope user;
2. memastikan status `published` dan readiness lulus;
3. menolak slug valid milik guru lain dengan `404` atau `403` yang konsisten;
4. snapshot nama/slug paket;
5. membentuk deck dari prompt aktif valid;
6. membuat round pertama dari snapshot prompt;
7. tidak mempercayai owner/status dari client.

### 15.3 Error codes minimum

| HTTP | Code | Kondisi |
|---:|---|---|
| 401 | `UNAUTHENTICATED` | Session tidak ada/kedaluwarsa |
| 403 | `PACK_NOT_ALLOWED` | User tidak berhak memakai pack |
| 404 | `PACK_NOT_FOUND` | Pack tidak ada pada scope yang boleh diketahui |
| 409 | `PACK_NOT_READY` | Pack berubah/tidak lagi published |
| 422 | `PACK_VALIDATION_FAILED` | Payload CRUD/readiness invalid |
| 413 | `MEDIA_TOO_LARGE` | Upload melebihi batas server |
| 415 | `MEDIA_TYPE_NOT_ALLOWED` | MIME tidak diizinkan |

Pesan UI berbahasa Indonesia dan tidak menampilkan exception mentah.

### 15.4 Compatibility protocol

- Protocol Word Pull tetap `word-pull.v1` kecuali perubahan payload benar-benar breaking.
- Penambahan field katalog bersifat additive.
- URL endpoint match existing tetap dipertahankan.
- One-time handoff dan credentialed session tetap dipakai.
- UtiPlay tidak menyimpan answer key lokal.

---

## 16. Deck, Snapshot, dan Histori

### 16.1 Aturan deck

1. Saat match dibuat, server mengambil seluruh prompt aktif valid dari pack.
2. Server membentuk urutan acak dan menyimpannya pada state match.
3. Satu prompt hanya dipakai sekali sebelum deck habis.
4. Setelah deck habis, server boleh reshuffle karena duel dapat berlangsung lebih dari jumlah prompt.
5. Prompt pertama siklus baru tidak boleh sama dengan prompt terakhir siklus sebelumnya jika jumlah prompt lebih dari satu.
6. Cursor dan cycle disimpan server-side agar refresh/retry tidak mengubah urutan.
7. Pembuatan ronde berikutnya dilakukan dalam transaction/row lock existing.
8. Timeout tetap mengonsumsi satu item deck.

Minimal 10 prompt adalah aturan kualitas paket, bukan asumsi bahwa duel pasti selesai dalam 10 ronde.

### 16.2 Snapshot ronde

Pada saat round dibuat, server menyalin:

- prompt type;
- asset key;
- asset source dan asset reference immutable;
- resolved image URL untuk compatibility client;
- alt text;
- canonical answer encrypted;
- aliases encrypted.

Setelah itu, edit prompt tidak mengubah round existing.

### 16.3 Snapshot paket dan laporan

Laporan memakai `pack_name_snapshot` dan bukan nama live pack. Archive atau rename setelah match tidak boleh mengubah nama historis pada report.

### 16.4 Rematch

Rematch:

- mengulang authorization pack terhadap host saat ini;
- ditolak jika pack archived/tidak lagi boleh dimainkan, kecuali keputusan produk kemudian menyediakan rematch snapshot khusus;
- membuat match dan deck baru;
- tidak memakai urutan deck lama;
- tetap mempertahankan batas ownership.

Keputusan MVP: rematch membutuhkan pack masih published dan terotorisasi.

---

## 17. Media, Storage, dan CSP

### 17.1 Preset builtin

- Preset diidentifikasi dengan key allow-list, contoh `wp-001`.
- Asset fisik tetap dilayani UtiPlay dari `/assets/prompts/...` pada MVP.
- Media resolver memakai `UTIPLAY_FRONTEND_URL` untuk menghasilkan URL absolut bila API membutuhkan URL penuh.
- Jangan membungkus URL preset dengan `asset()` UtiClass.
- Manifest preset memuat key, path, default alt text, dan status lisensi.

### 17.2 Upload guru

- Disk: `public` UtiClass.
- Direktori logis: `utiplay/prompts/{user_id}/`.
- Nama file: UUID/server-generated; nama asli tidak dipakai sebagai path.
- MIME allow-list: `image/jpeg`, `image/png`, `image/webp`.
- SVG, GIF animasi, executable, dan file yang hanya mengandalkan extension ditolak.
- Maksimal file MVP: 3 MB.
- Dimensi minimum: 320×320 px.
- Dimensi maksimum: 4096×4096 px.
- Preview tidak menggantikan validasi server.
- `storage:link`/public storage production harus diverifikasi.

### 17.3 URL resolver

```text
asset_source = builtin
  → {UTIPLAY_FRONTEND_URL}/assets/prompts/{preset}.webp

asset_source = upload
  → absolute URL dari public disk UtiClass / APP_URL
```

Resolver harus menghasilkan URL HTTPS pada production dan URL origin local yang dikonfigurasi pada development.

### 17.4 CSP

UtiPlay production harus mengizinkan minimal:

```text
img-src 'self' data: blob: https://class.utiverse.app
```

Perubahan diterapkan konsisten pada:

- `C:\laragon\www\UtiPlay\vercel.json`;
- `C:\laragon\www\UtiPlay\public\.htaccess`.

Jika Portal Guru memuat preview preset langsung dari `play.utiverse.app`, CSP UtiClass juga harus diperiksa. Jangan melonggarkan ke wildcard `https:` tanpa kebutuhan.

### 17.5 Retensi dan cleanup

- File tidak ditimpa; edit upload membuat file baru.
- File yang dipakai match historis dipertahankan selama histori dipertahankan.
- Cleanup hanya menghapus file orphan yang tidak direferensikan prompt, round snapshot policy, atau record lain.
- Cleanup massal bukan bagian MVP kecuali disertai dry-run dan tests.

---

## 18. UX Form Sesi Word Pull di UtiPlay

Target utama:

```text
src/games/word-pull/WordPullOnlineHost.tsx
src/services/utiplay-api.ts
```

### 18.1 State wajib

| State | Tampilan | Aksi |
|---|---|---|
| Loading | Skeleton/spinner katalog | Tidak dapat create |
| Guest | Ajakan login UtiClass | One-time handoff |
| Ready | Dropdown paket valid | Create match aktif |
| Empty | Tidak ada paket tersedia | Buka Bank Soal UtiPlay |
| Error | Pesan koneksi/API | Retry |
| Preselected missing | Paket URL tidak tersedia | Kembali ke Bank Soal/pilih manual bila ada |

### 18.2 Larangan fallback palsu

Hapus fallback:

```ts
{ slug: 'starter-picture-words', name: 'Starter Picture Words' }
```

Daftar kosong harus tetap kosong. UtiPlay tidak boleh menyajikan opsi yang tidak dikembalikan API.

### 18.3 Empty state copy

Copy minimum:

> Belum ada paket Word Pull yang siap dimainkan. Buat atau terbitkan paket melalui Bank Soal UtiPlay di Portal Guru.

Aksi:

- **Buka Bank Soal UtiPlay**;
- **Coba Lagi** bila request sebelumnya gagal.

### 18.4 Preselect pack

- Baca query `pack` hanya setelah katalog terotorisasi selesai.
- Cocokkan slug secara exact pada katalog.
- Jangan mengirim slug tersebut sebagai create match bila tidak ada pada katalog.
- Jika tidak ada query pack, pilih paket pertama hanya ketika katalog tidak kosong.

---

## 19. Compatibility Contract — Tidak Boleh Dilanggar

1. URL Word Pull existing tetap bekerja.
2. `word-pull.v1` tetap kompatibel untuk client existing.
3. Session exchange dan one-time handoff tetap menjadi metode login UtiPlay.
4. Controller tim siswa tetap dapat join menggunakan PIN sesuai policy existing.
5. Answer, aliases, dan answer snapshots tidak bocor sebelum resolution.
6. Match lifecycle, timeout, cooldown, idempotent submission, recovery, dan report existing tidak boleh regresi.
7. Starter pack sistem existing tetap playable setelah backfill.
8. Pick A Side tidak memakai tabel Word Pull dan tidak berubah perilaku karena fitur ini.
9. Paket privat guru tidak dapat diakses melalui direct URL, direct POST, slug guessing, atau rematch.
10. Edit/rename/archive paket tidak mengubah match historis.
11. Migration lama tidak diedit.
12. UtiClass tetap sumber identitas dan server authority.
13. UtiPlay production tidak boleh fallback diam-diam ke mock/default pack.

---

## 20. Strategi Pengujian

### 20.1 Backend feature tests — CRUD dan policy

- guru dapat membuat draft;
- siswa tidak dapat membuka route bank soal;
- guru lain tidak dapat melihat/edit/publish/archive/delete paket privat;
- admin dapat mengelola paket guru dan sistem;
- owner berasal dari session, bukan request;
- paket sistem read-only bagi guru;
- draft dapat disimpan di bawah 10 prompt;
- publish ditolak di bawah 10 prompt aktif;
- publish berhasil pada 10 prompt valid;
- edit invalid menurunkan published menjadi draft;
- archive menghapus paket dari katalog tanpa menghapus histori;
- hard delete ditolak setelah pack pernah dipakai;
- nested prompt binding tidak menerima prompt dari pack lain.

### 20.2 Backend tests — media

- JPEG/PNG/WebP valid diterima;
- MIME palsu ditolak;
- SVG/GIF/executable ditolak;
- file terlalu besar atau dimensi invalid ditolak;
- nama/path dibuat server;
- upload failure/database rollback tidak meninggalkan orphan baru;
- file historis tidak dihapus;
- preset key di luar allow-list ditolak;
- resolver builtin memakai origin UtiPlay;
- resolver upload memakai origin UtiClass.

### 20.3 Backend API tests

- guru menerima paket sistem + miliknya;
- guru tidak menerima paket guru lain;
- admin menerima seluruh paket published;
- siswa forbidden;
- direct create match dengan slug guru lain ditolak;
- archived/draft pack tidak dapat dimainkan;
- response tidak memuat answer/aliases;
- readiness minimum konsisten 10;
- rematch mengulang authorization;
- pack snapshot dan round snapshot tidak berubah setelah source diedit.

### 20.4 Backend runtime/deck tests

- 10 prompt pertama unik dalam satu cycle;
- cycle berikutnya boleh reshuffle tetapi tidak langsung mengulang prompt terakhir;
- timeout menggerakkan cursor sekali;
- concurrent resolution tidak menggandakan round/cursor;
- refresh/sync tidak mengubah urutan deck;
- answer dan visual snapshot berasal dari prompt yang sama;
- report memakai pack name snapshot;
- submission idempotency existing tetap lulus.

### 20.5 UtiClass frontend tests/manual QA

- menu hanya untuk guru/admin;
- index dan filter usable;
- create/edit draft;
- preset/upload toggle;
- preview dan row error;
- aliases input;
- reorder mouse, touch, dan keyboard fallback;
- readiness panel;
- publish/archive/restore/delete confirmation;
- mobile 320/390 px tanpa overflow;
- dark/light mode terbaca bila layout mendukung keduanya.

### 20.6 UtiPlay tests

- daftar pack tidak mempunyai fallback palsu;
- loading/empty/error/ready berbeda;
- create disabled saat tidak ada pilihan;
- CTA bank soal memakai URL UtiClass yang benar;
- preselected pack dipilih bila tersedia;
- preselected pack invalid tidak dipakai;
- URL upload dapat dirender;
- CSP header/config sesuai;
- Word Pull Host/Controller existing tidak regresi;
- Pick A Side tests tetap lulus.

### 20.7 Security tests

- IDOR pada pack/prompt;
- mass assignment `created_by`, `status`, `asset_reference`;
- path traversal filename/path;
- XSS pada nama, deskripsi, alt text, canonical answer, aliases;
- direct POST bypass terhadap publish/readiness;
- answer leakage pada JSON, error, log, dan Inertia props;
- CSRF pada mutation web/API credentialed;
- slug enumeration tidak membocorkan paket privat.

---

## 21. Fase Implementasi dan Checkpoint

### Fase 0 — Baseline, Kontrak, dan Safety Net

**Tujuan:** mengunci perilaku existing sebelum perubahan schema/runtime.

Pekerjaan:

- verifikasi status Git kedua repo;
- catat commit awal dan perubahan lokal yang tidak boleh disentuh;
- jalankan targeted tests UtiPlay existing;
- jalankan Word Pull frontend gates;
- dokumentasikan katalog, create match, round presenter, dan seed behavior existing;
- tambahkan regression test untuk scope create match dan pola pengulangan deck sebelum memperbaikinya;
- buat checkpoint Fase 0.

Acceptance:

- [x] baseline test dan failure existing terdokumentasi;
- [x] contract endpoint existing dicatat;
- [x] perubahan lokal pengguna tidak ikut tersentuh;
- [x] belum ada migration atau UI baru sebelum safety net tersedia.

### Fase 1 — Schema Tambahan, Lifecycle, dan Policy

**Tujuan:** membangun domain paket yang aman tanpa UI penuh.

Pekerjaan:

- buat migration baru untuk pack status, prompt ownership/source, dan snapshots;
- backfill starter/system pack;
- tambah model casts/scopes;
- buat `UtiplayPackPolicy`;
- buat pack/media service contract;
- implementasikan readiness;
- tambah tests migration, policy, lifecycle, dan backfill.

Acceptance:

- [x] migration up/down aman sesuai dukungan database proyek;
- [x] starter pack tetap published/playable;
- [x] draft/publish/archive transition teruji;
- [x] system/owner/admin rules teruji;
- [x] migration lama tidak diedit.

### Fase 2 — Backend CRUD, Media, dan Publication Workflow

**Tujuan:** menyediakan operasi server lengkap dan aman.

Pekerjaan:

- controller dan form requests;
- routes web;
- prompt CRUD dan reorder;
- upload validation dan media resolver;
- publish/archive/restore/delete safeguards;
- activity/audit event yang tidak menyimpan answer mentah;
- feature/security tests.

Acceptance:

- [x] semua mutation melewati policy;
- [x] nested prompt ownership aman;
- [x] upload dan rollback file teruji;
- [x] hard delete historis ditolak;
- [x] readiness hanya dihitung server.

### Fase 3 — Portal Guru UtiClass

**Tujuan:** memberikan alur authoring yang usable bagi guru/admin.

Pekerjaan:

- menu sidebar;
- index, filters, badges, dan empty states;
- create/edit/listbuilder;
- preset picker dan upload preview;
- aliases, reorder, row errors, readiness panel;
- publish/archive/restore/delete dialogs;
- Buka di UtiPlay melalui handoff;
- responsive dan accessibility QA.

Acceptance:

- [ ] guru dapat menyelesaikan draft → 10 prompt → publish;
- [ ] guru tidak melihat control admin/paket guru lain;
- [ ] system pack read-only bagi guru;
- [ ] mobile 320/390 px usable;
- [ ] keyboard flow dan error focus lulus.

### Fase 4 — Integrasi Katalog, Runtime UtiPlay, dan Deck

**Tujuan:** menghubungkan paket guru dengan match tanpa merusak runtime.

Pekerjaan UtiClass:

- scope `packs()`;
- authorization-aware create match dan rematch;
- readiness minimum 10 konsisten;
- deck/cursor/cycle;
- pack dan round snapshot penuh;
- presenter memakai snapshot;
- absolute media URL resolver.

Pekerjaan UtiPlay:

- hapus fallback pack palsu;
- loading/empty/error/ready states;
- CTA Portal Guru;
- preselect query `pack`;
- update type contract;
- CSP kedua hosting config.

Acceptance:

- [ ] paket published milik guru muncul dan dapat dimainkan;
- [ ] direct create paket guru lain ditolak;
- [ ] tidak ada pengulangan sebelum deck habis;
- [ ] edit prompt tidak mengubah match berjalan;
- [ ] preset dan upload tampil dari origin benar;
- [ ] answer key tetap aman.

### Fase 5 — Hardening, Browser QA, dan Regression

**Tujuan:** membuktikan fitur secara lintas role, viewport, dan origin.

Pekerjaan:

- seluruh automated gates kedua repo;
- browser QA UtiClass guru/admin;
- browser QA UtiPlay host + dua controller;
- storage link dan CSP verification;
- network inspection answer leakage;
- test archive/edit saat match aktif;
- test reconnect/rematch;
- audit logs dan error messages;
- buat checkpoint lengkap.

Acceptance:

- [ ] automated gates lulus;
- [ ] Chrome/Edge local flow lulus;
- [ ] 320, 390, 1024×600, dan 1366×768 tidak memiliki blocker;
- [ ] tidak ada IDOR/answer leakage;
- [ ] Word Pull existing dan Pick A Side tidak regresi.

### Fase 6 — Rollout Terbatas dan Verifikasi Produksi

**Tujuan:** merilis pasangan frontend/backend yang cocok dan membuktikan alur production.

Pekerjaan:

- backup database sesuai prosedur UtiClass;
- deploy migration/backend UtiClass;
- verifikasi `storage:link`, APP_URL, UTIPLAY_FRONTEND_URL, CORS, cookies, dan CSRF;
- deploy frontend UtiPlay dengan CSP baru;
- smoke guru create draft/upload/publish;
- smoke Buka di UtiPlay/create match/play/report;
- verifikasi paket guru lain tidak bocor memakai akun berbeda;
- verifikasi starter pack dan Pick A Side;
- dokumentasikan rollback.

Acceptance:

- [ ] backup dan migration evidence tersedia;
- [ ] source parity frontend/backend terbukti;
- [ ] upload image production tampil di UtiPlay;
- [ ] CSP tidak memblokir preset/upload;
- [ ] full authenticated smoke lulus tanpa fallback palsu;
- [ ] authorization dua akun guru lulus;
- [ ] rollback siap dan terdokumentasi.

---

## 22. Acceptance Criteria Produk MVP

### 22.1 Produk

- [ ] Guru dapat membuat, mengedit, menerbitkan, mengarsipkan, dan memulihkan paket Word Pull.
- [ ] Draft boleh kurang dari 10 prompt.
- [ ] Published wajib minimal 10 prompt aktif valid.
- [ ] Paket sistem tetap tersedia bagi semua guru.
- [ ] Paket guru hanya tersedia bagi pemilik/admin.
- [ ] Buka di UtiPlay memilih paket yang benar.

### 22.2 Correctness

- [ ] Create match dan rematch mengulang authorization server-side.
- [ ] Deck tidak mengulang prompt sebelum habis.
- [ ] Ronde memakai visual dan jawaban dari snapshot versi yang sama.
- [ ] Rename/archive/edit tidak mengubah laporan dan match historis.
- [ ] Readiness API dan Portal Guru memakai aturan yang sama.
- [ ] Empty catalog tidak menghasilkan starter pack palsu.

### 22.3 Security dan privasi

- [ ] Answer dan aliases encrypted at rest.
- [ ] Answer tidak bocor sebelum resolution.
- [ ] Upload tervalidasi MIME/size/dimensi dan bebas path traversal.
- [ ] Guru tidak dapat mengakses paket/prompt guru lain melalui direct request.
- [ ] Client tidak dapat menentukan owner/status/system flag.
- [ ] Error/log tidak memuat answer mentah atau token handoff.

### 22.4 UX dan accessibility

- [ ] Authoring flow usable pada desktop dan mobile.
- [ ] Reorder dapat dilakukan tanpa drag.
- [ ] Status tidak bergantung pada warna.
- [ ] Error tampil pada row/field yang tepat.
- [ ] UtiPlay membedakan loading, empty, error, dan ready.
- [ ] CTA dan helper menggunakan bahasa Indonesia yang jelas.

### 22.5 Kompatibilitas

- [ ] Starter pack existing tetap playable.
- [ ] URL/API Word Pull existing tetap kompatibel.
- [ ] Host/controller/reconnect/report Word Pull tidak regresi.
- [ ] Pick A Side tidak berubah perilaku.
- [ ] One-time handoff tetap bekerja.

---

## 23. Verification Commands

Jalankan dari `C:\laragon\www\UtiClass` sesuai fase:

```powershell
php artisan route:list --path=utiplay
php artisan test --filter=Utiplay
php artisan test
vendor\bin\pint --test
npm run typecheck
npm run build
```

Jalankan dari `C:\laragon\www\UtiPlay`:

```powershell
npm run lint
npm run typecheck
npm test
npm run build
```

Verification tambahan:

- migration status sebelum/sesudah;
- storage public link;
- response headers/CSP pada environment target;
- network payload sebelum dan setelah resolution;
- browser smoke dengan minimal dua akun guru berbeda;
- host + dua controller untuk Word Pull;
- smoke Pick A Side setelah perubahan shared config.

Jumlah test/assertion, command aktual, dan failure baseline harus dicatat pada checkpoint. Jangan menulis “semua test lulus” tanpa output aktual.

---

## 24. Rollout dan Rollback

### 24.1 Urutan rollout

1. Backup database.
2. Deploy backend UtiClass dan migration.
3. Jalankan backfill/seed idempotent bila diperlukan.
4. Verifikasi Portal Guru dan media URL.
5. Deploy UtiPlay dengan contract/CSP baru.
6. Smoke starter pack.
7. Smoke satu paket guru upload.
8. Smoke dua akun untuk isolation.
9. Pantau error dan storage.

Backend harus kompatibel dengan frontend lama selama jeda deployment. Field katalog baru additive dan fallback palsu baru dihapus setelah API production siap.

### 24.2 Rollback

- Nonaktifkan menu/fitur authoring melalui rollback aplikasi bila terjadi blocker.
- Paket guru dapat di-set draft/archived tanpa menghapus data.
- Frontend UtiPlay dapat dikembalikan ke bundle sebelumnya selama API additive tetap kompatibel.
- Jangan rollback migration destruktif yang menghapus data upload/paket setelah digunakan.
- Jika media origin bermasalah, hentikan publish baru dan pertahankan starter pack sambil memperbaiki config/CSP.

---

## 25. Risiko dan Mitigasi

| Risiko | Dampak | Mitigasi |
|---|---|---|
| Direct POST memakai slug guru lain | Kebocoran konten/otorisasi | Policy + scoped lookup pada create/rematch + feature test |
| Gambar diedit tetapi jawaban lama | Pertanyaan salah/menyesatkan | Snapshot visual dan jawaban per round |
| Prompt berulang dua item saja | Permainan monoton | Deck/cursor server-side tanpa repeat sebelum habis |
| Hard delete pack historis | FK failure/histori rusak | Archive default; delete hanya unused draft |
| File lama terhapus | Gambar histori rusak | Immutable path + reference-aware retention |
| Preset diarahkan ke UtiClass | Broken image | Asset source resolver per origin |
| Upload berbahaya | Security/storage abuse | MIME, size, dimension, UUID path, SVG reject |
| `status` dan `is_active` drift | Katalog salah | Satu domain service mengubah keduanya |
| Form terlalu berat | Guru gagal authoring | Draft + prompt operations terpisah, row feedback |
| CSP berbeda antar hosting | Production-only broken image | Update Vercel + `.htaccess`, verify headers |
| Generic schema terlalu cepat | Kompleks/regresi Pick A Side | Shared shell/service, model game-specific |
| Deployment frontend/backend tidak cocok | Runtime error | Pair release + smoke + source parity evidence |

---

## 26. Current Repo Map

### UtiClass — existing

```text
app/Http/Controllers/Api/UtiplayMatchController.php
app/Http/Controllers/Api/UtiplaySessionController.php
app/Http/Requests/Utiplay/CreateMatchRequest.php
app/Models/UtiplayPack.php
app/Models/UtiplayPrompt.php
app/Models/UtiplayMatch.php
app/Models/UtiplayMatchRound.php
app/Policies/UtiplayMatchPolicy.php
app/Services/UtiplayMatchService.php
app/Services/UtiplayMatchPresenter.php
config/utiplay.php
database/migrations/2026_08_28_200000_create_utiplay_tables.php
database/migrations/2026_08_28_200100_create_utiplay_auth_handoffs_table.php
database/migrations/2026_08_28_200200_add_lifecycle_fields_to_utiplay_matches.php
database/seeders/UtiplayStarterPackSeeder.php
resources/js/layouts/AuthenticatedLayout.tsx
routes/api.php
routes/web.php
tests/Feature/UtiplayApiTest.php
```

### UtiClass — target baru

```text
app/Http/Controllers/UtiplayPackController.php
app/Http/Requests/Utiplay/StorePackRequest.php
app/Http/Requests/Utiplay/UpdatePackRequest.php
app/Http/Requests/Utiplay/StorePromptRequest.php
app/Http/Requests/Utiplay/UpdatePromptRequest.php
app/Policies/UtiplayPackPolicy.php
app/Services/UtiplayPackService.php
app/Services/UtiplayPromptMediaService.php
resources/js/Pages/Utiplay/Packs/Index.tsx
resources/js/Pages/Utiplay/Packs/Form.tsx
tests/Feature/UtiplayPackManagementTest.php
tests/Feature/UtiplayPackAuthorizationTest.php
```

### UtiPlay — existing/target perubahan

```text
src/App.tsx
src/services/utiplay-api.ts
src/games/word-pull/WordPullOnlineHost.tsx
src/games/word-pull/OnlinePromptImage.tsx
src/test/utiplay-api.test.ts
vercel.json
public/.htaccess
```

Nama test/file target dapat disesuaikan dengan konvensi repo, tetapi coverage kontrak PRD harus tetap ada.

---

## 27. Decision Log

| Tanggal | Keputusan | Alasan |
|---|---|---|
| 2026-09-24 | Backend tetap di UtiClass | Identitas, role, konten, match, dan report sudah otoritatif di UtiClass |
| 2026-09-24 | Portal authoring berada di UtiClass | Guru sudah login dan UI pembelajaran berada di sana |
| 2026-09-24 | MVP hanya Word Pull | Mengurangi scope dan menyelesaikan kebutuhan aktual lebih cepat |
| 2026-09-24 | UI memakai payung Bank Soal UtiPlay tetapi schema game-specific | Word Pull dan Pick A Side memiliki struktur pertanyaan berbeda |
| 2026-09-24 | Paket guru privat; paket sistem global read-only bagi guru | Menjaga ownership sambil menyediakan starter content |
| 2026-09-24 | Draft boleh kurang dari 10, publish minimal 10 | Guru perlu workflow penyusunan tanpa paket invalid muncul di runtime |
| 2026-09-24 | Archive menjadi default removal | Match existing memakai FK restrict dan histori harus dipertahankan |
| 2026-09-24 | Visual dan jawaban disnapshot per ronde | Mencegah mismatch setelah edit konten |
| 2026-09-24 | Deck tidak berulang sebelum habis | Ambang minimum paket tidak cukup tanpa algoritma runtime yang benar |
| 2026-09-24 | Preset dan upload memiliki source berbeda | Origin URL, CSP, ownership, dan cleanup berbeda |
| 2026-09-24 | Tidak ada backend/account UtiPlay kedua | Menghindari duplicate identity dan sinkronisasi lintas backend yang belum diperlukan |

---

## 28. Definition of Done

Fitur hanya boleh dinyatakan selesai jika seluruh kondisi berikut terpenuhi:

1. Seluruh acceptance criteria Bagian 22 dicentang berdasarkan bukti aktual.
2. Semua fase memiliki checkpoint dengan commit awal/akhir dan perubahan lokal yang dilindungi.
3. Migration baru, backfill, dan rollback strategy terdokumentasi.
4. Policy dan direct endpoint tests membuktikan isolation antar guru.
5. Readiness, deck, snapshot, archive, media resolver, dan file lifecycle teruji.
6. UtiClass targeted/full tests, Pint, typecheck, dan build lulus.
7. UtiPlay lint, typecheck, tests, dan build lulus.
8. Browser QA Portal Guru dan full Word Pull host/controller selesai.
9. Pick A Side dan starter Word Pull tidak regresi.
10. Production storage, CSP, CORS/session, dan media URL diverifikasi.
11. Dua akun guru membuktikan paket privat tidak bocor di production candidate.
12. Push, deployment, migration, dan production smoke dilaporkan sebagai bukti terpisah.
13. Tidak ada perubahan milik sesi lain yang ikut commit tanpa izin.

---

## 29. Template Handoff Setiap Fase

```md
# Bank Soal UtiPlay — Fase N Checkpoint

Tanggal:
Status: not started / in progress / blocked / complete

## Tujuan fase

## Repo dan branch
- UtiClass branch/commit awal:
- UtiClass branch/commit akhir:
- UtiPlay branch/commit awal:
- UtiPlay branch/commit akhir:
- Perubahan lokal yang sengaja tidak disentuh:

## Keputusan PRD yang dipakai

## Implementasi aktual
- Backend:
- Frontend UtiClass:
- Frontend UtiPlay:
- Migration/config/env:
- Perubahan API/protocol:

## Pengujian aktual
- Command:
- Hasil/jumlah test/assertion:
- Browser/viewport:
- Akun/role:
- Bukti security/network:

## Checklist acceptance fase
- [ ] ...

## Risiko atau defect tersisa

## Batas klaim
- Local source:
- Remote Git:
- Deployment:
- Production/browser:

## Langkah pertama fase berikutnya
```

---

## 30. Ringkasan Kontrak yang Tidak Boleh Hilang

Jika hanya satu bagian dapat dibaca saat handoff, pertahankan ini:

- UtiPlay tidak memakai backend atau akun kedua; UtiClass tetap authority.
- MVP authoring hanya untuk Word Pull.
- Paket guru privat; paket sistem global dan read-only bagi guru.
- Draft boleh kurang dari 10; publish wajib minimal 10 prompt aktif valid.
- UI katalog dan direct create/rematch memakai authorization scope yang sama.
- Paket historis diarsipkan, bukan dihapus.
- Visual, alt text, answer, aliases, dan nama pack disnapshot untuk histori.
- Deck tidak mengulang prompt sebelum seluruh prompt aktif habis.
- Preset dilayani dari origin UtiPlay; upload dari origin UtiClass.
- File upload memakai path immutable dan tidak dihapus selama masih direferensikan.
- Empty catalog tidak boleh menghasilkan fallback pack palsu.
- Word Pull existing, one-time handoff, dan Pick A Side tidak boleh regresi.
- Implementasi dilakukan per fase dengan test dan checkpoint yang dapat diverifikasi.
