Dokumentasi teknis · v2026.11

Cara ChatZAP bekerja, dari guardrail sampai basis data

Referensi lengkap untuk tim teknis, peninjau, dan pengguna yang ingin tahu batas sistem. Isi halaman ini dibaca dari kode sumber yang berjalan di repositori ini: nama fungsi, angka batas, status respons, dan konsekuensi keamanannya. Tidak ada klaim pemasaran di sini.

Coba ChatZAPReferensi APIZero-Answer Policy aktif
11 bagian±20 menit bacaDiperbarui 14 Nov 2026

Ringkasan & prinsip

CFL · SymPykebenaran
LLMbahasa
dua lapis kebenaran

ChatZAP memisahkan dua pertanyaan yang sering dicampur sistem tutor AI: apakah langkah ini benar secara matematis dan bagaimana mengatakannya dengan bahasa yang manusiawi. Pertanyaan pertama dijawab mesin simbolik deterministik. Pertanyaan kedua dijawab model bahasa.

LapisPenentuBoleh melakukanTidak boleh melakukan
KebenaranSymPy + mesin CFL (backend/cfl/)Mengurai langkah, membandingkan ekuivalensi, melokalisasi fault pertama, membuat counter-example, memberi status terverifikasi.Menyerahkan penilaian validitas ke LLM atau ke tebakan probabilistik.
BahasaModel bahasa (Gemini, NVIDIA NIM, BazaarLink, OpenAI, Anthropic, Mistral, atau model lokal)Merumuskan pertanyaan Sokratik, empati, klarifikasi, dan penjelasan konsep.Menyatakan jawaban akhir, mengubah vonis CFL, atau mengarang rumus yang tidak ada.

Zero-Answer Policy adalah invariant produk: sistem tidak pernah mengeluarkan solusi akhir soal latihan. Yang dikirim adalah pertanyaan, petunjuk arah, dan diagnosis pada langkah yang sedang dikerjakan. Penegakannya berlapis: instruksi prompt, sanitasi keluaran di backend, dan sanitasi terakhir di finalizeModelReply() sebelum teks menyentuh peramban.

  • Determinisme lebih dulu. Bila mesin tidak dapat memutuskan, statusnya UNDECIDABLE_STEP, bukan vonis salah.
  • Fail-closed untuk rahasia. Endpoint harness menolak permintaan tanpa secret yang cocok saat secret terpasang; di production tanpa secret, endpoint menolak dengan 503.
  • Isolasi pengguna. Memori episodik disimpan per user_key hash, dan referensi hasil promosi percakapan hanya muncul untuk pemiliknya.
  • Batas diakui. Setiap bagian di bawah menyertakan limitasi yang benar-benar ada di kode, bukan hanya kemampuan.
Catatan:Halaman ini adalah dokumentasi, bukan janji SLA. Angka seperti jumlah memori, ukuran korpus, dan jumlah pola adalah snapshot saat dokumen ditulis dan akan berubah ketika data bertambah.

Arsitektur

alur browser → SymPy

Ada dua proses utama. Next.js 14 menjalankan UI dan route handler di port 3000. FastAPI menjalankan CFL, harness, korpus, dan relay papan kolaborasi di port 8000. Keduanya berbicara lewat HTTP; rute chat di Next bertindak sebagai orchestrator yang memanggil backend dengan secret server-ke-server.

Alur satu giliran chat (mode streaming SSE)
Peramban (app/chat/page.tsx)
  │  POST /api/chat  { message, stream: true, targetVariable: "x", ... }
  ▼
Next.js route handler  (app/api/chat/route.ts)
  ├─ 1. parse JSON ......................... 400 bila body kosong / bukan JSON
  ├─ 2. cek strike & suspend ............... registry server, bukan nilai dari klien
  ├─ 3. POST /api/v1/harness/context ....... timeout 1.200 ms
  │      └─ FastAPI → memori episodik + korpus + rumus geometri
  ├─ 4. pilih provider LLM ................. Gemini / NVIDIA NIM / BazaarLink /
  │      (hanya untuk bahasa)               OpenAI / Anthropic / Mistral / lokal
  ├─ 5. finalizeModelReply() ............... cleanModelResponse + sanitizeZeroAnswer
  └─ 6. kirim balasan ...................... JSON utuh ATAU SSE:
         event: meta → delta* → done
  ▼
Peramban
  │  (fire-and-forget) POST /api/v1/harness/learn ... timeout 1.500 ms
  ▼
FastAPI  backend/main.py
  └─ ZAPAgentHarness.learn_from_conversation()
       redaksi PII → ekstraksi langkah → gerbang CFL SymPy
       → upsert memori episodik → promosi korpus (hanya ALL_STEPS_VALID)
KomponenLokasi berkasTanggung jawab
UI chat & Smartboardfrontend/app/chat/page.tsxMengirim pesan, menampilkan balasan bertahap, papan geometri, dan status diagnosis.
Route chatfrontend/app/api/chat/route.tsOrkestrasi: validasi, deteksi jailbreak, ambil konteks harness, pilih provider, sanitasi, SSE/JSON.
Guardrail keluaranfrontend/lib/zero-answer.tsPintu keluar terakhir: bersihkan artefak penalaran lalu sensor deklarasi jawaban.
Gateway FastAPIbackend/main.pyEndpoint diagnose/verify/harness, CORS, secret gate, dan WebSocket papan kolaborasi.
Parser amanbackend/cfl/parser.pyWhitelist-first, batas eksponen, parsing SymPy/LaTeX.
Mesin CFLbackend/cfl/cfl_engine.pyMembandingkan langkah berurutan, melokalisasi fault, menandai langkah tak dapat diputuskan.
Klasifikasi & counter-examplebackend/cfl/classifier.py, counter_example.pyMenamai miskonsepsi dan mencari substitusi integer kecil yang menunjukkan kontradiksi.
Harness memoribackend/data_engine/agent_harness.pyMemori episodik, isolasi pengguna, retrieval, profil belajar, umpan balik, promosi korpus.
Korpus & harvesterbackend/data_engine/store.py, build_corpus.py, harvester.pyPenyimpanan SQLite + mirror JSONL, pembangunan korpus, panen sumber internet terverifikasi.
Vault & sesi akunfrontend/lib/vault-crypto.ts, vault-session.ts, account-session.tsEnkripsi sisi klien, kunci sesi per tab, token sesi HMAC, sinkronisasi envelope.
Sinkronisasi cloudfrontend/app/api/user/sync/route.tsMenerima envelope terenkripsi dan metadata identitas; menolak plaintext yang tidak berhak.
Peringatan:Batas arsitektur yang penting: LLM tidak pernah menjadi penentu benar-salah. Semua penilaian matematis berasal dari SymPy melalui CFL. Bila Anda mengganti model bahasa, kebenaran matematis tidak berubah - yang berubah hanya kualitas kalimat Sokratiknya.

Zero-Answer Policy

guardzero-answer
x = 4
diblokir
guardrail menahan jawaban

Ada dua implementasi guardrail yang saling melengkapi: satu di backend Python untuk prompt yang dihasilkan server, satu di frontend TypeScript untuk semua balasan model sebelum dikirim ke klien. Keduanya bekerja dengan pola deklaratif, bukan dengan memahami makna kalimat.

3.1 Guardrail backend: RobustZeroAnswerGuardrail

backend/cfl/guardrail.py mendefinisikan DECLARATIVE_ANSWER_PATTERNS: 11 regex yang menangkap deklarasi jawaban akhir, simpulan variabel, nilai geometri, hasil aritmetika, satuan fisika, pH/pOH, output program, dan kompleksitas Big-O. Setiap kecocokan diganti dengan Bagaimanakah menurut analisismu?. Parameter opsional final_solution hanya menyensor bila nilai itu muncul setelah penanda kunci atau nilai akhir, sehingga kalimat instruksional seperti jika x = 2 tetap utuh.

