# Definisi Metrik — Sales Copilot (F2)

Tanpa dokumen ini, dua orang bisa membaca angka yang sama dengan arti berbeda
(docs/05 bagian 8). Implementasi: `lib/semantic/metrics.ts`. Semua SQL ditulis
tangan dan bisa direview. Data sintetis, `SEED=20260904`.

**Periode default:** 90 hari terakhir (`tanggalOffset(-90)`..`-1`) kecuali disebut
lain. String periode ("kuartal ini", "bulan lalu", "3 bulan terakhir", "2026-08",
"tahun lalu") diterjemahkan `terjemahkanPeriode()` ke tanggal absolut; hasil
memuat `cakupan: {dari, sampai}`.

**"Hari ini"** dipatok `2026-09-04` (data sandbox berhenti di sana).

---

| # | Fungsi | Definisi | Sumber | Parameter |
|---|---|---|---|---|
| 1 | `load_factor` | `SUM(kursi_terjual) / SUM(kapasitas) × 100`, diagregasi per `group_by` (route/layanan/bulan/hari). Bukan rata-rata dari rata-rata. | `trip_performance` | `route?`, `layanan?`, `dari`/`sampai` atau `periode`, `group_by` |
| 2 | `pendapatan` | `SUM(pendapatan)` (dimensi route/layanan/cabang) dari `trip_performance`; untuk dimensi `kanal` → `SUM(total_bayar)` booking berstatus dibayar/selesai. | `trip_performance` / `bookings` | `dimensi`, `periode` |
| 3 | `pendapatan_per_km` | `SUM(pendapatan) / SUM(km_tempuh)` vs `SUM(biaya_operasi) / SUM(km_tempuh)`. `margin_per_km` = selisihnya. Rute dengan `margin_per_km < 0` = rugi per km. | `trip_performance` | `route?`, `periode` |
| 4 | `margin_operasi_rute` | `SUM(pendapatan) − SUM(biaya_operasi)`; `margin_persen` = margin ÷ pendapatan × 100. Urut menaik (paling rugi dulu). | `trip_performance` | `route?`, `periode` |
| 5 | `rute_underperform` | Rute dengan **load factor < `threshold_lf`** (default 45) selama periode (default 60 hari terakhir). Ini "jawaban" yang ditanam di seed: **tepat 5 rute**. | `trip_performance` | `threshold_lf`, `dari`/`sampai` |
| 6 | `tren` | Deret waktu satu metrik (load_factor/pendapatan/margin) per bucket (hari/minggu/bulan), default bulan, jendela 180 hari. | `trip_performance` | `metrik`, `route?`/`layanan?`, `granularitas`, `periode` |
| 7 | `perbandingan_periode` | Nilai metrik pada `periode_a` vs `periode_b`, plus selisih absolut & persen relatif terhadap A. | `trip_performance` | `metrik`, `periode_a`, `periode_b` |
| 8 | `top_n` | `n` teratas/terbawah (`arah`) menurut metrik, dimensi route/layanan. | `trip_performance` | `metrik`, `dimensi`, `n`, `arah`, `periode` |
| 9 | `kontribusi_kanal` | Jumlah transaksi & pendapatan per kanal booking + `persen_transaksi` dari total. Kanal: apps/web/loket/agen/access_kai. | `bookings` (`created_at`) | `periode` |
| 10 | `performa_agen` | Per agen: `SUM(jumlah_transaksi)`, `SUM(total_penjualan)`, `SUM(selisih)`, `hari_selisih` (jumlah hari selisih≠0). | `daily_sales` | `agent?`, `cabang?`, `periode` |
| 11 | `utilisasi_armada` | `hari_terpakai / (unit × hari_periode) × 100`. `hari_terpakai` = total hari penugasan (`fleet_assignments`) yang beririsan dengan periode. | `fleet`, `fleet_assignments` | `tipe?`, `cabang?`, `periode` |
| 12 | `pipeline_charter` | Jumlah & `SUM(estimasi_tengah)` penawaran per status (draft/terkirim/negosiasi/menang/kalah), jendela 180 hari. | `charter_quotes` (`created_at`) | `status?`, `periode` |
| 13 | `rasio_menang_charter` | `menang / (menang + kalah) × 100`. Hanya menghitung penawaran yang sudah selesai (menang/kalah). Jendela 365 hari. | `charter_quotes` | `tipe?`, `periode` |
| 14 | `musiman` | Metrik per bulan dipecah `hari_kerja` vs `akhir_pekan` untuk satu tahun. Menyorot pola akhir pekan / libur. | `trip_performance` | `metrik`, `route?`, `tahun` |
| 15 | `anomali` | Rute yang `|lf_rata − mean| > sensitivitas × stddev` (default sensitivitas 2) atas load factor rata-rata harian. Output menyertakan `deviasi` (z-score). | `trip_performance` | `metrik`, `periode`, `sensitivitas` |

---

## Konvensi

- **Pendapatan vs total_bayar.** `trip_performance.pendapatan` adalah pendapatan
  operasional per perjalanan (jalur reguler). `bookings.total_bayar` adalah nilai
  transaksi tiket. Untuk analisis kanal dipakai `bookings`; untuk performa rute
  dipakai `trip_performance`.
- **Biaya operasi** di seed adalah rasio terhadap pendapatan (struktur industri
  yang masuk akal, bukan biaya DAMRI riil — Batasan Kejujuran no. 3).
- **Load factor** disimpan juga per baris di `trip_performance.load_factor`, tapi
  metrik agregat SELALU dihitung ulang dari `SUM(kursi_terjual)/SUM(kapasitas)`
  agar tidak bias "rata-rata dari rata-rata".
- **Selisih setoran.** `daily_sales.selisih = setoran − total_penjualan`. Negatif
  = agen kurang setor. 6 agen ditanam dengan selisih berulang.

## Fallback SQL (Tingkat 3)

Dipakai HANYA kalau 15 fungsi tidak cukup. `lib/semantic/query.ts`:
koneksi read-only sungguhan (`readonly: true` + `PRAGMA query_only`), tokenizer
membuang string/komentar sebelum cek kata kunci tulis, satu pernyataan
SELECT/WITH, whitelist tabel (`conversations`/`messages`/`tool_calls` tidak pernah
bisa), `LIMIT 500` disisipkan bila tak ada. SQL selalu ditampilkan ke pengguna
dan minta persetujuan sebelum dijalankan. **Utang diakui:** batas waktu 5 detik
belum ditegakkan runtime (butuh worker thread); dimitigasi ukuran dataset + LIMIT.
