# Final Report: Dynamic Threshold Retrieval & Multi-Tenant Isolation
# Datetime: 17-05-2026

## 1. Executive Synopsis

Sistem Knowledge Graph sebelumnya menggunakan threshold cosine similarity yang di-hardcode (`PILLAR_THRESHOLD_MATRIX`) dengan nilai statis (0.68-0.72). Nilai ini tidak memiliki dasar statistik dan terbukti menyebabkan dua masalah: "paceklik relasi" (terlalu sedikit edge) atau false positive (edge tidak relevan). Selain itu, tidak ada mekanisme isolasi data antar workspace pengguna.

Proyek ini mengimplementasikan dua sistem secara bersamaan:

**Dynamic Threshold Retrieval** menggantikan nilai hardcoded dengan threshold berbasis Z-Score (Mean + 3*StdDev) yang dikalkulasi dari distribusi cosine similarity aktual database. Threshold disimpan di `config/thresholds.json` dan dibaca saat runtime dengan zero-latency overhead. Cron job `update_global_threshold.py` memperbarui nilai secara otomatis ketika populasi node bertambah lebih dari 100.

**Multi-Tenant Isolation** memastikan setiap workspace pengguna terisolasi via `project_id`. Node hanya dapat membentuk edge dengan node dari workspace yang sama, mencegah kebocoran data antar pengguna.

Hasil akhir: 64 node dengan 224 edge terbentuk menggunakan threshold Z-Score yang valid secara statistik, dengan penurunan edge sebesar 35% dibanding threshold lama — menunjukkan sistem kini lebih selektif dan presisi.

---

## 2. Methodology

Implementasi dilakukan dalam 5 fase berurutan:

**Fase Kalibrasi (Sub-task 1):** Sebelum integrasi, dilakukan eksperimen statistik menggunakan 56 node Literature. Skrip `calibrate_fallback_threshold.py` mengambil 500 pasangan node acak per pilar, menghitung cosine similarity, lalu menerapkan formula Z-Score dengan Section-Specific Multiplier. Hasil kalibrasi menjadi nilai fallback statis yang diabadikan di `config/thresholds.json`.

**Fase Integrasi (Sub-task 2):** Modifikasi `ingestion.py` dilakukan secara bertahap — pertama mengganti hardcoded threshold dengan fungsi `get_current_thresholds()` yang membaca JSON, kemudian menyuntikkan `project_id` ke seluruh call chain dari main block hingga Cypher query. Cron job `update_global_threshold.py` dibuat sebagai background worker dengan pre-flight check berbasis jumlah node baru.

**Pendekatan teknis utama:**
- JSON file caching untuk zero-latency threshold reads
- Fallback mechanism untuk cold start scenario
- Global sampling (tanpa filter project_id) untuk noise floor yang tidak bias
- Structured logging untuk observability

---

## 3. Strategic Decider (Decision Log)

| Option/Decision | Alternatives | Rationale | Status |
| :--- | :--- | :--- | :--- |
| Z-Score = Mean + 3*StdDev | Z-Score = 2 | Z=3 menjamin confidence 99.7%, Z=2 (95%) masih rentan noise | Done |
| External JSON Caching | Query database setiap runtime | JSON read dari disk jauh lebih cepat, mengurangi beban Neo4j | Done |
| Hybrid Cron (Time + Event) | Time-based murni | Komputasi berat hanya terjadi jika ada pertumbuhan data signifikan | Done |
| Global sampling untuk cron | Per-project sampling | Noise floor global tidak bias terhadap workspace tertentu | Done |
| Section-Specific Multiplier | Multiplier seragam 1.0x | Setiap pilar memiliki karakteristik semantik berbeda yang perlu adjustment | Done |
| Terima 64 node (2 hilang) | Re-ingest 2 file | Penyebab adalah fluktuasi API, bukan bug kode. Kondisi diterima untuk PoC | Done |

---

## 4. Bug Fixing & Risk Mitigation