backend/cfl/guardrail.py - sanitize (diringkas)
# backend/cfl/guardrail.py
class RobustZeroAnswerGuardrail:
    DECLARATIVE_ANSWER_PATTERNS = [ ... ]   # 11 regex: deklaratif + domain non-matematika

    @classmethod
    def sanitize(cls, text: str, final_solution: str | None = None) -> str:
        sanitized = text
        for pattern in cls.DECLARATIVE_ANSWER_PATTERNS:
            sanitized = re.sub(pattern, "Bagaimanakah menurut analisismu?",
                               sanitized, flags=re.IGNORECASE)

        if final_solution and str(final_solution).strip():
            sol_escaped = re.escape(str(final_solution).strip())
            target_pattern = (
                rf"(?:kunci|nilai\s+akhir)\s*(?:adalah|=|:)\s*"
                rf"(?:[a-zA-Z]\s*=\s*)?{sol_escaped}\b"
            )
            sanitized = re.sub(target_pattern, "kunci adalah [DIRAHASIAKAN]",
                               sanitized, flags=re.IGNORECASE)
        return sanitized

/api/v1/diagnose memakai guardrail ini untuk membersihkan socratic_prompt, lalu menghitung guardrail_status.final_answer_suppressed dengan memeriksa ulang teks yang benar-benar dikirim terhadap pola yang sama. Status itu bukan klaim kosong: ia dihitung dari hasil sanitasi nyata.

3.2 Guardrail frontend: delimiter-aware

Temuan audit 13 November 2026 menunjukkan versi lama hanya mencocokkan teks mentah, sehingga jawaban dalam matematika inline/display lolos. Perbaikannya ada di frontend/lib/zero-answer.ts: seluruh span matematika dinormalisasi lebih dulu, lalu pola lama dijalankan.

  • $$...$$, \[...\], \(...\), dan $...$ diperiksa isinya; span yang hanya berisi variabel = konstanta diganti placeholder Sokratik.
  • Baris penuh x = 4 tanpa delimiter juga disensor oleh pola nomor 11.
  • Bila targetVariable sesi diketahui (misalnya x), bentuk x = 4 di tengah kalimat ikut disensor tanpa merusak langkah seperti 2x = 8 atau x = 8/2.
  • Balasan dari provider model melewati finalizeModelReply(): bersihkan blok <think>, sanitasi, lalu pakai fallback bila teks yang tersisa terlalu pendek.
frontend/lib/zero-answer.ts - urutan sanitasi
// frontend/lib/zero-answer.ts
export function sanitizeZeroAnswer(text: string, options?: ZeroAnswerOptions): string {
  if (!text) return "";
  // 1. Seluruh span matematika dinormalisasi lebih dulu:
  //    $$…$$, \[…\], \(…\), lalu $…$
  let result = sanitizeMathSpans(text);
  // 2. Bila variabel target sesi diketahui, sensor "x = 4" di tengah kalimat.
  const targetVariable = options?.targetVariable?.trim();
  if (targetVariable) {
    result = redactTargetAssignments(result, targetVariable);
  }
  // 3. Baru jalankan 11 pola deklarasi jawaban.
  for (const item of ZERO_ANSWER_PATTERNS) {
    result = result.replace(item.regex, item.replacement);
  }
  return result;
}

export function finalizeModelReply(text, fallback, options) {
  const cleaned = cleanModelResponse(text || "");
  const guarded = sanitizeZeroAnswer(cleaned, options).trim();
  // Balasan yang seluruhnya tersensor tidak boleh dikirim kosong.
  if (guarded.replace(/[^A-Za-z0-9\u00C0-\u024F]/g, "").length < 8) {
    return fallback;
  }
  return guarded;
}

3.3 Contoh sebelum dan sesudah

SebelumSesudah (pola yang berlaku)Catatan
$$x = 4$$[Berapakah nilainya menurut hitunganmu?]Span matematika yang hanya berisi jawaban jadi.
x = 4[Berapakah nilainya menurut hitunganmu?]Satu baris penuh tanpa delimiter.
coba hitung x = 4 dulucoba hitung [Berapakah nilainya menurut hitunganmu?] duluHanya bila targetVariable = x.
Jadi x = 4Coba tentukan nilai variabel tersebut berdasarkan penalaranmuPola kesimpulan (jadi/maka/sehingga).
jawabannya adalah 4Bagaimanakah menurut analisismu?Deklarasi jawaban eksplisit.
2x = 8tetap 2x = 8Langkah penalaran, bukan kunci jawaban.
Peringatan:Limitasi jujur. (1) Sensor di tengah kalimat membutuhkan targetVariable yang diketahui dan hanya menerima satu huruf. (2) final_solution backend hanya aktif bila nilainya didahului penanda kunci/nilai akhir. (3) Semua ini pencocokan regex: parafrase baru, bahasa lain di luar pola, atau jawaban yang dipecah dengan cara tak terduga bisa lolos. (4) Pada mode streaming, potongan ditahan sampai batas kalimat dan sekitar 48 karakter terakhir, lalu teks final pada event done menjadi sumber kebenaran; tetap saja tidak ada jaminan matematis bahwa setiap formulasi akan tertangkap.

Mesin CFL (Cognitive Fault Localization)

parsewhitelist
compareekuivalensi
faultlangkah 2
pipeline verifikasi

CFL menerima daftar baris aljabar, mengurainya satu per satu, lalu membandingkan pasangan baris berurutan. Tujuannya bukan menilai seluruh jawaban, tetapi menemukan titik patah logika pertama dan menjelaskannya secara deterministik.

4.1 Whitelist-first parsing

parse_expr SymPy memanggil eval() di internalnya. Karena itu urutannya wajib: validasi whitelist, batas eksponen, baru parsing. Membalik urutan ini berarti mengeksekusi parser pada input yang belum dipercaya.

  1. Jalur teks biasa: _is_plain_algebra() menolak panjang > 200 karakter, garis bawah, kutip, backtick, titik-koma, dan backslash. Hanya karakter aljabar standar yang lolos; setiap identifier harus ada di whitelist.
  2. Jalur LaTeX: _is_safe_latex() mewajibkan backslash, menolak underscore/kutip/titik-koma/dolar, dan hanya mengizinkan perintah frac, sqrt, cdot, times, sin, cos, tan, log, exp, pm.
  3. Batas eksponen: eksponen numerik > 100 ditolak (termasuk bentuk kurung dan kurung kurawal), indeks akar \sqrt[n] dengan n > 100 ditolak, dan pangkat bertingkat seperti (9^9)^9 ditolak.
  4. Baru kemudian parse_expr() atau parse_latex() dijalankan. AssertionError selalu di-raise ulang sebelum except Exception generik (aturan Anti-Masking).

Variabel yang diizinkan: x, y, z, a, b, c, d, m, n, t. Fungsi: sin, cos, tan, sqrt, log, exp. Konstanta: pi, E.

backend/cfl/parser.py - parse_single_side
# backend/cfl/parser.py
ALLOWED_FUNCS = {"sin", "cos", "tan", "sqrt", "log", "exp"}
ALLOWED_VARS  = {"x", "y", "z", "a", "b", "c", "d", "m", "n", "t"}
ALLOWED_CONSTS = {"pi", "E"}
ALLOWED_LATEX_CMDS = {"frac", "sqrt", "cdot", "times", "sin", "cos",
                      "tan", "log", "exp", "pm"}

def parse_single_side(side_str: str) -> sp.Expr:
    cleaned = side_str.strip()
    if not cleaned:
        raise ValueError("Ekspresi matematika tidak boleh kosong.")

    # 1. Whitelist DULU, baru parse.
    if _is_plain_algebra(cleaned):
        if _has_dangerous_exponents(cleaned):
            raise ValueError("Eksponen melebihi batas aman (maks 100) ...")
        try:
            return parse_expr(cleaned, transformations=TRANSFORMATIONS)
        except AssertionError:
            raise                      # Anti-Masking: jangan ditelan except generik
        except Exception as err:
            raise ValueError(f"Gagal mengurai aljabar '{side_str}': {err}")

    if "\\" in cleaned and _is_safe_latex(cleaned):
        if _has_dangerous_exponents(cleaned):
            raise ValueError("Eksponen/indeks akar melebihi batas aman ...")
        try:
            return parse_latex(cleaned)
        except AssertionError:
            raise
        except Exception as err:
            raise ValueError(f"Gagal mengurai LaTeX '{side_str}': {err}")

    # 3. Di luar whitelist = ditolak, bukan ditebak.
    raise ValueError(f"Ekspresi tidak valid atau di luar whitelist aljabar: '{side_str}'")

