# 02 — Aturan Penilaian per Tipe Soal

Seluruh penilaian otomatis dipusatkan pada satu fungsi, `gradeAnswer()`, yang
mengembalikan **fraksi** antara `0` dan `1`. Fraksi inilah yang kemudian
dikalikan dengan bobot soal.

## Ringkasan

| Tipe Soal | Fraksi yang Mungkin | Nilai Parsial |
| --- | --- | --- |
| `pilihan_ganda` | `0` atau `1` | Tidak |
| `pg_kompleks` | `0` atau `1` | Tidak |
| `isian_singkat` | `0` atau `1` | Tidak |
| `menjodohkan` | `0` … `1` | **Ya** |
| `benar_salah_tabel` | `0` … `1` | **Ya** (mode PARSIAL) |
| `essay` | selalu `0` | — dinilai manual |

---

## 1. Pilihan Ganda (`pilihan_ganda`)

Benar bila `opsi_id` yang dipilih siswa sama persis dengan opsi yang ditandai
`is_benar` di basis data.

```ts
const correct = soal.opsi.find((o) => o.is_benar)
return correct && ans?.opsi_id === correct.id ? 1 : 0
```

Pengacakan urutan opsi tidak memengaruhi penilaian, karena yang dibandingkan
adalah `id` opsi, bukan huruf A/B/C/D yang tampil di layar.

## 2. Pilihan Ganda Kompleks (`pg_kompleks`)

Siswa memilih beberapa opsi sekaligus. Penilaiannya **seluruhnya benar atau
tidak sama sekali** — himpunan pilihan siswa harus persis sama dengan himpunan
kunci.

Memilih 3 dari 4 kunci yang benar tetap bernilai `0`. Begitu pula memilih 4 kunci
benar ditambah 1 opsi salah.

## 3. Isian Singkat (`isian_singkat`)

Jawaban siswa dinormalisasi lebih dulu sebelum dibandingkan:

```ts
function normalizeText(s: string): string {
  return s.trim().toLowerCase().replace(/\s+/g, ' ')
}
```

Artinya perbedaan berikut **dimaafkan**: huruf besar/kecil, spasi di awal dan
akhir, serta spasi ganda di tengah kalimat.

Guru dapat menuliskan **beberapa kunci alternatif**, satu per baris, pada kolom
kunci jawaban. Jawaban dinilai benar bila cocok dengan salah satu baris.

> Contoh: kunci `Jakarta` dan `DKI Jakarta` ditulis di dua baris terpisah, maka
> keduanya diterima. Namun `Jkt` tetap salah — pencocokan bersifat tepat setelah
> normalisasi, bukan pencocokan kemiripan.

Jawaban kosong selalu bernilai `0`.

## 4. Menjodohkan (`menjodohkan`)

Tipe pertama yang memberi **nilai parsial**. Fraksi dihitung sebagai proporsi
pasangan yang benar:

```
fraksi = jumlah_pasangan_benar / jumlah_seluruh_pasangan
```

Benar 3 dari 4 pasangan menghasilkan fraksi `0,75`. Pada soal berbobot 4, siswa
memperoleh 3 poin. Perbandingan tiap pasangan juga melewati normalisasi teks
yang sama dengan isian singkat.

## 5. Tabel Pernyataan Benar/Salah (`benar_salah_tabel`)

Tipe paling rumit, karena punya **bobot bertingkat**: soalnya punya bobot, dan
tiap baris pernyataan di dalamnya juga punya bobot sendiri.

### Dua Mode Penilaian

Mode diatur guru melalui `scoring_mode` pada konfigurasi soal.

**Mode `PARSIAL` (bawaan)** — bobot antar baris dinormalisasi:

```
fraksi = Σ bobot_baris_yang_benar / Σ seluruh_bobot_baris
```

**Mode `FULL`** — semua baris harus benar:

```
fraksi = 1 bila seluruh baris benar, selain itu 0
```

### Bobot Baris Bersifat Relatif di Dalam Soal

Ini bagian yang paling sering disalahpahami. Bobot baris **tidak menambah** bobot
soal; ia hanya menentukan porsi antar baris.

> **Contoh.** Sebuah soal berbobot **4** berisi 3 baris pernyataan dengan bobot
> baris 1, 1, dan 2 (total bobot baris = 4). Siswa menjawab benar baris ke-1 dan
> ke-3.
>
> ```
> fraksi = (1 + 2) / (1 + 1 + 2) = 3 / 4 = 0,75
> poin   = bobot_soal × fraksi = 4 × 0,75 = 3
> ```
>
> Seandainya bobot soal diubah menjadi 10 sementara bobot baris tetap, poinnya
> menjadi `10 × 0,75 = 7,5`. Total bobot baris berapa pun **tidak** mengubah
> kontribusi soal terhadap penyebut — yang masuk penyebut hanyalah bobot soal.

### Label Pilihan Kustom (TRN-51)

Sejak TRN-51 label tiap baris tidak harus "Benar/Salah" — boleh diganti menjadi
`Fact/Opinion`, `True/False`, `Sesuai/Tidak Sesuai`, dan seterusnya. Perbandingan
jawaban dilakukan melalui `canonicalPilihan()` yang mengabaikan beda huruf
besar/kecil dan spasi berlebih.

Bila jawaban siswa maupun kunci sama-sama tidak cocok dengan label baris (data
hasil suntingan manual), sistem membandingkan keduanya secara langsung agar soal
yang konsisten dengan dirinya sendiri tetap dapat dinilai.

## 6. Essay (`essay`)

Fraksi selalu `0`. Kode secara eksplisit melewatinya:

```ts
if (s.tipe === 'essay') continue   // graded manually
```

Halaman hasil siswa menampilkan keterangan *"Essay dinilai secara manual oleh
guru"*. Namun perlu diperhatikan bahwa **bobot soal essay tetap masuk ke
penyebut** — implikasinya dibahas tuntas di
[03-temuan-dan-batasan.md](./03-temuan-dan-batasan.md).

---

## Soal yang Dilewati

Soal tanpa jawaban diperlakukan sama dengan jawaban salah: fraksi `0`.

Tidak ada **penalti negatif** dan tidak ada koreksi tebakan. Menebak selalu lebih
menguntungkan daripada mengosongkan jawaban, dan siswa tidak pernah dirugikan
karena mencoba.
