# 01 — Model Penilaian

## 1. Kenapa Bukan Sekadar "Benar Dibagi Jumlah Soal"

Karena tiap soal boleh punya **bobot** berbeda. Soal essay panjang tidak adil
bila dihargai sama dengan soal pilihan ganda satu baris. Karena itu sistem
menghitung nilai berdasarkan **bobot yang terkumpul**, bukan **jumlah soal yang
benar**.

Bayangkan tiap soal sebagai wadah berisi poin sebanyak bobotnya. Nilai akhir
adalah berapa persen dari seluruh poin yang berhasil diambil siswa.

## 2. Rumus Inti

```
                 Σ ( bobot_i  ×  fraksi_i )
    nilai  =  ─────────────────────────────────  ×  100
                        Σ bobot_i
```

Untuk setiap soal `i` dalam tryout:

- `bobot_i` — bobot soal tersebut (bilangan bulat positif).
- `fraksi_i` — porsi kebenaran jawaban siswa, bernilai `0` sampai `1`.

Implementasinya di `jawaban.routes.ts`:

```ts
let totalBobot = 0
let bobotBenar = 0
let totalBenar = 0

for (const s of soalList) {
  totalBobot += s.bobot
  if (s.tipe === 'essay') continue          // dinilai manual
  const fraction = gradeAnswer(s, answersMap.get(s.id) ?? null)
  bobotBenar += s.bobot * fraction
  if (fraction === 1) totalBenar += 1
}

const nilai = totalBobot > 0 ? (bobotBenar / totalBobot) * 100 : 0
```

## 3. Arti Kolom `bobot`

| Aspek | Ketentuan |
| --- | --- |
| Tipe data | `INTEGER NOT NULL DEFAULT 1` pada tabel `soal` |
| Validasi API | `z.number().int().positive()` — wajib bulat dan lebih besar dari nol |
| Diisi di mana | Editor soal (Guru & Admin Soal), kolom **Bobot**, minimum `1` |
| Impor Word | Baris `Bobot: 5` di dalam blok soal pada berkas `.docx` |
| Bila kosong | Otomatis `1` |

Yang penting dipahami: **bobot bersifat relatif, bukan absolut.** Bobot `10`
tidak berarti "10 poin dari 100". Artinya hanya "sepuluh kali lebih berharga
daripada soal berbobot 1 di tryout yang sama". Karena penyebutnya adalah total
bobot seluruh soal, menaikkan bobot satu soal otomatis memperkecil porsi semua
soal lain.

## 4. Contoh Perhitungan Lengkap

Sebuah tryout berisi 5 soal:

| No | Tipe | Bobot | Jawaban Siswa | Fraksi | Poin Diperoleh |
| --- | --- | --- | --- | --- | --- |
| 1 | Pilihan ganda | 1 | Benar | `1` | 1 × 1 = **1** |
| 2 | Pilihan ganda | 1 | Salah | `0` | 1 × 0 = **0** |
| 3 | Isian singkat | 2 | Benar | `1` | 2 × 1 = **2** |
| 4 | Menjodohkan (4 pasang, benar 3) | 3 | Sebagian | `0,75` | 3 × 0,75 = **2,25** |
| 5 | Tabel pernyataan (parsial) | 3 | Sebagian | `0,5` | 3 × 0,5 = **1,5** |
| | | **Σ 10** | | | **Σ 6,75** |

```
nilai = 6,75 / 10 × 100 = 67,50
```

Perhatikan bahwa siswa hanya menjawab **2 soal secara sepenuhnya benar** dari 5
soal, tetapi nilainya 67,50 — bukan 40. Nilai parsial pada soal nomor 4 dan 5
ikut diperhitungkan.

## 5. `nilai` vs `total_benar` — Dua Angka yang Berbeda

Tabel `hasil` menyimpan dua besaran yang mudah tertukar:

| Kolom | Arti | Pada contoh di atas |
| --- | --- | --- |
| `nilai` | Persentase bobot terkumpul (0–100) | `67.50` |
| `total_benar` | Cacah soal dengan fraksi **tepat 1** | `2` |
| `total_soal` | Cacah seluruh soal dalam tryout | `5` |

Konsekuensinya: pada halaman hasil, siswa bisa melihat **"Benar 2/5"** bersanding
dengan **nilai 67,50**. Keduanya benar — yang satu menghitung soal, yang lain
menghitung bobot. Soal bernilai parsial tidak pernah dihitung sebagai "benar",
tetapi tetap menyumbang poin.

## 6. Kapan Nilai Dihitung

Hanya sekali, di server, pada endpoint:

```
POST /sesi/:sesiId/selesai
```

Alurnya:

1. Kunci jawaban dibaca langsung dari database service jenjang yang bersangkutan
   (`loadSoalForScoring`) — kunci **tidak pernah dikirim ke browser siswa** saat
   ujian berlangsung.
2. Seluruh jawaban tersimpan diambil dari tabel `jawaban`.
3. Nilai dihitung, lalu disimpan ke tabel `hasil` di dalam satu transaksi
   bersama pembaruan status sesi.
4. Sebuah catatan audit `EXAM_SESSION_SUBMIT` dibuat berisi nilai akhir.

**Tidak ada endpoint hitung ulang.** Bila kunci jawaban sebuah soal diperbaiki
setelah siswa mengerjakan, nilai yang sudah tersimpan tidak ikut berubah. Nilai
lama harus dikoreksi secara manual di basis data bila memang diperlukan.

## 7. Peringkat pada Papan Peringkat

Peringkat tidak semata-mata mengikuti nilai. Urutannya:

1. `nilai` tertinggi lebih dulu;
2. bila seri — **waktu pengerjaan tersingkat** menang
   (`selesai_at − mulai_at`);
3. bila masih seri — yang lebih dahulu dihitung (`dihitung_at`) menang.

Hanya sesi yang benar-benar diselesaikan (`selesai_at IS NOT NULL`) yang masuk
papan peringkat.