4.2 Perbandingan langkah & batas derajat

Untuk dua persamaan, CFL memeriksa apakah ruas kiri identik; bila ya, selisih ruas kanan harus nol. Bila tidak, himpunan solusi real kedua persamaan dibandingkan lewat solveset. Derajat polinomial di atas MAX_SOLVESET_DEGREE = 12 tidak dipaksa: hasilnya None dan API melaporkan UNDECIDABLE_STEP. Angka 12 dipilih karenax^100 + x^99 = 1 terhadap x^100 = 1 - x^99 tidak selesai dalam 15 detik dan dapat memblokir worker.

backend/cfl/cfl_engine.py - inti perbandingan
# backend/cfl/cfl_engine.py
MAX_SOLVESET_DEGREE = 12   # x^100 + x^99 = 1 sempat > 15 detik, jadi dibatasi

def steps_equivalent(prev, nxt, var=sp.Symbol("x")) -> bool | None:
    if isinstance(prev, sp.Eq) and isinstance(nxt, sp.Eq):
        if prev.lhs == nxt.lhs:
            if sp.simplify(nxt.rhs - prev.rhs) == 0:
                return True
        degrees = (_polynomial_degree(prev.lhs - prev.rhs, var),
                   _polynomial_degree(nxt.lhs - nxt.rhs, var))
        if any(d is not None and d > MAX_SOLVESET_DEGREE for d in degrees):
            return None                       # UNDECIDABLE_STEP, bukan vonis
        s1 = sp.solveset(prev.lhs - prev.rhs, var, domain=sp.S.Reals)
        s2 = sp.solveset(nxt.lhs - nxt.rhs, var, domain=sp.S.Reals)
        if isinstance(s1, sp.ConditionSet) or isinstance(s2, sp.ConditionSet):
            return None
        if s1 == sp.EmptySet and s2 == sp.EmptySet:
            return sp.simplify((prev.lhs - prev.rhs) - (nxt.lhs - nxt.rhs)) == 0
        return s1 == s2
    if not isinstance(prev, sp.Eq) and not isinstance(nxt, sp.Eq):
        return sp.simplify(nxt - prev) == 0
    return False

4.3 Status dan jenis miskonsepsi

StatusArtiPerilaku API
FAULT_DETECTEDAda pasangan langkah yang tidak ekuivalen.has_fault true, fault_type diisi, counter_example bila tersedia.
ALL_STEPS_VALIDSemua pasangan langkah ekuivalen.has_fault false, tidak ada fault_type.
UNDECIDABLE_STEPConditionSet, derajat > 12, atau di luar cakupan.has_fault false, fault_type UNDECIDABLE_STEP, first_fault_line diisi.
PARSING_ERRORSalah satu baris tidak lolos parser.Berhenti; endpoint mengembalikan 422 dengan pesan baris.
fault_typeCara classifier memutuskannya
DISTRIBUTIVE_EXPANSION_ILLUSIONEkspresi memiliki pangkat; hasil expand tidak sama dengan ekspresi siswa.
ROOT_LOSSHimpunan solusi langkah baru adalah subset murni dari langkah sebelumnya.
EXTRANEOUS_ROOT_INTRODUCTIONHimpunan solusi langkah baru adalah superset murni dari langkah sebelumnya.
SIGN_INVERSION_OR_TRANSPOSITION_ERRORKedua ruas berubah bukan nol setelah penyederhanaan, mengindikasikan pindah ruas/salah tanda.
GENERAL_ALGEBRAIC_DERIVATION_ERRORFallback bila tidak ada pola yang lebih spesifik.

Counter-example dicari dengan substitusi integer kecil [2, 3, 1, 4]. Bila ditemukan selisih, respons berisi test_variable, test_value, lhs_expected, rhs_calculated, dan contradiction_statement. Bila tidak ada yang berhasil, respons berisi catatan untuk memeriksa aturan ekspansi - bukan counter-example palsu.

4.4 Contoh nyata /api/v1/diagnose

Permintaan berikut dijalankan langsung ke backend yang sedang berjalan saat dokumentasi ini disusun.

Permintaan
POST http://127.0.0.1:8000/api/v1/diagnose
Content-Type: application/json

{
  "confirmed_steps": [
    "2x + 6 = 14",
    "2x = 20",
    "x = 10"
  ],
  "target_variable": "x"
}
Respons aktual (200 OK)
HTTP/1.1 200 OK

{
  "status": "success",
  "session_id": "sess_5733145a",
  "transcription": {
    "total_lines": 3,
    "lines": [
      { "line_number": 1, "raw_latex": "2x + 6 = 14", "is_valid": true },
      { "line_number": 2, "raw_latex": "2x = 20",     "is_valid": false },
      { "line_number": 3, "raw_latex": "x = 10",      "is_valid": false }
    ]
  },
  "diagnosis": {
    "has_fault": true,
    "first_fault_line": 2,
    "fault_type": "SIGN_INVERSION_OR_TRANSPOSITION_ERROR",
    "status": "FAULT_DETECTED",
    "human_explanation": "Terdapat kekeliruan pembalikan tanda atau transposisi suku saat memindahkan ruas persamaan.",
    "socratic_prompt": "Perhatikan baris ke-2. Operasi aljabar apa yang kamu terapkan pada kedua ruas persamaan? Coba periksa kembali apakah tanda positif atau negatifnya sudah seimbang.",
    "counter_example": {
      "test_variable": "x",
      "test_value": 2,
      "lhs_expected": "-4",
      "rhs_calculated": "-16",
      "contradiction_statement": "Ketika x = 2, rumus awal bernilai -4, tetapi langkahmu menghasilkan -16."
    }
  },
  "guardrail_status": {
    "final_answer_suppressed": true
  }
}
Catatan:Perhatikan: baris 2 dan 3 ditandai tidak valid karena berada pada atau setelah fault pertama. Baris 1 valid. Ini bukan berarti setiap baris setelah fault otomatis salah secara independen - CFL hanya melaporkan titik patah pertama, sesuai namanya.
Peringatan:Batas CFL: hanya aljabar satu variabel target, maksimum 50 langkah per sesi, tiap sisi maksimum 200 karakter, dan pemeriksaan hanya pada pasangan langkah berurutan. CFL tidak memeriksa justifikasi pedagogis, tidak membaca gambar, dan tidak membuktikan identitas trigonometri lanjutan. Di luar cakupan itu, jawabannya adalah klarifikasi atau status tidak dapat diputuskan.

Agent harness: memori & self-learning

memoryepisodik
loop memori episodik

Harness menyimpan apa yang terjadi antar-giliran percakapan supaya sistem tidak mulai dari nol. Penyimpanan utamanya SQLite agent_episodic_memory, dengan mirror JSONL di data/zap_agent_memory.jsonl. Setiap baris adalah satu pola percakapan yang dideduplikasi, bukan transkrip mentah seluruh sesi.

Kelompok kolomIsi
Identitasmemory_id, user_key, conversation_id
Klasifikasitier, topic, shape_kind
Isi percakapanuser_query (maks 500 karakter), extracted_steps (JSON)
Hasil CFLcfl_status, misconception_type, counter_example, sympy_verified
Scaffoldingsocratic_resolution yang sudah disanitasi guardrail
Sinyal kualitashit_count, upvotes, downvotes, quality_score, updated_at

5.1 Isolasi pengguna

Identitas mentah tidak disimpan. user_id di-hash SHA-256, diambil 16 karakter hex, lalu diberi awalan u_. Tanpa identitas, baris masuk keranjang eksplisit anonymous - bukan NULL - agar predikat user_key selalu dapat diprediksi. Untuk pengguna beridentitas, memory_id juga menyertakan user_key, sehingga dua pengguna dengan pertanyaan identik tidak saling menimpa.