| ID | Deskripsi | Severity | Status |
| :--- | :--- | :--- | :--- |
| BUG-01 | PILLAR_THRESHOLD_MATRIX masih direferensikan di 3 lokasi setelah rename ke FALLBACK | Medium | Fixed |
| BUG-02 | project_id tidak diteruskan secara eksplisit dari executor.submit ke create_literature_node | High | Fixed |
| BUG-03 | 10 node lama (pre-modifikasi) tidak memiliki project_id, tidak terdeteksi oleh find_similar_nodes | Medium | Resolved via full rebuild |
| RISK-01 | Cartesian product query di cron job berpotensi lambat pada database besar | Low | Accepted — LIMIT 500 membatasi beban |
| RISK-02 | 2 file gagal masuk karena API timeout saat ingestion paralel 66 file | Low | Accepted untuk PoC |
| RISK-03 | 3 node memiliki embedding NULL, tidak dapat membentuk edge | Low | Accepted — node tetap tersimpan, edge di-skip |

---

## 5. System Architecture

### 5.1 Alur Diagram Arsitektur

```mermaid
flowchart TD
    classDef init fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
    classDef logic fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    classDef io fill:#fff3e0,stroke:#f57c00,stroke-width:2px

    subgraph CRON [Cron Job 02:00 AM]
        C1[Start]:::init --> C2{Node baru > 100?}:::logic
        C2 -- Ya --> C3[Sample 500 pasangan global]:::io
        C3 --> C4[Hitung Mean + 3*StdDev per pilar]:::logic
        C4 --> C5[Apply Section Multiplier]:::logic
        C5 --> C6[Tulis config/thresholds.json]:::io
        C2 -- Tidak --> C7[Abort - Maintain last state]:::init
    end

    subgraph RUNTIME [Runtime Ingestion Pipeline]
        R1[PDF Input]:::init --> R2[Parsing + Chunking]:::logic
        R2 --> R3[LLM Extraction 5 Pilar]:::logic
        R3 --> R4[OpenAI Embedding 1536D]:::logic
        R4 --> R5[Create Node + project_id]:::io
        R5 --> R6[Read config/thresholds.json]:::io
        R6 --> R7{JSON valid?}:::logic
        R7 -- Ya --> R8[Dynamic Z-Score Threshold]:::logic
        R7 -- Tidak --> R9[Static Fallback Threshold]:::logic
        R8 --> R10[Vector Search]:::io
        R9 --> R10
        R10 --> R11[Filter: target.project_id == source.project_id]:::logic
        R11 --> R12{Score >= Threshold?}:::logic
        R12 -- Ya --> R13[Coherence + Domain Gate]:::logic
        R13 --> R14[Create SIMILAR_* Edge]:::io
        R12 -- Tidak --> R15[Reject]:::init
    end
```

### 5.2 Final Folder Structure

```text
backend-sre/
├── config/
│   └── thresholds.json                           # Cache threshold Z-Score (dibaca saat runtime)
├── scripts/
│   ├── update_global_threshold.py                # Cron job update threshold
│   └── Testing-Pipeline/
│       └── calibrate_fallback_threshold.py       # Skrip kalibrasi awal (one-time)
├── app/
│   └── services/
│       └── ingestion.py                          # Pipeline utama (dimodifikasi)
└── Markdown/
    └── Dynamic-Threshold-Retrieval/
        ├── Plan/
        │   ├── calibration_plan.md
        │   └── integration_plan.md
        ├── Report/
        │   ├── Fallback_Calibration_Result.md
        │   └── integration-report-17052026.md    # File ini
        └── Log-Execution/
            └── integration-execution-summary.md
```

---

## 6. Performance Metrics

