From 1accfd9390137ccf4f6a2b98e92568aed07b8fbc Mon Sep 17 00:00:00 2001 From: asepharyana Date: Mon, 24 Aug 2026 18:51:23 +0700 Subject: [PATCH] perf(ai-moderation): naikkan cache hit dgn guard akurasi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fase-1 exact-cache lookup: N query serial -> SATU query ANY($1::text[]) - Global reuse utk bare key legacy, HANYA verdict non-actionable (clean/flagless/action=none, conf>=0.85, umur<=72h) — flagged/warn tetap context-scoped - Semantic cache dua-band: clean band 0.92 default, actionable tetap 0.97; di antara band -> LLM (fail-open ke akurasi) - hit_count kini di-increment (bulk UPDATE per batch) -> hit-rate terukur - Cache hasil wikipediaSearch di Redis (6h, hanya hasil non-kosong) - Memoize fetchUrlSafely utk type=text (LRU 30m + in-flight dedupe) - makeImageCacheKey strip query CDN Discord (?ex/is/hm, format/width) -> attachment sama = satu key vision, skip re-download+re-vision Spec: .hermes/plans/2026-08-24-ai-analysis-cache-optimization.md Tests: +33 (cacheGuards, discordImageKeyNormalize, cacheBatchLookup) --- ...08-22_154707-ai-analysis-audit-optimize.md | 111 ++++++++ ...26-08-24-ai-analysis-cache-optimization.md | 127 +++++++++ ...8-24_135602-gmw-fe-constellation-rombak.md | 205 ++++++++++++++ .../ai-moderation/moderationOrchestrator.ts | 223 ++++++++++----- .../modules/ai-moderation/textCacheStore.ts | 265 +++++++++++++++--- .../src/modules/ai-moderation/urlFetcher.ts | 42 +++ .../modules/ai-moderation/wikipediaClient.ts | 36 +++ .../src/shared/config/index.ts | 24 ++ .../tests/cacheBatchLookup.test.ts | 127 +++++++++ .../discord-gateway/tests/cacheGuards.test.ts | 160 +++++++++++ .../tests/discordImageKeyNormalize.test.ts | 89 ++++++ 11 files changed, 1297 insertions(+), 112 deletions(-) create mode 100644 .hermes/plans/2026-08-22_154707-ai-analysis-audit-optimize.md create mode 100644 .hermes/plans/2026-08-24-ai-analysis-cache-optimization.md create mode 100644 .hermes/plans/2026-08-24_135602-gmw-fe-constellation-rombak.md create mode 100644 services/discord-gateway/tests/cacheBatchLookup.test.ts create mode 100644 services/discord-gateway/tests/cacheGuards.test.ts create mode 100644 services/discord-gateway/tests/discordImageKeyNormalize.test.ts diff --git a/.hermes/plans/2026-08-22_154707-ai-analysis-audit-optimize.md b/.hermes/plans/2026-08-22_154707-ai-analysis-audit-optimize.md new file mode 100644 index 00000000..fcc13dce --- /dev/null +++ b/.hermes/plans/2026-08-22_154707-ai-analysis-audit-optimize.md @@ -0,0 +1,111 @@ +# AI Analysis Flow — Audit & Optimization (discord-gateway) + +**Goal:** Analisis alur AI analysis end-to-end, temukan bug/inconsistency yang merusak kualitas verdict, lalu perbaiki root cause-nya. + +## Scope +- `services/discord-gateway/src/modules/ai-moderation/**` +- Tidak menyentuh chatbot backend / frontend. + +## Alur saat ini (hasil tracing) +``` +message capture → aiAnalyzer.queueMessageAnalysis(messageId) + → batchScheduler.scheduleConversationAnalysis(conversationKey) [debounce 250ms, CB gate] + → messageStore.getPendingMessagesByConversation(≤200) + → skipAgeRestrictedMessages + → pickBatchWithinBudget(14000 tokens, 50/msg) + → processBatch [Piscina worker, ≤4 threads] + → ai-analysis-worker.processBatch + → getConversationContextBefore(20 msgs) + attachments + → attachment-upload race guard (pending upload → skip) + → runModerationAnalysis + → Phase 1: exact-hash cache (PG text_analysis_cache, per channel/thread) + → Phase 2: semantic cache (embedTexts → Qdrant batch search; PG fallback) + → split text-only vs media + → runTextOnlyBatch: URL fetch + wiki search + glossary (paralel) + → dedup short messages → sub-batches (60/sub-batch) + → vision evidence utk URL images (hoisted, 15s cap per image) + → callModerationLLM per sub-batch (stream:true, retries 3, JSON parse + correction retry) + → runMediaBatch: download → vision per image (cache LRU→DB→live, lock) → 1 LLM call + → setCachedTextModeration (PG + Qdrant upsert w/ embedding) + → normalizeResult (confidence clamp, fallback analysis) + → updateMessagesAIAnalysisBulk → broadcast + scheduleAutoDelete + → recovery worker tiap 10s: pending keys → re-schedule; incomplete → individual fallback queue + → individual fallback: 1 msg = 1 worker job (context + full LLM) + → cache prune tiap 6 jam (PG expired + Qdrant expired points) +``` + +## Temuan audit (ranked) + +### F1 — Cache hit menghapus status "warn" (BUG AKURASI) +`moderationOrchestrator.ts` Phase-2 semantic hit & PG-fallback memetakan status via +`parseQdrantVerdict`: storedStatus bukan "warn"/"flagged" → dipaksa "clean". +TAPI exact-hash lookup (`getCachedTextModeration`, textCacheStore.ts:288-295) lebih parah: +hanya menerima "clean"|"flagged" — **"warn" jatuh ke branch flags.length===0 ? clean : flagged** +→ warn dengan flags=["conflict_instigation"] dibaca sebagai FLAGGED. +Efek: auto-delete eligibility (butuh recommendedAction delete/escalate + severity list) salah baca; +dashboard menampilkan flagged padahal verdict asli warn. Root cause: type narrowing legacy +(`status: "clean" | "flagged"`) tidak diupdate ketika "warn" ditambahkan ke schema. + +### F2 — Exact-cache key mengabaikan edit (BUG EVASION) +Key = sha256(content)+context. Pesan yang DIEDIT (`edited_content`) menghasilkan hash berbeda, +tapi verdict lama utk konten pre-edit tetap hidup; lebih penting: pesan edited="true" adalah sinyal +evasion di prompt, sedangkan cache bisa menyajikan verdict dari konten lama jika content sama. +(Minor, tapi konsistensi: `resolveIsEdited` ada di prompt, tidak ada di cache key.) + +### F3 — `pickBatchWithinBudget` skip-bukan-break (LATENSI/KUALITAS) +Loop `if (usedTokens + msgTokens <= maxTokens) {push}` — pesan BESAR di tengah list dilewati +dan iterasi lanjut mencoba msg berikutnya. Efek: batch berisi "lubang" (msg pending tetap pending, +dianalisis di gelombang berikutnya = LLM call tambahan). Ini by-design tolerable, tapi ada bug halus: +pesan >budget tunggal tidak pernah masuk (scheduler sudah punya fallback slice(0,1), OK). +Keputusan: biarkan (bukan bug nyata), catat saja. + +### F4 — `callModerationLLM` max_tokens 16384 hardcoded (COST) +Sub-batch 60 pesan × output ~150 token/pesan ≈ 9k token cukup; 16k aman. Biarkan. + +### F5 — Dead code builder user-profile/reputation +`buildUserProfilesBlock`, `buildUserProfileRef`, `UserProfileEntry` di moderationBuilders.ts +tidak dipakai lagi sejak context minimization (hanya tests). `` juga tak pernah +di-inject (rules masih menyebutnya — misleading bagi model). Bersihkan referensi prompt. + +### F6 — rules.ts menyebut `` yang tidak pernah ada di payload +Model diberi instruksi tentang blok yang tak pernah muncul → pemborosan token + potensi +kelakuan aneh ("menunggu" data yang tak ada). Hapus/ubah kalimat. + +### F7 — system.ts "Blok Data" menyebut ` (SearXNG)` — STALE +Sumber sudah Wikipedia. Komentar kode & teks prompt menyebut SearXNG. Perbaiki teks (kecil). + +### F8 — output.ts typo "secifik", baris tabel `-|-` rusak +Kualitas prompt: typo + markdown table broken (`||-`) di beberapa baris. Rapikan. + +### F9 — llmCaller parse-error correction tail hanya di SYSTEM +Correction tail ditambahkan ke system prompt; provider caching fine, tapi preview invalid +content (800 char) ikut SYSTEM — ok. Skip. + +### F10 — `getLlmSemaphore` race kecil saat config berubah di tengah flight +Non-issue praktis (config statis per proses). Skip. + +## Keputusan perbaikan (yang dieksekusi sekarang) +1. **F1 (utama):** normalisasi status di SATU tempat — `normalizeStoredStatus()` di + textCacheStore.ts yang menerima clean/warn/flagged; pakai di getCachedTextModeration + DAN parseQdrantVerdict; perluas return types ke union penuh. Orchestrator tinggal pakai. +2. **F6+F7+F8:** bersihkan stale references di prompts (user_history, SearXNG, typo). +3. **F5:** hapus dead builders + test-nya (biome/tsc yang jaga). +4. Regression test untuk F1 (vitest): warn tersimpan → warn terbaca (exact + qdrant path). + +## Files touched +- services/discord-gateway/src/modules/ai-moderation/textCacheStore.ts (F1) +- services/discord-gateway/src/modules/ai-moderation/moderationOrchestrator.ts (type only) +- services/discord-gateway/src/modules/ai-moderation/prompts/rules.ts (F6) +- services/discord-gateway/src/modules/ai-moderation/prompts/system.ts (F7) +- services/discord-gateway/src/modules/ai-moderation/prompts/output.ts (F8) +- services/discord-gateway/src/modules/ai-moderation/moderationBuilders.ts (F5) +- services/discord-gateway/tests/contextEnrichment.test.ts (F5 test cleanup + F1 regression test baru) + +## Verification +``` +cd services/discord-gateway +npx tsc --noEmit +npx biome check --diagnostic-level=error . +npx vitest run +``` +Semua harus hijau sebelum commit. Deploy via GHA (push main) — user konfirmasi belakangan. diff --git a/.hermes/plans/2026-08-24-ai-analysis-cache-optimization.md b/.hermes/plans/2026-08-24-ai-analysis-cache-optimization.md new file mode 100644 index 00000000..69f22bed --- /dev/null +++ b/.hermes/plans/2026-08-24-ai-analysis-cache-optimization.md @@ -0,0 +1,127 @@ +# Spec: Optimasi AI Analysis GMW — Naikkan Cache Hit Tanpa Kehilangan Akurasi + +Tanggal: 2026-08-24 · Repo: `~/GMW` (branch `main`) · Service: `services/discord-gateway` + +## Latar & Evidence (audit 2026-08-24) + +State produksi: +- Qdrant `gmw_text_moderation`: **1.550 poin, status green** (vectors size 2048, Cosine). +- PG `text_analysis_cache`: 1.634 row `user_moderation`, 277 `vision_llm`; **sum(hit_count) = 0** → + hit-rate tidak pernah terukur. +- Embedding aktif (`AI_LLM_EMBEDDING_MODEL` set, Nemotron-embed, dim 2048), `AI_LLM_EMBEDDING_MIN_SIMILARITY` + tidak diset di BWS → default **0.97** (sangat konservatif). +- Messages: 9.375 total; 643 status `error` (banyak retry), 49 pending. + +Temuan audit alur (`moderationOrchestrator.ts` → `textCacheStore.ts` → `qdrantClient.ts`, +`textBatchProcessor.ts`, `urlFetcher.ts`, `wikipediaClient.ts`, `visionAnalyzer.ts`): + +| # | Temuan | Dampak | +|---|--------|--------| +| F1 | Exact-hash cache key menyertakan context (channel/thread) → teks sama di channel lain selalu miss | Killer hit-rate #1 | +| F2 | Semantic tier TIDAK memfilter context (Qdrant payload tak punya context) — sudah global tapi hanya aman krn sim 0.97 ketat | Inkonsisten dgn exact tier | +| F3 | Phase-1 lookup loop `await getCachedTextModeration(key)` per pesan → N round-trip PgBouncer per batch (60 msg = 60 query serial) | Latensi + beban DB | +| F4 | Verdict actionable (flagged/warn) dan clean sama-sama boleh di-serve semantic; toleransi akurasi beda | Risiko akurasi | +| F5 | `hit_count` tidak pernah di-increment oleh reader manapun | Hit-rate tak terukur | +| F6 | `wikipediaSearch()` (blok ``) tanpa cache — re-fetch tiap batch utk query sama | Latensi + spam ke WP | +| F7 | `fetchUrlSafely()` tanpa cache — link sama di batch berikutnya di-download lagi penuh | Latensi + bandwidth | +| F8 | Vision cache key dari data-URL base64 hasil resize → attachment sama via jalur berbeda (URL vs embed) = key beda → re-download + re-vision | Duplikasi kerja vision | + +Non-goals: mengubah pipeline enforcement (auto-mute/ban trust-store writes), mengubah prompt +kebijakan moderasi, mengubah model/embedding provider. + +## Desain + +Semua perubahan degrade gracefully — cache gagal → perilaku lama (LLM). Akurasi dilindungi +asimetris: **hemat boleh untuk verdict non-actionable, konservatif untuk yang memicu aksi.** + +### D1 — Cache metrics (F5) +- `textCacheStore.getCachedTextModeration()`: saat hit valid, increment `hit_count` + (`UPDATE ... SET hit_count = hit_count + 1`) fire-and-forget (`.catch(()=>{})`), jangan blokir return. +- Log info periodik ringkas di orchestrator sudah ada ("User moderation cache applied") — cukup. + +### D2 — Batched exact-cache lookup (F3) +- Fungsi baru `getCachedTextModerations(keys: string[]): Promise>` + di `textCacheStore.ts`: **satu** `SELECT ... WHERE text = ANY($1)` (chunk 200 key/query), + parse + `normalizeStoredStatus` per row (reuse helper existing). +- Orchestrator fase-1: kumpulkan semua key unik → satu call batched → distribusi hasil. +- Semantik identik dengan loop lama (row expired/error-artifact tetap miss); hanya jumlah round-trip + yang turun N→1. + +### D3 — Global exact reuse untuk verdict non-actionable (F1) +- Key scoped-context TETAP ditulis (kompatibel, invalidasi moderator tetap presisi). +- Reader tambahan: kalau key `:` miss, coba key legacy global `text_mod:` (bare). +- Guard akurasi (WAJIB semua terpenuhi): + - `status === "clean"` DAN `flags.length === 0`; + - `confidence >= AI_CACHE_GLOBAL_REUSE_MIN_CONFIDENCE` (default 0.85); + - `recommendedAction === "none"`; + - umur entry ≤ `AI_CACHE_GLOBAL_REUSE_MAX_AGE_H` (default 72h) — cek `analyzed_at`. +- Flag baru `policyVersion: "cached-global-clean-2026-08"` supaya terlacak di dashboard/log. +- Verdict flagged/warn TETAP context-scoped (tidak pernah lintas channel). + +### D4 — Semantic dua-band similarity (F2+F4) +- Config baru: `AI_LLM_EMBEDDING_MIN_SIMILARITY_ACTIONABLE` default **0.97** (perilaku lama), + `AI_LLM_EMBEDDING_MIN_SIMILARITY_CLEAN` default **0.92**, keduanya coerce number 0..1. +- Satu Qdrant batch search pakai threshold RENDAH (0.92). Per hit, klasifikasi ulang: + - verdict non-actionable (clean, no flags, action=none): terima jika `score >= CLEAN_BAND`; + - verdict actionable (warn/flagged atau flags ada / action != none): terima hanya jika + `score >= ACTIONABLE_BAND` (0.97 — persis gate lama); + - di antara dua band → buang hit, pesan lanjut ke LLM (fail-open ke akurasi). +- Legacy PG fallback path: filter serupa di `findSimilarTextModeration` via parameter band. + +### D5 — Cache Wikipedia search (F6) +- `wikipediaClient.wikipediaSearch(query)`: cek `cacheGet(makeCacheKey("wikisearch", q))` dulu; + miss → fetch (timeout existing) → sukses & hasil non-kosong → `cacheSet(..., TTL 6h)`. + Hasil kosong TIDAK di-cache (biar retry nanti). Redis down → langsung fetch (no-op cache). + +### D6 — Cache URL text fetch (F7) +- `urlFetcher.fetchUrlSafely(url)`: wrapper async memoize in-process LRU (max 500, TTL 30 menit) + untuk `type === "text"` saja (image tetap selalu fresh-download karena dipakai sbg bukti vision + + buffer besar; error tidak di-cache). +- Import `LRUCache` dari `lru-cache` (sudah dep gateway). + +### D7 — Unified vision cache key (F8) +- `makeImageCacheKey(imageUrl)` di `textCacheStore.ts`: sebelum hash, strip query Discord CDN + (`?ex=&is=&hm=` signed tokens, `format/width/height/size`) — regex `(\?[^#]*)$` dibuang bila host + CDN discord (`cdn.discordapp.com`, `media.discordapp.net`, `images-ext-*.discordapp.net`); + URL non-Discord: hash full URL seperti sekarang. +- Efek: attachment sama yang lolos lewat jalur embed vs inline vs re-fetch dgn token beda → SATU + entry cache → skip download+vision kedua kali. Data-URL base64 tetap di-hash apa adanya. + +## File yang disentuh + +1. `src/shared/config/index.ts` — 3 config baru (D3×2, D4×2 — total 4 nilai, 3 baris zod + deskripsi). +2. `src/modules/ai-moderation/textCacheStore.ts` — hit_count inc (D1), batched getter (D2), + global-reuse guard helper (D3), image-key normalize (D7). +3. `src/modules/ai-moderation/moderationOrchestrator.ts` — pakai batched getter (D2), + global bare-key fallback (D3), dua-band semantic accept (D4). +4. `src/modules/ai-moderation/qdrantClient.ts` — `searchQdrantBatch` menerima threshold rendah + (sudah parametrik — mungkin tanpa perubahan; verifikasi). +5. `src/modules/ai-moderation/wikipediaClient.ts` — cache layer (D5). +6. `src/modules/ai-moderation/urlFetcher.ts` — LRU text-fetch memoize (D6). + +## Schema/type changes + +- Tidak ada migrasi DB (kolom `hit_count`, `analyzed_at`, `expires_at` sudah ada). +- Tidak ada perubahan kontrak WS/oRPC/frontend. +- Type baru: none public; internal `StoredModerationVerdict` dipakai ulang. + +## Verification + +1. Unit tests baru (`tests/`): + - `cacheBatchLookup.test.ts`: batched getter — hit/miss/expired/error-artifact mapping, + chunking >200 keys (mock executeAll), hit_count increment called. + - `globalReuseGuard.test.ts`: guard menerima clean+conf≥0.85+action none+umur ≤72h; + menolak flagged/warn/conf rendah/action≠none/stale. + - `semanticBands.test.ts`: clean @0.93 diterima, flagged @0.93 ditolak, flagged @0.98 diterima. + - `imageKeyNormalize.test.ts`: URL Discord dgn/ex token → key sama; non-Discord beda query → beda. +2. Gate service: `pnpm typecheck && pnpm exec biome check --diagnostic-level=error . && pnpm exec vitest run`. +3. Deploy via GHA (`git push origin main`) → watch `Build & Deploy (Nix)` → verifikasi + `systemctl show gmw-discord-gateway -p ActiveEnterTimestamp` baru. +4. Runtime probe pasca-deploy: journalctl level 30 normal; beberapa jam kemudian + `SELECT sum(hit_count) FROM text_analysis_cache WHERE source='user_moderation'` > 0 membuktikan + metrics jalan; log "User moderation cache applied" menunjukkan hits>0 pada traffic ramai. + +## Rollback + +Semua fitur behind config defaults yang mempertahankan perilaku lama pada nilai konservatif; +rollback = redeploy commit sebelumnya (tanpa migrasi DB, tanpa state eksternal). diff --git a/.hermes/plans/2026-08-24_135602-gmw-fe-constellation-rombak.md b/.hermes/plans/2026-08-24_135602-gmw-fe-constellation-rombak.md new file mode 100644 index 00000000..768cbf44 --- /dev/null +++ b/.hermes/plans/2026-08-24_135602-gmw-fe-constellation-rombak.md @@ -0,0 +1,205 @@ +# GMW FE Rombak — "Constellation Ops" Implementation Plan + +> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. +> Setiap task = edit → lint → build/test → smoke → commit (author `asepharyana`, tanpa Co-Authored-By). + +**Goal:** Rombak total FE GMW dari template dashboard klasik menjadi **Constellation Node Graph** — graf node interaktif (three.js force layout) di mana channel/event/moderasi adalah node bintang dan navigasi adalah terbang antar node — untuk semua 9 route sekaligus. + +**Architecture:** Hapus total shell template (TopBar + NavRail + MobileNav + main scroll container). Ganti dengan satu `ConstellationStage`: canvas full-bleed fixed di belakang seluruh app yang me-render force-directed graph per-route (scene), plus overlay HTML mengambang (tanpa card box standar) untuk konten detail. Router tetap Next App Router; perpindahan route = kamera "fly-to" ke scene baru. Data tetap lewat pola SSR seed (`page.tsx` server fetch → `view.tsx` client + SWR fallbackData) — tidak ada endpoint baru yang diarankan. + +**Tech Stack:** Next.js 16 App Router · React 19 · TypeScript strict · Tailwind v4 (token oklch existing) · three r180 (+ `@types/three`) · d3-force (+ `@types/d3-force`) untuk simulasi layout deterministik · motion v12 (transisi antar-scene) · biome (0 error **dan** 0 warning) · bun:test untuk modul murni. + +--- + +## Keputusan Terkunci (2026-08-24, user MythEclipse) + +| Keputusan | Pilihan user | +|---|---| +| Cakupan | **Semua 9 route sekali jalan** (dashboard, voice, media, recordings, moderation, messages, analysis, channels, glossary) | +| Arah struktural | **Constellation Node Graph** — navigasi = terbang antar node | +| Tema | **Pertahankan toggle light/dark** (jangan rebuild sistem tema — harden saja) | + +## Kondisi Saat Ini (fakta repo, 2026-08-24) + +- Repo: `/home/code/GMW/services/frontend` (monorepo GMW, deploy Nix-first via GHA "Build & Deploy (Nix)" ke `imphnen.asepharyana.my.id`; nginx gmw-proxy :4009 → Next standalone :4017). +- Shell aktif: `src/components/shell/ambient-app.tsx` → `AppFrame` = NavRail + TopBar + `
` scroll + MobileNav + MiniPlayer, dipakai SEMUA route via `(dashboard)/layout.tsx`. AmbientCanvas (233 l) masih ada sebagai background di belakang chrome template. +- 9 route di `src/app/(dashboard)/`: dashboard (368 l), voice (311), media (293), moderation (424), messages (611), analysis (196), recordings (201), channels (23), glossary (15). +- `next.config.ts`: `output: "standalone"` + `trailingSlash: true` → **nav via `router.push`/Link bisa no-op; pakai `` polos** (pitfall terverifikasi). +- Token desain ada di `globals.css` `@theme`: `--color-canvas/-2`, `--color-ink/-soft/-faint`, `--color-signal(+glow)`, amber, vermilion, hairline; font Bricolage Grotesque/JetBrains Mono; `.light` class override + next-themes sudah jalan — JANGAN dibangun ulang. +- Sudah ada & dipertahankan: CommandPalette (⌘K), Chatbot, MiniPlayer, primitives (GlassPanel/GlassCard/Badge/…), shared states+skeletons, `lib/format.ts`, `lib/ai-status.ts`, WS typed events (`message_created/updated/deleted/analyzed/snapshot*`, `voice_state`, dll). +- Tooling lokal: `pnpm lint|build|format`, `pnpm exec biome check --write --unsafe .` buat import order; bun 1.3.14 tersedia untuk unit test modul murni. +- Smoke test SELALU di port non-prod (mis. 4024), cek `ss -ltnp | grep ` dulu; JANGAN pernah bind 4017. + +## Struktur Target + +``` +src/components/shell/ + constellation-frame.tsx # pengganti AppFrame: tanpa topbar/rail/bottom-bar + constellation-stage.tsx # canvas three.js full-bleed + hit-test + kamera + route-scenes.ts # map pathname → scene config (nodes/edges/focus) + floating-chrome.tsx # brand mark + status dot + route switcher minim + palette hint + (hapus setelah migrasi): topbar.tsx, nav-rail.tsx, mobile-nav.tsx, ambient-app.tsx* +src/lib/constellation/ + graph.ts # tipe GraphNode/GraphEdge + builder dari data API (murni) + layout.ts # wrapper d3-force deterministik (seeded) + fallback radial (murni) + camera.ts # interpolasi kamera fly-to (murni, testable) + *.test.ts # bun:test utk modul murni di atas +``` + +*ambient-canvas/context dievaluasi: bisa di-refactor jadi bagian dari stage atau dihapus. + +### Scene per route (bahasa visual satu kesatuan) + +| Route | Scene | +|---|---| +| `/dashboard/` | Guild = bintang pusat; channel = node orbit; event WS = pulse edge; metric cluster floating (tanpa box) | +| `/channels/` | Semua channel sebagai konstelasi; klik channel = fly-to + panel detail mengambang | +| `/moderation/` | Pesan flagged = node merah (vermilion) mengelilingi hub AI-verdict; LiveModerationFeed jadi ribbon ticker | +| `/messages/` | Stream pesan = sabuk orbital; node baru masuk lewat animasi dari tepi | +| `/voice/` | Stage node di tengah; speaker aktif = node mengorbit dengan glow intensitas | +| `/media/` | Galeri spiral node (thumbnail via div bg-image); klik = MiniPlayer handoff | +| `/recordings/` | Variasi spiral media, timeline density sebagai cincin | +| `/analysis/` | Hub analitik; chart existing dirender sebagai panel translusen mengambang ter-anchor ke node | +| `/glossary/` | Term KB = cincin satelit; search memfilter node real-time | + +### Checklist anti-default (wajib lolos sebelum commit akhir) + +- [ ] Tidak ada top bar / nav rail / side panel / bottom prompt bar +- [ ] Tidak ada card grid standar sebagai struktur utama +- [ ] Layout beda SPASIAL dari versi lama (bukan ganti warna) +- [ ] Verifikasi visual sungguhan: browser_navigate + browser_vision → "bukan template dashboard" + +--- + +## Tasks + +### Phase 0 — Fondasi + +#### Task 1: Dependencies + scaffold direktori +**Files:** `package.json`, create `src/lib/constellation/` +1. `cd /home/code/GMW/services/frontend && pnpm add three @types/three d3-force @types/d3-force && pnpm add -d @types/bun` +2. Buat 4 file kosong bertipe sesuai struktur target (ekspor placeholder bertipe agar tsc lolos). +3. Run: `pnpm exec tsc --noEmit` → expected PASS. +4. Commit: `chore(frontend): scaffold constellation lib + deps`. + +> Catatan: jika `@types/bun` bentrok dengan tsconfig Next (duplikat globals), fallback: exclude `"**/*.test.ts"` dari `tsconfig.json` dan jalankan test hanya via `bun test` (tanpa typecheck). Putuskan saat eksekusi, dokumentasikan di commit body. + +#### Task 2 (TDD): Modul murni graph + layout + camera +**Files:** `src/lib/constellation/{graph,layout,camera}.ts` + `*.test.ts` +1. **Tulis test dulu** (bun:test): + - `graph.test.ts`: builder `dashboardToGraph()` menghasilkan node guild pusat + N channel + edges benar dari fixture `/api/dashboard/stats|channels` shape (lihat AGENTS.md; nama channel di `message.metadata.channelName`). + - `layout.test.ts`: `computeLayout(nodes, edges, {seed:42})` DETERMINISTIK (dua pemanggilan = posisi identik), semua posisi finite, radius dalam bound; `radialLayout()` fallback untuk reduced-motion. + - `camera.test.ts`: `flyTo(from,to,t)` easing monotonic, t=0→from, t=1→to. +2. Run: `bun test src/lib/constellation` → expected FAIL (belum diimplementasi). +3. Implementasi minimal sampai PASS. +4. Run: `bun test src/lib/constellation && pnpm exec tsc --noEmit` → PASS. +5. Commit: `feat(frontend): constellation graph/layout/camera pure modules (tested)`. + +### Phase 1 — Shell Baru + +#### Task 3: `ConstellationStage` (canvas renderer) +**Files:** `src/components/shell/constellation-stage.tsx` +- `` fixed inset-0 `-z-10`; three.js orthographic 2D-ish render node+edge (sprite/glow shader sederhana — jangan over-engineer; titik + garis + glow cukup). +- Interaksi: drag = pan, wheel/pinch = zoom, hover = highlight node + tooltip title, click node ber-`href` = navigasi via `` semantics (window.location assign — hindari router.push no-op). +- DPR-aware resize; pause render loop saat `document.hidden`; `prefers-reduced-motion` → render statis tanpa animasi. +- Sampling warna dari CSS var (`getComputedStyle`) + `MutationObserver` pada `documentElement.classList` agar ikut toggle dark/light. +- Verify: `pnpm lint && pnpm build` PASS → commit `feat(frontend): constellation stage canvas renderer`. + +#### Task 4: `route-scenes.ts` + `ConstellationFrame` +**Files:** `src/components/shell/{route-scenes,constellation-frame,floating-chrome}.tsx` +- `route-scenes.ts`: `usePathname()` → config scene (builder mana, focus node, overlay slots). +- `ConstellationFrame`: render `ConstellationStage` + `children` (overlay HTML absolute, BUKAN flex column bertingkat) + `floating-chrome` (brand mark kiri-atas dgn status dot WS, switcher route minimal kanan-bawah, hint ⌘K). MiniPlayer + Chatbot + CommandPalette tetap termounting. +- Switcher & semua nav internal pakai `` polos (trailingSlash!). Verifikasi klik di browser nanti, bukan cuma baca kode. +- Wire `(dashboard)/layout.tsx`: ganti `AppFrame` → `ConstellationFrame` (provider Ambient/WS tetap). +- Smoke: `ss -ltnp | grep 4024` kosong → `PORT=4024 pnpm start` → curl semua 9 route = 200 → kill server. +- Commit: `feat(frontend): replace classic shell with constellation frame`. + +#### Task 5: Hapus chrome lama + bersih-bersih +**Files:** hapus `shell/topbar.tsx`, `shell/nav-rail.tsx`, `shell/mobile-nav.tsx`, update `shell/index.ts`; eval `ambient-*` (refactor jadi layer stage ATAU hapus). +- `grep -rn "TopBar\|NavRail\|MobileNav\|AppFrame" src/` → 0 referensi tersisa. +- `pnpm exec biome check --write --unsafe .` → import order rapi. +- Commit: `refactor(frontend): remove classic dashboard chrome`. + +### Phase 2 — Migrasi 9 Route (batch, tiap batch ship hijau) + +Urutan sengaja dari yang paling graph-native ke paling padat konten: + +#### Task 6: `/dashboard/` + `/channels/` +- Dashboard view: guild-star + channel orbit + WS pulse; metric cluster floating (reuse MetricTile tapi tanpa card-box — ubah jadi teks besar + hairline). +- Channels: konstelasi channel, klik = fly-to + panel detail (data dari hook `use-channels`/server fetch existing — jangan karang endpoint). +- Lint+build+smoke 2 route → commit `feat(frontend): dashboard & channels constellation scenes`. + +#### Task 7: `/glossary/` + `/analysis/` +- Glossary: cincin satelit term + search filter node (client-side filter pada node label). +- Analysis: hub node + panel chart translusen mengambang (reuse komponen charts/* existing, anchor posisi ke node). +- Commit: `feat(frontend): glossary & analysis scenes`. + +#### Task 8: `/messages/` + `/moderation/` +- Messages: sabuk orbital stream (reuse sortMessages/logika WS `use-messages.ts` — jangan duplikasi); pesan baru animate-in dari tepi. +- Moderation: flagged nodes vermilion + hub verdict; LiveModerationFeed jadi ticker ribbon bawah (bukan panel). +- Commit: `feat(frontend): messages & moderation scenes`. + +#### Task 9: `/voice/` + `/media/` + `/recordings/` +- Voice: speaker aktif orbit dgn glow (seed dari snapshot `activeSpeakers` server-authoritative — pertahankan perilaku useSpeakers). +- Media/Recordings: spiral galeri; thumbnail pakai div background-image (hindari `noImgElement`; kalau terpaksa `` → `// biome-ignore lint/performance/noImgElement: `); MiniPlayer handoff tidak boleh regress. +- Commit: `feat(frontend): voice, media & recordings scenes`. + +### Phase 3 — Polish, A11y, Tema + +#### Task 10: Motion & micro-interaction +- Fly-to antar scene pakai `motion` v12 (interpolasi kamera dari `lib/constellation/camera.ts`). +- Overlay: stagger masuk (pattern `animate-stagger` + `staggerDelay(i)` existing); tambah semua `animate-*` baru ke kill-list `prefers-reduced-motion` di `globals.css`. +- Commit: `feat(frontend): scene transitions + motion polish`. + +#### Task 11: A11y & fallback semantik +- Setiap scene punya fallback semantik: `