backend/data_engine/agent_harness.py - kunci anonim
# backend/data_engine/agent_harness.py
@classmethod
def _user_key(cls, user_id: Optional[str]) -> str:
    if user_id and str(user_id).strip():
        return cls._profile_key(str(user_id))
    return "anonymous"          # keranjang eksplisit, bukan NULL

@staticmethod
def _profile_key(user_id: str) -> str:
    digest = hashlib.sha256(
        user_id.strip().lower().encode("utf-8")
    ).hexdigest()[:16]
    return f"u_{digest}"        # identitas mentah tidak pernah disimpan
backend/data_engine/agent_harness.py - dedup per pemilik
# backend/data_engine/agent_harness.py
@staticmethod
def _compute_memory_id(topic, normalized_query, steps, user_key="anonymous"):
    parts = [topic.lower(), normalized_query.lower().strip(), "||".join(steps)]
    if user_key and user_key != "anonymous":
        parts.insert(0, user_key)        # dedup terpisah per pemilik
    payload = "|".join(parts)
    return "mem_" + hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]

5.2 Alur belajar enam langkah

  1. Redaksi PII. Email, nomor telepon Indonesia, NIK 16 digit, dan pola API key (sk-, nvapi-, AIza, ghp_) diganti placeholder.
  2. Ekstraksi langkah. Langkah eksplisit dipakai lebih dulu; bila kosong, extract_math_steps_from_text() memecah teks pada baris baru, tanda panah, atau titik-koma, lalu memvalidasi setiap kandidat dengan parser CFL.
  3. Gerbang CFL. Dua langkah atau lebih menjalani isolate_first_fault_breakpoint(). Fault adalah diagnosis, bukan verifikasi: sympy_verified tetap false.
  4. Resolusi Sokratik. Bila balasan asisten tidak ada, template dipilih berdasarkan jenis miskonsepsi atau geometri, lalu disanitasi RobustZeroAnswerGuardrail.sanitize().
  5. Upsert memori. INSERT ... ON CONFLICT(memory_id) DO UPDATE menaikkan hit_count dan memperbarui resolusi. Mirror JSONL ditulis dengan lock proses.
  6. Promosi korpus. Hanya derivasi dengan status ALL_STEPS_VALID, sympy_verified true, dan minimal dua langkah yang masuk MathCorpusStore permanen.
backend/data_engine/agent_harness.py - gerbang CFL & promosi
# backend/data_engine/agent_harness.py — langkah 3 (gerbang CFL)
if len(steps) >= 2:
    fault_idx, diag = isolate_first_fault_breakpoint(steps, target_var=target_var)
    cfl_status = diag.get("status", "UNKNOWN")
    if cfl_status == "FAULT_DETECTED":
        # Diagnosis BUKAN verifikasi. Turunan salah tidak boleh dipromosikan.
        misconception_type = diag.get("misconception_type")
        counter_example = diag.get("counter_example")
        sympy_verified = False
    elif cfl_status == "ALL_STEPS_VALID":
        sympy_verified = True
elif len(steps) == 1:
    try:
        parse_step_to_symbolic(steps[0])
        cfl_status = "SINGLE_EXPRESSION_VALID"
        sympy_verified = True
    except Exception:
        cfl_status = "CONCEPTUAL_QUERY"

# langkah 6 — promosi ke MathCorpusStore permanen
promoted_to_corpus = False
if cfl_status == "ALL_STEPS_VALID" and sympy_verified and len(steps) >= 2:
    corpus_item = {
        "id": f"conv_{memory_id}",
        "tier": tier,
        "topic": f"Pembelajaran Percakapan ({topic.capitalize()})",
        "question": clean_user[:500],
        "steps": steps,
        "target_variable": target_var,
        "sympy_verification": {"cfl_status": cfl_status, "is_valid": True},
        "socratic_prompt": socratic_resolution,
        "source": "ZAP Continuous User Conversation Memory",
    }
    self.corpus_store.insert_batch([corpus_item])
    promoted_to_corpus = True

5.3 Retrieval: kata kunci, bukan embedding

Sebelum menjawab, rute chat memanggil POST /api/v1/harness/context dengan timeout 1.200 ms. Harness mengambil maksimum 2.000 baris milik satu user_key, memberi skor tumpang tindih istilah, lalu memilih baris teratas. Bobotnya: user_query 2,0; extracted_steps 1,25; topic 0,75; socratic_resolution 0,25; bonus frasa utuh 6,0 dan prefiks 40 karakter 2,0. Stopword Indonesia/Inggris umum dibuang. Bila tidak ada istilah yang cocok, yang dikembalikan adalah baris milik pengguna itu saja - bukan baris acak lintas pengguna.

backend/data_engine/agent_harness.py - ranking memori
# backend/data_engine/agent_harness.py
def retrieve_relevant_memories(self, query, limit=3, user_id=None):
    user_key = self._user_key(user_id)
    terms = _query_terms(query)
    rows = conn.execute(
        "SELECT * FROM agent_episodic_memory WHERE user_key = ? "
        "ORDER BY updated_at DESC LIMIT 2000",
        (user_key,),
    ).fetchall()

    ranked = sorted(rows, key=lambda r: str(r.get("updated_at") or ""), reverse=True)
    scored = [(_score_memory(row, terms, query), row) for row in ranked]
    scored.sort(key=lambda pair: (
        -pair[0],                                  # skor tumpang tindih istilah
        -int(pair[1].get("sympy_verified") or 0),  # lalu status terverifikasi
        -int(pair[1].get("hit_count") or 0),       # lalu frekuensi
    ))
    matched = [row for score, row in scored if score > 0]
    return matched[:limit] if matched else [row for _, row in scored[:limit]]

Setelah itu, hasil korpus yang ber-ID conv_<memory_id> disaring: hanya pemilik memory_id yang boleh melihatnya. Item korpus sintetis/harvester tetap global karena berisi pengetahuan terverifikasi, bukan pertanyaan pribadi.

5.4 Umpan balik & statistik

POST /api/chat/feedback menerima rating up/down atau 1/-1, lalu meneruskannya ke POST /api/v1/harness/feedback. Backend menyimpan baris di agent_feedback. Kolom upvotes, downvotes, dan quality_score pada memori hanya diperbarui bila user_key pemilik memori cocok; umpan balik untuk memori orang lain tetap dicatat tetapi tidak mengubah baris tersebut.

GET /api/harness/stats adalah proxy Next.js yang menyimpan ZAP_HARNESS_SECRET di server. Endpoint backend aslinya secret-gated. Proxy mengembalikan agregat saja dan meng-cache 60 detik dengan stale-while-revalidate=300.

Metrik (snapshot 14 Nov 2026)Nilai
Memori episodik unik77
Total hit seluruh giliran216
Memori berstatus sympy_verified25
Sebaran tierSD 1 · SMP 65 · SMA 11
Miskonsepsi tercatatSIGN_INVERSION 8 · DISTRIBUTIVE 1 · ROOT_LOSS 1 · GENERAL 1
Umpan balik1 total · 1 up · 0 down
Catatan JSONL77 record · 0 baris rusak · delta DB/JSONL 0
Peringatan:Keterbatasan harness. (1) Tabel FTS agent_memory_fts lama tidak dipelihara; retrieval memakai skoring kata di Python sehingga tidak menangkap sinonim atau makna. (2) Setiap retrieval memuat sampai 2.000 baris per pengguna ke memori proses. (3) Pengguna tanpa identitas berbagi keranjang anonymous, meskipun memory_id tetap memisahkan pola yang berbeda. (4) Mirror JSONL dapat tertinggal dari SQLite bila penulisan gagal; endpoint statistik melaporkan delta-nya secara eksplisit. (5) Promosi korpus hanya dari percakapan yang benar-benar lolos CFL, sehingga sistem tidak belajar dari jawaban yang salah.

Basis pengetahuan multi-domain