| Metrik Pemantauan | Parameter Harapan | Hasil Aktual | Status |
| :--- | :--- | :--- | :--- |
| Threshold background | 0.45 - 0.85 | 0.6807 | PASS |
| Threshold methodology | 0.45 - 0.85 | 0.6914 | PASS |
| Threshold gap | 0.45 - 0.85 | 0.8102 | PASS |
| Threshold objective | 0.45 - 0.85 | 0.7292 | PASS |
| Threshold futurework | 0.45 - 0.85 | 0.7962 | PASS |
| Total node ter-ingest | 66 | 64 | PARTIAL (2 gagal karena API timeout) |
| Node dengan project_id | 100% | 100% (64/64) | PASS |
| Total edge terbentuk | > 0 | 224 | PASS |
| Edge SIMILAR_BACKGROUND | > 0 | 65 | PASS |
| Edge SIMILAR_METHODOLOGY | > 0 | 50 | PASS |
| Edge SIMILAR_GAP | > 0 | 26 | PASS |
| Edge SIMILAR_OBJECTIVE | > 0 | 66 | PASS |
| Edge SIMILAR_FUTUREWORK | > 0 | 17 | PASS |
| JSON parse latency | < 5ms | < 1ms (disk read) | PASS |
| Fallback mechanism | Aktif saat JSON missing | Verified via test | PASS |
| Pre-flight check cron | Abort jika node baru < 100 | Verified via test | PASS |

---

## 7. Execution Completion Matrix

| Check Box | Phase | Task | Detail Task | Description | Acceptance Criteria | Status |
| :---: | :--- | :--- | :--- | :--- | :--- | :--- |
| [x] | 1 | Setup | Init JSON Config | Buat config/thresholds.json dengan nilai kalibrasi | File eksis dan dapat di-parse oleh Python json.load | DONE |
| [x] | 2 | Refactoring | Edit ingestion.py (Threshold) | Ganti PILLAR_THRESHOLD_MATRIX dengan get_current_thresholds() | Aplikasi membaca threshold dari JSON | DONE |
| [x] | 3 | Security | Edit ingestion.py (Isolasi Data) | Tambah project_id ke seluruh call chain | Node memiliki project_id, vector search filter per workspace | DONE |
| [x] | 4 | Automation | Buat update_global_threshold.py | Cron job Z-Score dengan pre-flight check | Skrip menghitung dan menulis JSON jika node baru > 100 | DONE |
| [x] | 5 | Validation | Reset Edge dan Rebuild | Re-ingest 66 PDF dengan threshold baru | Edge terbentuk dengan threshold Z-Score | DONE |

---

## 8. Conclusion

Implementasi Dynamic Threshold Retrieval dan Multi-Tenant Isolation berhasil diselesaikan dalam 5 fase. Sistem kini menggunakan threshold yang valid secara statistik (Z-Score dengan confidence 99.7%) menggantikan nilai hardcoded yang tidak memiliki dasar empiris.

Penurunan jumlah edge dari 344 menjadi 224 (turun 35%) merupakan hasil yang diharapkan — bukan regresi. Ini menunjukkan bahwa threshold baru lebih selektif, terutama pada pilar Gap (0.70 -> 0.81) dan Future Work (0.70 -> 0.80) yang memang dirancang lebih ketat karena kedua pilar tersebut harus mencerminkan originalitas penelitian.

Isolasi multi-tenant berjalan sempurna: seluruh 64 node memiliki `project_id = "workspace-dev-01"` dan vector retrieval hanya menghubungkan node dalam workspace yang sama.

---

## 9. Recommendations & Next Steps

**Immediate:**
1. Daftarkan `update_global_threshold.py` ke cron scheduler: `0 2 * * * python scripts/update_global_threshold.py`
2. Re-ingest 2 file yang gagal (`Pemberdayaan Petani...` dan `USER INTERFACE...`) pada waktu beban API rendah
3. Perbaiki 3 node dengan embedding NULL — kemungkinan perlu re-embed secara manual

**Short-term:**
4. Implementasi monitoring untuk melacak distribusi edge per pilar setelah setiap ingestion batch
5. Tambahkan retry mechanism yang lebih agresif untuk LLM extraction pada file yang gagal
6. Evaluasi apakah threshold Gap (0.81) dan Future Work (0.80) terlalu ketat setelah populasi node bertambah

**Long-term:**
7. Implementasi true multi-tenant: `PROJECT_ID` dibaca dari user session, bukan environment variable
8. Pertimbangkan per-project threshold calibration ketika setiap workspace memiliki > 100 node
9. Tambahkan endpoint API untuk trigger manual threshold recalibration