korpusMathCSPhysChem
korpus multi-domain

Selain aljabar, repositori ini membangun korpus terverifikasi untuk empat domain. Verifikasinya berbeda per domain karena kebenaran matematis, kebenaran program, dan kebenaran fisika/kimia tidak dapat diperiksa dengan cara yang sama.

DomainStrukturItem di SQLiteMesin verifikasi
Matematika (TK–S3)7 jenjang · 18 topik · 34 subtopik320SymPy CAS: gerbang boxed, anotasi GSM8K, ekuivalensi simbolik
Pemrograman3 topik · 6 subtopik10AST whitelist: eval/exec tanpa builtins, maks 160 node, eksponen bilangan bulat
Fisika4 topik · 7 subtopik10Python/SymPy numerik eksak + pengecekan satuan
Kimia3 topik · 5 subtopik10Python/SymPy numerik eksak

Angka item dari audit 14 November 2026: total 350 record SQLite. Audit yang sama melaporkan 12.553 item unik di mirror JSONL - arsip lama yang belum direkonsiliasi, bukan isi database aktif.

6.1 Gerbang verifikasi per domain

  • Matematika. GSM8K diverifikasi ulang dari anotasi kalkulator; MATH dan NuminaMath wajib lolos _boxed_parseable(): isi \boxed{...} dinormalisasi, di-parse SymPy, tidak boleh punya variabel bebas, dan harus menghasilkan nilai yang dapat dievaluasi. Yang gagal tidak masuk korpus.
  • Pemrograman. Template kode dijalankan di sandbox AST, bukan sandbox OS. Hanya node yang diizinkan, tanpa __builtins__, dan eksponen harus bilangan bulat. Ini membatasi eksekusi template yang ditulis sendiri, bukan menjalankan kode pengguna.
  • Fisika & kimia. Jawaban dihitung ulang dengan Python/SymPy pada saat generasi; hasil numerik harus cocok dan jawaban tidak boleh muncul di field yang terlihat pengguna.

6.2 Harvester internet

SumberStatusFormatVerifikasi
gsm8k_sampleAktifJSONL GitHub OpenAIAnotasi kalkulator dievaluasi ulang SymPy
hf_gsm8k_apiAktifHuggingFace Datasets Server rowsGerbang yang sama dengan GSM8K
wikimedia_math_theoremsAktifWikimedia REST v1 summaryIdentitas matematis diperiksa SymPy; gagal = unverified
indo_kurikulum_sdNonaktifJSONLURL mengembalikan HTTP 404; tidak pernah dipanggil

Prinsip harvester: verified-only. Sumber yang gagal atau tidak dapat diverifikasi tidak menghasilkan fallback berlabel verified. Idempotent: item dideduplikasi berdasarkan teks pertanyaan yang dinormalisasi, sehingga menjalankan harvester berulang tidak menggandakan item. Ringkasan setiap putaran ditulis ke data/harvest_state.json (20 putaran terakhir). Putaran terakhir yang tercatat berhasil memasukkan 28 item terverifikasi: 10 dari GSM8K sample, 10 dari HF API, dan 8 dari Wikimedia.

data/harvest_state.json - ringkasan putaran
// data/harvest_state.json — ringkasan 20 putaran terakhir
{
  "runs": [
    {
      "finished_at": "2026-11-13T18:00:43.528860+00:00",
      "max_items_per_source": 10,
      "inserted_verified": 28,
      "per_source": {
        "gsm8k_sample":            { "fetched": 10, "verified": 10, "rejected_unverified": 0 },
        "hf_gsm8k_api":            { "fetched": 10, "verified": 10, "rejected_unverified": 0 },
        "wikimedia_math_theorems": { "fetched": 8,  "verified": 8,  "rejected_unverified": 0 }
      },
      "disabled_sources": {
        "indo_kurikulum_sd": "HTTP 404 - URL kurikulum tidak tersedia (perlu sumber resmi baru)"
      }
    }
  ],
  "last_run_at": "2026-11-13T18:00:43.528860+00:00",
  "last_inserted_verified": 28
}
Catatan:Perintah panen: python .agents/skills/zap-skills/scripts/train_zap_sources.py --harvest --max-items 25. Sumber nonaktif tetap tercatat beserta alasannya agar laporan kejujuran tidak menghitungnya sebagai sumber aktif.

6.3 Cara menambah domain

  1. Tambahkan definisi domain dan subtopik di taxonomy.py, termasuk mesin verifikasinya.
  2. Tambahkan generator deterministik di generators.py; hitung jawaban lebih dulu, lalu rakit item lewat _build_domain_item().
  3. Tambahkan pemeriksaan zero-answer dengan _answer_absent() agar jawaban tidak muncul di field yang terlihat.
  4. Daftarkan domain di DOMAIN_NAMES, rebuild korpus, lalu audit.
Pola penambahan domain
# 1. backend/data_engine/taxonomy.py
DOMAIN_TAXONOMY["baru"] = KnowledgeDomainDefinition(
    domain="baru",
    label="…",
    description="…",
    verification_engine="…",   # mesin deterministik yang memutuskan benar/salah
    topics=[DomainTopicDefinition(...)],
)
DOMAIN_NAMES = ("matematika", "pemrograman", "fisika", "kimia", "baru")

# 2. backend/data_engine/generators.py
_DOMAIN_ENGINE["baru"] = "…"

def generate_domain_sample(domain: str, idx: int) -> Dict[str, Any]:
    # hitung jawaban lebih dulu, lalu rakit item lewat _build_domain_item()
    ...

# 3. Tambahkan pemeriksaan zero-answer: _answer_absent(answer, *teks_terlihat)
# 4. Rebuild korpus, lalu jalankan audit:
python .agents/skills/zap-skills/scripts/train_zap_sources.py --train --domain baru --count 25 --audit
Peringatan:Jangan menambah domain tanpa mesin verifikasi deterministik. Domain yang hanya dinilai LLM akan melanggar pemisahan kebenaran/bahasa dan tidak boleh dipromosikan ke korpus terverifikasi.

Vault terenkripsi zero-knowledge

AES
256
passphraselokal
envelopeserver
envelope terenkripsi

Vault melindungi percakapan, proyek, preferensi, dan kunci provider di peramban sebelum dikirim ke server. Server hanya menerima envelope terenkripsi dan metadata identitas non-rahasia; passphrase, kunci turunan, dan plaintext tidak pernah dikirim.

ParameterNilai di kode
KDFPBKDF2-SHA256
Iterasi310.000 (rekomendasi OWASP 2023 untuk PBKDF2-SHA256)
Salt16 byte acak per envelope
CipherAES-GCM-256
Nonce / IV12 byte acak (96-bit)
Panjang passphrase minimum8 karakter
Kode pemulihan32 karakter Crockford base32, 8 grup × 4 (entropi ±160 bit)
Penyimpanan kunci sesimemori + sessionStorage per tab, bukan localStorage
frontend/lib/vault-crypto.ts - parameter & tipe envelope
// frontend/lib/vault-crypto.ts
export const VAULT_ENVELOPE_VERSION = 1;
export const PBKDF2_ITERATIONS = 310_000;   // rekomendasi OWASP 2023
const SALT_BYTES = 16;
const IV_BYTES = 12;                        // nonce AES-GCM 96-bit
const KEY_BITS = 256;

export interface VaultEnvelope {
  v: number;
  alg: "AES-GCM";
  kdf: { name: "PBKDF2-SHA256"; iterations: number; saltB64: string };
  ivB64: string;
  ctB64: string;
}

// passphrase + salt → PBKDF2-SHA256 (310.000 iterasi) → AES-GCM-256
const key = await subtle.deriveKey(
  { name: "PBKDF2", salt, iterations, hash: "SHA-256" },
  baseKey,
  { name: "AES-GCM", length: KEY_BITS },
  false,                 // tidak extractable untuk kunci normal
  ["encrypt", "decrypt"]
);
Bentuk envelope yang dikirim ke server
{
  "v": 1,
  "alg": "AES-GCM",
  "kdf": {
    "name": "PBKDF2-SHA256",
    "iterations": 310000,
    "saltB64": "…16 byte salt…"
  },
  "ivB64": "…12 byte nonce…",
  "ctB64": "…ciphertext + authentication tag…"
}
Alur sinkron lintas perangkat
Perangkat A                                      Server Next.js
──────────                                       ─────────────
passphrase vault
   │ PBKDF2-SHA256 × 310.000
   ▼
KEK AES-256  (hanya di memori + sessionStorage tab)
   │ AES-GCM-256, IV acak 12 byte
   ▼
envelope { v, alg, kdf, ivB64, ctB64 }  ─────────►  simpan envelope + metadata:
                                                   email, nama, avatar, provider,
                                                   tier, updatedAt
                                                   (tanpa passphrase / KEK / plaintext)

Perangkat B
   │ GET /api/user/sync  (Bearer token)
   ▼
envelope yang sama  ──► passphrase yang sama ──► KEK yang sama ──► dekripsi lokal

7.1 Token sesi akun vs Google token

KredensialDibuat olehBentukCara diverifikasi
Token sesi akun ZAPfrontend/lib/account-session.tsbase64url(payload).base64url(HMAC-SHA256), payload berisi email, exp, nonce, versiHMAC timing-safe dengan ZAP_SESSION_SECRET; TTL bawaan 30 hari; gagal = null (fail-closed)
Google ID tokenGoogle Identity ServicesJWT dari GoogleDiverifikasi ke endpoint tokeninfo; aud harus cocok, email terverifikasi, email token = email sinkronisasi

Token sesi akun dibuat saat signup/login di /api/auth/credentials dan disimpan klien di localStorage. Bila ZAP_SESSION_SECRET tidak diatur, server membuat secret acak 32 byte dan menyimpannya di .zap_vault/security/session-secret agar sesi tidak putus setiap restart.

7.2 Kode pemulihan

Kode pemulihan vault adalah passphrase alternatif yang dihasilkan perangkat, dinormalkan dengan membuang spasi/strip dan menyeragamkan huruf besar, lalu dipakai untuk menurunkan kunci yang sama. Kode ini berbeda dari token pemulihan akun di /api/admin/security, yang bersifat sekali pakai, dibuat administrator, dan dipakai untuk membuka suspend akun - bukan untuk membuka vault.

Peringatan:Batas yang tidak bisa dilanggar oleh desain. (1) Lupa passphrase vault dan kode pemulihan berarti isi vault tidak dapat dipulihkan; server tidak memiliki kunci. (2) Metadata identitas tetap plaintext di server: email, nama, avatar, provider, tier, dan updatedAt. (3) Akun tamu tidak memiliki sinkronisasi cloud. (4) Vault hanya sekuat perangkat: bila peramban terinfeksi XSS saat vault terbuka, kunci di memori/sessionStorage dapat dicuri. (5) Sinkronisasi bersifat envelope-terakhir-menang; tidak ada merge CRDT antar-perangkat.

Referensi API

POST
/api/chatroute handler
SSE
meta → deltastream
request → stream

Ada dua permukaan API: FastAPI di port 8000 untuk diagnostik/harness, dan route handler Next.js di port 3000 untuk percakapan, sinkronisasi, autentikasi, dan bagi tautan. Base URL backend dibaca dari NEXT_PUBLIC_BACKEND_URL dengan fallback http://127.0.0.1:8000.

8.1 Backend FastAPI

MetodeJalurAuthBody / parameterResponsStatus
GET/healthPublik-status, service, engine, version200
POST/api/v1/diagnosePublikconfirmed_steps[] atau lines[]; target_variable atau target_var; maks 50 langkahDiagnoseResponse (transcription, diagnosis, guardrail_status)200 · 422
POST/api/v1/verifyPublikleft, right (maks 200 karakter)isEquivalent, verified, method, message200 · 422
POST/api/v1/harness/learnHeader X-Harness-Secretuser_message, assistant_reply, explicit_steps, target_variable, tier_hint, user_idstatus learned, memory_id, cfl_status, sympy_verified, promoted_to_corpus, hit_count200 · 401 · 503
POST/api/v1/harness/contextHeader X-Harness-Secretquery, limit/top_k (1–10), user_idprompt_injection, episodic_memories, corpus_references, geometry_intent, learner_profile200 · 401 · 503
GET/api/v1/harness/profileHeader X-Harness-Secretuser_idlearner_profile agregat200 · 401 · 503
POST/api/v1/harness/feedbackHeader X-Harness-Secretrating, memory_id, conversation_id, user_id/user_key, comment, cuplikan pesanstatus recorded, feedback_id, applied_to_memory201 · 401 · 422 · 503
POST/api/v1/harness/geometry-patternHeader X-Harness-Secretphrase, specstatus, pattern200 · 401 · 503
GET/api/v1/harness/geometry-patternsHeader X-Harness-Secretlimit (bawaan 200)status, total, patterns200 · 401 · 503
POST/api/v1/harness/geometry-matchHeader X-Harness-Secretquerystatus, matched, match200 · 401 · 503
GET/api/v1/harness/statsHeader X-Harness-Secret-memori agregat + corpus + feedback200 · 401 · 503
WS/api/v1/board/{room}Kode ruangFrame JSON: seg, clear, notes, statewelcome, join, leave, siaran frame101

8.2 Route handler Next.js

MetodeJalurAuthBody / parameterResponsStatus
POST/api/chatPublikmessage, conversationHistory, targetVariable, stream, modelId, kunci provider opsionalJSON reply/model/source atau SSE meta → delta → done200 · 400 · 403
POST/api/chat/feedbackPublikrating, memoryId, conversationId, cuplikan pesanditeruskan dari backend201 · 400 · 429 · 503
GET/api/chat/sharePublik (kode tautan)idexists, data tanpa authorEmail200 · 400 · 403 (dicabut) · 404
POST/api/chat/sharePublik; authorEmail wajib untuk mengubah/mencabutconversationId, title, messages, authorEmail, isPublic, shareIdsuccess, shareId, sharePath, altSharePath200 · 400 · 403 · 413 · 500
GET/api/user/syncBearer Google token / token sesi-vault pengguna (envelope, tanpa kunci)200 · 401 · 403 · 429 · 500
POST/api/user/syncBearer Google token / token sesiuser, encrypted envelope, conversations, configs, workspacessuccess, encrypted, updatedAt200 · 400 · 401 · 403 · 413 · 429 · 500
GET/api/harness/statsPublik (proxy)-agregat harness200 · 502 · 503
POST/api/auth/credentialsPublikaction signup/login, email, password, namesessionToken, sessionExpiresAt, user200 · 400 · 401 · 409 · 429
GET/api/admin/securityADMIN_SECRET atau token pemulihanaction check/list, email, tokenstatus/list200 · 400 · 401 · 403 · 405 · 500
POST/api/admin/securityADMIN_SECRET atau token pemulihanaction, email, adminSecret, tokensuccess/message200 · 400 · 401 · 403 · 500
GET/api/user/avatarPublikurl / emailgambar atau 404200 · 400 · 404 · 429 · 500
POST/api/user/avatarPublikemail, dataUrlsynced200 · 400 · 413 · 429 · 500

8.3 Contoh curl

Chat, mode SSE. stream: true membuat server mengirim event meta, potongan delta, lalu done berisi JSON final.

curl /api/chat - SSE
curl -N http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Bantu cek coretan saya: 2x + 6 = 14 -> 2x = 20 -> x = 10",
    "stream": true,
    "targetVariable": "x"
  }'

# Potongan SSE yang diterima:
# event: meta
# data: {"targetVariable":"x"}
#
# event: delta
# data: {"t":"…potongan teks yang sudah disanitasi…"}
#
# event: done
# data: {"reply":"…teks final…","model":"…","source":"…"}

Chat, mode JSON. Tanpa stream, respons adalah satu JSON utuh dengan reply, model, source, dan opsional corpus_references.

curl /api/chat - JSON
curl http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Bantu cek coretan saya: 2x + 6 = 14 -> 2x = 20 -> x = 10",
    "targetVariable": "x"
  }'

# HTTP/1.1 200 OK
# {
#   "reply": "…pertanyaan Sokratik yang sudah lolos guardrail…",
#   "model": "…",
#   "source": "…"
# }
# Bila ada referensi korpus, respons juga memuat "corpus_references".
curl /api/v1/diagnose
curl http://127.0.0.1:8000/api/v1/diagnose \
  -H "Content-Type: application/json" \
  -d '{
    "confirmed_steps": ["2x + 6 = 14", "2x = 20", "x = 10"],
    "target_variable": "x"
  }'
curl /api/v1/verify
curl http://127.0.0.1:8000/api/v1/verify \
  -H "Content-Type: application/json" \
  -d '{"left": "(x+3)^2", "right": "x^2 + 6x + 9"}'
curl /api/user/sync
# Token = token sesi akun ZAP (HMAC) atau Google ID token.
curl http://localhost:3000/api/user/sync \
  -H "Authorization: Bearer $ZAP_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "user": {
      "id": "usr_a1b2c3d4e5f6",
      "email": "siswa@example.com",
      "name": "Siswa Contoh"
    },
    "encrypted": {
      "v": 1,
      "alg": "AES-GCM",
      "kdf": {
        "name": "PBKDF2-SHA256",
        "iterations": 310000,
        "saltB64": "…"
      },
      "ivB64": "…",
      "ctB64": "…"
    }
  }'

# 200 OK
# {
#   "success": true,
#   "encrypted": true,
#   "message": "Vault terenkripsi tersimpan (server tidak dapat membacanya).",
#   "updatedAt": 1763000000000
# }
curl /api/chat/share
curl http://localhost:3000/api/chat/share \
  -H "Content-Type: application/json" \
  -d '{
    "conversationId": "conv_123",
    "title": "Latihan persamaan linear",
    "messages": [
      { "sender": "user", "rawText": "2x + 6 = 14" },
      { "sender": "assistant", "replyText": "Operasi apa yang kamu terapkan pada kedua ruas?" }
    ],
    "authorEmail": "siswa@example.com",
    "isPublic": true
  }'

# 200 OK
# {
#   "success": true,
#   "shareId": "…uuid…",
#   "isPublic": true,
#   "sharePath": "/a/chat/s/…uuid…",
#   "altSharePath": "/chat/s/…uuid…",
#   "title": "Latihan persamaan linear"
# }

Untuk verifikasi cepat: POST http://127.0.0.1:8000/api/v1/verify mengembalikan symbolic_simplify karena SymPy menyederhanakan selisih menjadi 0.

8.4 Rate limit & batas ukuran

RuteBatas yang benar-benar ada di kodeCatatan
POST /api/chatTidak ada rate limiter per IP di routeDeteksi jailbreak memberi strike; 2 strike = suspend. Tidak ada cap ukuran body eksplisit.
POST /api/chat/feedback30 permintaan/menit per IPSliding window di memori proses.
POST /api/chat/shareBody maks 4 MiB; maks 500 pesan; judul 200 karakterauthorEmail wajib cocok untuk mengubah share yang sudah ada.
GET/POST /api/user/sync120 permintaan/menit per IP; body maks 8 MiB; ciphertext maks 12 juta karakterGET juga dibatasi; envelope divalidasi sebelum disimpan.
POST /api/auth/credentialssignup 5/menit; login 15/menit per IPMembatasi brute force; hasil 429 dengan Retry-After.
POST /api/auth/verify-key10/menit per IPDigunakan untuk verifikasi kunci provider.
GET/POST /api/user/avatarGET 300/menit; POST 20/menit; gambar maks 2 MiBRespons data URL dibatasi 512 KiB.
POST /api/waitlist5/menit per IPBukan bagian alur belajar.
Backend /api/v1/diagnoseMaks 50 langkah; target_variable maks 5 karakter422 untuk daftar kosong, terlalu banyak langkah, atau variabel di luar whitelist.
Backend /api/v1/verifyleft/right maks 200 karakterPydantic menolak input lebih panjang dengan 422.
Backend harnesslearn: pesan 8.000 karakter; feedback: komentar 1.000 karakterContext limit/top_k dibatasi 1–10.
Peringatan:Rate limiter disimpan di Map dalam memori satu proses Next.js. Pada deployment multi-instans, batas tidak global; komentar di utils/rate-limit.ts secara eksplisit menyarankan Redis untuk itu. Backend juga tidak memiliki rate limiter internal - pembatasan bergantung pada route Next dan reverse proxy.

Konfigurasi & self-hosting

.env

NEXT_PUBLIC_APP_URL=…

FASTAPI_URL=…

HARNESS_SECRET=

npm run dev
self-hosting

Variabel lingkungan dibaca di dua tempat. Jangan menaruh rahasia server di variabel berawalan NEXT_PUBLIC_: nilai itu ikut ke bundle peramban.

9.1 Frontend (Next.js)

VariabelWajib?Kegunaan
NEXT_PUBLIC_BACKEND_URLDisarankanBase URL FastAPI; fallback http://127.0.0.1:8000.
NEXT_PUBLIC_APP_URLOpsionalmetadataBase dan canonical; fallback https://chatzap.web.id.
NEXT_PUBLIC_GOOGLE_CLIENT_IDUntuk login GoogleClient ID di peramban.
ZAP_GOOGLE_CLIENT_IDUntuk sync GoogleAudiens token Google yang diverifikasi server.
ZAP_SESSION_SECRETDisarankanMenandatangani token sesi HMAC; minimal 32 karakter agar dipakai.
ZAP_HARNESS_SECRETProductionHeader X-Harness-Secret untuk semua endpoint harness.
ADMIN_SECRETUntuk adminAkses rute /api/admin/security.
ZAP_ALLOWED_HOSTSDi belakang proxyDaftar host yang dipercaya untuk redirect middleware.
GEMINI_API_KEY / GOOGLE_API_KEYSalah satuProvider bahasa utama bila tersedia.
NVIDIA_API_KEYOpsionalNVIDIA NIM, termasuk fallback model cepat.
BAZAARLINK_API_KEYOpsionalGateway DeepSeek/Qwen.
OPENAI_API_KEY / ANTHROPIC_API_KEY / MISTRAL_API_KEYOpsionalProvider alternatif.
ZAP_PLATFORM_LLM_BASE_URL / MODEL / API_KEYOpsionalEndpoint kompatibel OpenAI milik platform.

9.2 Backend (FastAPI)

VariabelKegunaan
CORS_ORIGINSDaftar origin dipisah koma; default hanya localhost:3000/3105 dan 127.0.0.1. Wildcard hanya bila env berisi *.
ZAP_HARNESS_SECRETBila diisi, header X-Harness-Secret wajib cocok.
ZAP_ENVBila bernilai production dan secret kosong, endpoint harness menolak 503.
ZAP_DB_PATHLokasi SQLite platform/korpus.
ZAP_JWT_SECRETSecret JWT platform (dipakai service platform).
ZAP_MASTER_SECRET / ZAP_MASTER_KEYSecret master untuk fitur platform; compose membaca ZAP_MASTER_KEY.

9.3 Menjalankan & menguji

Perintah pengembangan, pengujian, dan audit
# 1. Backend FastAPI (port 8000)
cd backend
uvicorn main:app --reload --port 8000
#    Swagger: http://127.0.0.1:8000/docs

# 2. Frontend Next.js (port 3000)
cd frontend
npm run dev

# 3. Docker Compose (production-like)
docker compose up -d --build

# 4. Pengujian backend
python -m pytest -v -p no:cacheprovider tests/

# 5. Audit 5 pilar + verifikasi suite (satu CLI)
python .agents/skills/zap-skills/scripts/train_zap_sources.py --audit --verify

# 6. Latih korpus prosedural multi-domain
python .agents/skills/zap-skills/scripts/train_zap_sources.py \
  --train --tier ALL --domain ALL --count 25

docker compose up -d --build menjalankan backend dan frontend. Compose mewajibkan ZAP_JWT_SECRET, ZAP_GOOGLE_CLIENT_ID, ADMIN_SECRET, dan POSTGRES_PASSWORD (untuk service Postgres opsional) terisi; sintaks ${VAR:?pesan} membuat compose berhenti bila kosong. Backend hanya dipublikasikan ke 127.0.0.1:8000, bukan ke semua antarmuka. Service Postgres belum dipakai kode saat ini.

Catatan:pytest dijalankan dari root repositori agar forwarder tests/ menemukan suite backend. Pada Windows, TestClient dapat memerlukan izin loopback; jika sandbox memblokir socket, jalankan di lingkungan yang mengizinkan bind localhost.

Model keamanan

aman
CSPHSTSXFO
lapisan pertahanan

Tujuan model keamanan ini bukan membuat sistem tidak bisa diserang, melainkan membatasi dampak dan menghindari klaim berlebihan. Berikut yang dilindungi dan yang tidak.

10.1 Yang dilindungi

  • Rahasia server. ZAP_HARNESS_SECRET, ADMIN_SECRET, kunci provider, dan ZAP_SESSION_SECRET hanya dibaca di server; proxy statistik ada agar klien tidak perlu memegang secret.
  • Isi vault. Enkripsi AES-GCM dilakukan di peramban; server menyimpan envelope dan metadata identitas saja.
  • Eksekusi parser. Whitelist dan batas eksponen mencegah input berbahaya mencapai eval() internal SymPy, serta membatasi biaya solveset.
  • Kebocoran jawaban. Dua lapis guardrail (backend dan frontend) menyensor deklarasi jawaban, termasuk bentuk ber-delimiter yang dulu lolos.
  • Isolasi memori. Retrieval memfilter user_key; referensi promosi percakapan difilter kepemilikannya.
  • PII. Email, telepon, NIK, dan pola API key diredaksi sebelum disimpan di memori.
  • Header & redirect. Middleware memvalidasi host dari header proxy sebelum membangun URL redirect; CSP, HSTS, X-Frame-Options, dan Referrer-Policy dipasang di next.config.mjs.

10.2 Yang tidak dilindungi

  • Perangkat yang sudah kompromi. Selama vault terbuka, kunci berada di memori dan sessionStorage. XSS atau malware di perangkat dapat mencurinya.
  • Token di localStorage. Google ID token dan token sesi akun disimpan di localStorage. Token ini dapat dibaca JavaScript mana pun yang berjalan di origin yang sama. Cookie zap_session hanyalah penanda keberadaan sesi, bukan kredensial.
  • Metadata server. Email, nama, avatar, provider, tier, dan waktu sinkronisasi tersimpan plaintext.
  • Rate limit terdistribusi. Limiter berjalan per proses; restart menghapus hitungan, dan multi-instans tidak berbagi state.
  • Prompt injection. Deteksi berbasis regex dan aturan; bisa salah tangkap (false positive) dan tidak menjamin semua upaya manipulasi tertangkap. Strike disimpan server-side dan dua pelanggaran menyebabkan suspend.
  • Papan kolaborasi. Kode ruang adalah satu-satunya kunci akses; siapa pun yang memegang kode dapat ikut menggambar. Ruang disimpan di memori server dan hilang saat proses berhenti.

10.3 Header keamanan

HeaderNilai
Content-Security-Policydefault-src self; script-src self unsafe-inline (dev juga unsafe-eval) accounts.google.com; frame-src accounts.google.com; connect-src self https: plus localhost untuk dev.
Strict-Transport-Securitymax-age=63072000; includeSubDomains; preload
X-Content-Type-Optionsnosniff
X-Frame-OptionsSAMEORIGIN
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(), microphone=(), geolocation=()
Catatan:Audit 13–14 November 2026 mencatat perbaikan pada beberapa isu nyata: kebocoran jawaban lewat delimiter LaTeX, reset password tanpa bukti kepemilikan, SSRF pada avatar, dan isolasi memori yang sebelumnya rusak. Klaim keamanan di halaman ini dibatasi pada perilaku kode saat ini, bukan jaminan bahwa tidak ada temuan baru di masa depan.

Keterbatasan & roadmap

korpus
limiter
FTS
vault
prioritas berurutan

Daftar ini sengaja eksplisit. Sebagian adalah batas desain yang diterima, sebagian adalah utang teknis yang sudah tercatat di audit.

KeterbatasanDampakStatus / arah perbaikan
CFL hanya aljabar satu variabel target; maks 50 langkah; sisi maks 200 karakterDi luar itu sistem memberi klarifikasi atau status tidak dapat diputuskan.Batas desain; perluas cakupan secara bertahap dengan uji regresi.
Derajat polinomial > 12 tidak diputuskanPersamaan berderajat tinggi bisa berhenti di UNDECIDABLE_STEP.Disengaja untuk mencegah worker terkunci; perlu strategi komputasi terpisah.
Retrieval berbasis kata kunci, FTS tidak dipeliharaSinonim dan makna tidak tertangkap; kualitas bergantung kosakata pengguna.Utang teknis tercatat; embedding atau FTS terpelihara adalah kandidat.
SQLite 350 record vs JSONL 12.553 recordLaporan berbeda tergantung sumber yang dibaca; JSONL adalah arsip lama.Rekonsiliasi belum dilakukan agar tidak menghapus tulisan yang sedang berjalan.
Satu sumber harvester nonaktif (HTTP 404)Kurikulum SD tidak mendapat item dari sumber itu.Perlu URL resmi baru yang dapat diverifikasi.
Rate limit per proses, bukan globalMulti-instans dapat melewati batas; restart mereset hitungan.Perlu penyimpanan bersama (mis. Redis).
/api/chat tidak memiliki rate limiter eksplisitBeban percakapan bergantung pada provider dan reverse proxy.Perlu limiter di route atau edge.
Prompt injection dideteksi dengan regexFalse positive dan false negative mungkin terjadi.Perlu pendekatan berlapis; saat ini strike 2 = suspend.
Vault tidak dapat dipulihkan tanpa passphrase/kodeKehilangan kunci berarti kehilangan data.Konsekuensi zero-knowledge; tidak ada backdoor.
Metadata identitas plaintext di serverEmail dan nama tetap terlihat oleh server.Dapat dienkripsi terpisah, tetapi mengubah alur sinkronisasi.
Akun tamu tanpa sinkronisasi cloudData tamu hanya di perangkat.Batas desain; pengguna perlu akun.
Sinkronisasi envelope last-write-winsDua perangkat yang menulis bersamaan dapat saling menimpa.Perlu versioning/merge.
Papan kolaborasi disimpan di memoriRuang hilang saat proses berhenti; tidak ada otentikasi selain kode ruang.Batas desain untuk demo; perlu penyimpanan persisten bila dipakai luas.
Model bahasa eksternal dapat gagal/timeoutKualitas balasan menurun, tetapi fallback Sokratik lokal tetap tersedia.Fallback sudah ada; pemantauan provider tetap diperlukan.

Urutan prioritas yang masuk akal

  1. Rekonsiliasi korpus SQLite dan JSONL, lalu pastikan angka publik selalu membaca satu sumber.
  2. Ganti rate limiter in-memory dengan penyimpanan bersama dan tambahkan limiter untuk /api/chat.
  3. Hidupkan kembali FTS atau ganti retrieval dengan indeks yang dipelihara.
  4. Pindahkan token sesi dari localStorage ke cookie HttpOnly bila arsitektur memungkinkan.
  5. Tambahkan versioning envelope vault untuk merge antar-perangkat.

Cara memverifikasi dokumen ini. Jalankan GET /api/harness/stats untuk angka memori/korpus, baca backend/cfl/parser.py untuk batas parser, dan buka frontend/lib/zero-answer.ts untuk pola guardrail. Bila halaman ini berbeda dari kode, kode yang benar - dan halaman ini perlu diperbarui.

Arsitektur
Next.js → FastAPI → CFL
Vault
PBKDF2 310k → AES-GCM-256
Uji
pytest + audit CLI
Bagian aktif: Ringkasan & prinsip