Created
August 15, 2026 03:23
-
-
Save AhmadYasin2/370c9a880ed97dc000f5fe0a44af840f to your computer and use it in GitHub Desktop.
Repository Wiki — generated by GitNexus
This file has been truncated, but you can view the full file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Harakat — Wiki</title> | |
| <script src="https://cdn.jsdelivr.net/npm/marked@11.0.0/marked.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script> | |
| <style> | |
| *{margin:0;padding:0;box-sizing:border-box} | |
| :root{ | |
| --bg:#ffffff;--sidebar-bg:#f8f9fb;--border:#e5e7eb; | |
| --text:#1e293b;--text-muted:#64748b;--primary:#2563eb; | |
| --primary-soft:#eff6ff;--hover:#f1f5f9;--code-bg:#f1f5f9; | |
| --radius:8px;--shadow:0 1px 3px rgba(0,0,0,.08); | |
| } | |
| body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; | |
| line-height:1.65;color:var(--text);background:var(--bg)} | |
| .layout{display:flex;min-height:100vh} | |
| .sidebar{width:280px;background:var(--sidebar-bg);border-right:1px solid var(--border); | |
| position:fixed;top:0;left:0;bottom:0;overflow-y:auto;padding:24px 16px; | |
| display:flex;flex-direction:column;z-index:10} | |
| .content{margin-left:280px;flex:1;padding:48px 64px;max-width:960px} | |
| .sidebar-header{margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid var(--border)} | |
| .sidebar-title{font-size:16px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:8px} | |
| .sidebar-title svg{flex-shrink:0} | |
| .sidebar-meta{font-size:11px;color:var(--text-muted);margin-top:6px} | |
| .nav-section{margin-bottom:2px} | |
| .nav-item{display:block;padding:7px 12px;border-radius:var(--radius);cursor:pointer; | |
| font-size:13px;color:var(--text);text-decoration:none;transition:all .15s; | |
| white-space:nowrap;overflow:hidden;text-overflow:ellipsis} | |
| .nav-item:hover{background:var(--hover)} | |
| .nav-item.active{background:var(--primary-soft);color:var(--primary);font-weight:600} | |
| .nav-item.overview{font-weight:600;margin-bottom:4px} | |
| .nav-children{padding-left:14px;border-left:1px solid var(--border);margin-left:12px} | |
| .nav-group-label{font-size:11px;font-weight:600;color:var(--text-muted); | |
| text-transform:uppercase;letter-spacing:.5px;padding:12px 12px 4px;user-select:none} | |
| .sidebar-footer{margin-top:auto;padding-top:16px;border-top:1px solid var(--border); | |
| font-size:11px;color:var(--text-muted);text-align:center} | |
| .content h1{font-size:28px;font-weight:700;margin-bottom:8px;line-height:1.3} | |
| .content h2{font-size:22px;font-weight:600;margin:32px 0 12px;padding-bottom:6px;border-bottom:1px solid var(--border)} | |
| .content h3{font-size:17px;font-weight:600;margin:24px 0 8px} | |
| .content h4{font-size:15px;font-weight:600;margin:20px 0 6px} | |
| .content p{margin:12px 0} | |
| .content ul,.content ol{margin:12px 0 12px 24px} | |
| .content li{margin:4px 0} | |
| .content a{color:var(--primary);text-decoration:none} | |
| .content a:hover{text-decoration:underline} | |
| .content blockquote{border-left:3px solid var(--primary);padding:8px 16px;margin:16px 0; | |
| background:var(--primary-soft);border-radius:0 var(--radius) var(--radius) 0; | |
| color:var(--text-muted);font-size:14px} | |
| .content code{font-family:'SF Mono',Consolas,'Courier New',monospace;font-size:13px; | |
| background:var(--code-bg);padding:2px 6px;border-radius:4px} | |
| .content pre{background:#1e293b;color:#e2e8f0;border-radius:var(--radius);padding:16px; | |
| overflow-x:auto;margin:16px 0} | |
| .content pre code{background:none;padding:0;font-size:13px;line-height:1.6;color:inherit} | |
| .content table{border-collapse:collapse;width:100%;margin:16px 0} | |
| .content th,.content td{border:1px solid var(--border);padding:8px 12px;text-align:left;font-size:14px} | |
| .content th{background:var(--sidebar-bg);font-weight:600} | |
| .content img{max-width:100%;border-radius:var(--radius)} | |
| .content hr{border:none;border-top:1px solid var(--border);margin:32px 0} | |
| .content .mermaid{margin:20px 0;text-align:center} | |
| .menu-toggle{display:none;position:fixed;top:12px;left:12px;z-index:20; | |
| background:var(--bg);border:1px solid var(--border);border-radius:var(--radius); | |
| padding:8px 12px;cursor:pointer;font-size:18px;box-shadow:var(--shadow)} | |
| @media(max-width:768px){ | |
| .sidebar{transform:translateX(-100%);transition:transform .2s} | |
| .sidebar.open{transform:translateX(0);box-shadow:2px 0 12px rgba(0,0,0,.1)} | |
| .content{margin-left:0;padding:24px 20px;padding-top:56px} | |
| .menu-toggle{display:block} | |
| } | |
| .empty-state{text-align:center;padding:80px 20px;color:var(--text-muted)} | |
| .empty-state h2{font-size:20px;margin-bottom:8px;border:none} | |
| </style> | |
| </head> | |
| <body> | |
| <button class="menu-toggle" id="menu-toggle" aria-label="Toggle menu">☰</button> | |
| <div class="layout"> | |
| <nav class="sidebar" id="sidebar"> | |
| <div class="sidebar-header"> | |
| <div class="sidebar-title"> | |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/></svg> | |
| Harakat | |
| </div> | |
| <div class="sidebar-meta" id="meta-info"></div> | |
| </div> | |
| <div id="nav-tree"></div> | |
| <div class="sidebar-footer">Generated by GitNexus</div> | |
| </nav> | |
| <main class="content" id="content"> | |
| <div class="empty-state"><h2>Loading…</h2></div> | |
| </main> | |
| </div> | |
| <script> | |
| var PAGES = {"deploy-inference":"# deploy — inference\n\n\n\n# deploy/inference — Inference Deployment Module\n\n## Overview\n\nThe `deploy/inference` module provides the infrastructure for running vLLM-based inference services on GPU workstations. It handles provisioning, configuration, and operational management of Qwen3.6-35B-A3B-FP8 multimodal models for document processing pipelines.\n\nThe module consists of:\n- **Provisioning scripts** (`provision.sh`) — Initial box setup and vLLM server launch\n- **Box management scripts** (`box-up.sh`) — Runtime configuration and OCR cleanup\n- **Performance benchmarks** — Throughput measurements and capacity planning\n- **Pipeline integration** — Connection to the v2 document processing system\n\n## Architecture\n\n```mermaid\ngraph TB\n A[Pipeline V2] -->|document requests| B[Inference Client]\n B -->|HTTP 8000+N| C[vLLM Shards]\n C -->|GPU| D[RTX PRO 6000]\n D -->|VRAM| E[Qwen3.6-35B-A3B-FP8]\n F[provision.sh] -->|setup| C\n G[box-up.sh] -->|runtime config| C\n H[ensure.sh] -->|health check| C\n I[status.sh] -->|monitoring| C\n```\n\n## Key Components\n\n### 1. Provisioning (`provision.sh`)\n\n**Purpose:** Blank box → N vLLM servers in one command.\n\n**Key behaviors:**\n- Installs vLLM via `uv` into `/venv/vllm` (faster than pip)\n- Downloads FP8 checkpoint to `/workspace/huggingface-cache`\n- Launches ONE server per GPU with isolated ports (8000+N)\n- Writes process-control scripts to `/workspace`\n\n**Critical flags:**\n```bash\n--max-model-len 69632\n--gpu-memory-utilization 0.90\n--max-num-seqs 64\n--max-num-batched-tokens 32768\n--enable-chunked-prefill\n--limit-mm-per-prompt '{\"image\": 4}'\n```\n\n**Process control scripts generated:**\n- `launch-shard.sh` — Individual shard launcher\n- `status.sh` — Health monitoring\n- `ensure.sh` — Auto-restart dead shards\n- `stop.sh` — Clean shutdown\n\n**Warning:** Never use `--language-model-only` — this hides the vision tower and breaks multimodal inference.\n\n### 2. Box Management (`box-up.sh`)\n\n**Purpose:** Runtime configuration after the owner starts the box.\n\n**Key operations:**\n1. **Clears OCR from VRAM** — Kills `DeepSeek-OCR` and `glm-ocr` processes\n2. **Launches Qwen vision** — Full multimodal model (not `--language-model-only`)\n3. **Falls back** — Tries JSON form of `--limit-mm-per-prompt`, then key=value\n\n**Port note:** Uses `127.0.0.1:18010` for the tunnel, not 18000 (which is the F2LLM query embedder).\n\n### 3. Process Control Scripts\n\n**`status.sh`** — Ground truth for client feeding:\n```bash\n# Reads from each server's log file\n# GPU-% is worthless (reads 100% while starving at 1 request)\n# Running: N reqs is the actual metric\n```\n\n**`ensure.sh`** — Relaunches dead shards:\n```bash\n# Checks HTTP /v1/models endpoint\n# Relaunches by bare path, not pattern (pkill -f matches the script itself)\n# Run from cron every 2 minutes\n```\n\n**`stop.sh`** — Clean shutdown:\n```bash\n# Kills by PID file first\n# Then kills EngineCore children (VLLM::EngineCore pattern doesn't match)\n# Uses nvidia-smi to find PIDs holding VRAM\n```\n\n## Configuration of Record\n\n**Server flags** (`/workspace/vllm.flags.BEST`):\n```bash\nvllm serve Qwen/Qwen3.6-35B-A3B-FP8 \\\n --host 127.0.0.1 --port 8000 \\\n --reasoning-parser qwen3 \\\n --max-model-len 69632 \\\n --gpu-memory-utilization 0.90 \\\n --max-num-seqs 512 \\\n --max-num-batched-tokens 8192 \\\n --enable-chunked-prefill \\\n --kv-cache-dtype fp8 \\\n --limit-mm-per-prompt '{\"image\":4}' \\\n --api-server-count 4\n```\n\n**Not set (with reasons):**\n- `--enable-prefix-caching` — Measured worthless (only ~50-token shared prefix)\n- `--mm-processor-cache-gb > 0` — Every page is distinct in production\n- Higher `--gpu-memory-utilization` — KV reaches 100% at ceiling; tradeoff is 6% throughput for 1.7× latency\n\n## Performance Characteristics\n\n### Single Card Ceiling (RTX PRO 6000 Blackwell)\n\n| Workload | total tok/s | pages/min | cards/min | KV max |\n|----------|-------------|-----------|-----------|--------|\n| page only | 5,607.5 | 99.0 | — | 0.835 |\n| mixed (1 card/5 pages) | 6,022.8 | 65.8 | 12.5 | 0.872 |\n| card only | 8,175.7 | — | 49.0 | 0.873 |\n\n**Operating point:** 5,607 tok/s at 384 offered concurrency, no preemption, KV at 83%\n\n**Peak:** 5,960 tok/s at 768 offered, KV at 100%, preemptions active\n\n### Throughput vs. Concurrency\n\n| offered | total tok/s | running | queued | limiter |\n|---------|-------------|---------|--------|---------|\n| 256 | 5,330 | 147.9 | 1.2 | front-end |\n| 384 | 5,607.5 | 218.8 | 5.0 | KV queue forming |\n| 512 | 5,608.9 | 278.6 | 17.5 | preemption |\n| 768 | 5,959.7 | 315.5 | 151.1 | preemption |\n\n**Two caps:**\n1. **API server pinned at ~1 core** — Single process limits request intake\n2. **`--max-num-seqs`** — Engine stops accepting when this is hit\n\n**Solution:** `--api-server-count 4` + `--max-num-seqs 512` + multi-process driver\n\n### Reasoning ON vs OFF\n\n| Setting | total tok/s | pages/min | cards/min | p50 |\n|---------|-------------|-----------|-----------|-----|\n| OFF | 6,022.8 | 65.8 | 12.5 | 226s |\n| ON | 4,409.8 | 34.1 | 6.82 | 315s |\n\n**Ratio:** 1.37× throughput, 1.93× pages/min, 1.84× cards/min\n\n**Quality:** Identical identifiers on 50 samples (0 lost in either setting)\n\n## Pipeline Integration\n\n### Document Processing Flow\n\n```mermaid\ngraph LR\n A[Document] -->|Lane 2/3| B[GPU Inference]\n B -->|card| C[Harakat_AI POST /parse/card]\n B -->|page| D[HarakatOffices_Server readDocumentV2]\n C -->|503| E[Retryable - host off]\n C -->|422| F[Stop re-queueing]\n D -->|store.ts rules| G[corpus-v2/reading.ts]\n G -->|unavailable ≠ failed| H[search-index queue]\n```\n\n### Lane Selection\n\n| Lane | Selection | count |\n|------|-----------|-------|\n| 3 — file types | `ext:xlsx,xls,tif,heic,zip,rar,doc,msg` | 2,654 |\n| 2 — Arabic re-read | `arabic:50` (≥50 Arabic chars, no Arabic in card) | 1,351 |\n| 1 — pageless | `by_type_handler` ⊂ lane 3 | 103 |\n\n**Lane 4 (DWG)** — Out of scope by owner instruction\n\n### Type Prep Converter\n\n**Supported formats:** `xlsx xls tif heic zip rar doc msg`\n\n**Key changes from v1:**\n- `openpyxl` instead of `pandas` (60 MB reduction)\n- `lsar` instead of `unrar`\n- Page attribution as member\n- LibreOffice per-call profile (single-instance lock)\n\n**Not supported:** `mpp` — No converter exists\n\n## Operational Procedures\n\n### Starting a New Box\n\n```bash\n# 1. Provision the box\nbash deploy/inference/provision.sh <instance_id>\n\n# 2. Set up the tunnel\nbash scripts/inference-tunnel.sh up <instance_id>\n\n# 3. Preflight checks\nbash scripts/pipeline-v2/preflight.sh --box\n\n# 4. Keep shards alive (cron every 2 min)\nssh -p <port> root@<ip> 'nohup bash -c \"while true; do bash /workspace/ensure.sh; sleep 120; done\" >/workspace/ensure.log 2>&1 &'\n```\n\n### Monitoring\n\n```bash\n# Check shard status\nbash /workspace/status.sh\n\n# Check GPU memory\nnvidia-smi --query-gpu=index,memory.used,memory.total,utilization.gpu --format=csv,noheader\n\n# Check vLLM health\ncurl -s http://127.0.0.1:8000/v1/models\n```\n\n### Stopping\n\n```bash\n# Clean shutdown\nbash /workspace/stop.sh\n```\n\n## Troubleshooting\n\n### Shard won't start\n\n**Symptoms:** `shard N DID NOT COME UP` in provision output\n\n**Check:**\n```bash\ntail -20 /workspace/shardN.log | grep -vE \"warnings.warn|softmax\"\n```\n\n**Common causes:**\n- VRAM contention during sequential launch (wait 15s between shards)\n- `--limit-mm-per-prompt` flag drift (JSON vs key=value fallback)\n- EngineCore child holding VRAM after API server kill\n\n### Dead shard\n\n**Symptoms:** `Running: 0 reqs` in status output\n\n**Fix:** `bash /workspace/ensure.sh` or manual relaunch\n\n### Client underfeeding\n\n**Symptoms:** Server running < 150 reqs at 256 offered\n\n**Diagnosis:**\n```bash\n# Check API server cores\n# APIServer cores busy: 1.06 (452 threads, one process)\n# EngineCore cores busy: 4.05\n```\n\n**Solution:** Increase `--api-server-count` and `--max-num-seqs`\n\n### KV cache not filling\n\n**Symptoms:** KV max < 80% at high concurrency\n\n**Cause:** `--max-num-seqs` too low\n\n**Fix:** Raise to 512+ (measured ceiling at 768)\n\n## Cost Estimates\n\n**Single card:**\n- ~$1.04/hr\n- ~5,710 pages per dollar\n- ~99 pages/min at operating point\n\n**Lane 2+3 (3,649 documents):**\n- ~5.3 hours with reasoning OFF\n- ~11.2 hours with reasoning ON\n- ~$5.50–$6.03 total\n\n**Per-item cost:**\n- One page read: ~3,400 tokens\n- One document card: ~10,000 tokens\n\n## Connection to Other Modules\n\n### `deploy/inference/` → `scripts/pipeline-v2/`\n\n- `preflight.sh --box` validates inference readiness\n- `owedPageReads` / `owedCardBuilds` queues drive GPU work\n- `readDocumentV2` + `buildCardV2` implement store.ts rules\n\n### `deploy/inference/` → `HarakatOffices_Server`\n\n- `POST /parse/card` — Card generation endpoint\n- `core/corpus-v2/reading.ts` — Reading path implementation\n- `corpus-v2:read` — Always-full queues for new documents\n\n### `deploy/inference/` → `type_prep`\n\n- `POST /parse/extract` — Type conversion endpoint\n- `type_prep.py` — File format converters (openpyxl, lsar, LibreOffice)\n\n## Notes\n\n- **No OCR model** — DeepSeek-OCR killed, Qwen-vision is the only reader\n- **No vector embedding** — `enqueueVectorIndex` not in scope (track Z7)\n- **No DWG processing** — Lane 4 out of scope by owner instruction\n- **No manual loading** — Lane output travels the product's own path\n- **No manifest-driven work** — Queues driven by database predicates","deploy-local":"# deploy — local\n\n\n\n# deploy/local\n\n## Overview\n\nThis module defines the canonical local development profile for the Harakat stack. Its primary purpose is to enforce a coherent local environment that prevents services from silently pointing at production infrastructure.\n\nThe recurring hazard identified in the test pipeline (2026-06-30) involved three independent configurations drifting to production. This profile, combined with the boot guard, mitigates this risk by enforcing strict validation at startup.\n\n## Safety Guard Mechanism\n\nThe core safety mechanism relies on the `HARAKAT_LOCAL=1` environment variable. When this variable is set, the server and AI service boot guards refuse to start if specific conditions are met.\n\n### Guard Implementation\n\nTwo distinct guard functions enforce this logic across the stack:\n\n1. **Server Guard**: `assertLocalProfileCoherent`\n * **Location**: `../../HarakatOffices_Server/src/core/config/local-guard.ts`\n * **Behavior**: Refuses to boot if any service URL contains `harakat.tech` or if an environment key is BOM-corrupted.\n2. **AI Guard**: `assert_local_profile_coherent`\n * **Location**: `../../Harakat_AI/core/local_guard.py`\n * **Behavior**: Refuses to boot if any service URL contains `harakat.tech` or if an environment key is BOM-corrupted.\n\n### Validation Logic\n\nWhen `HARAKAT_LOCAL=1` is active, the guards perform the following checks:\n\n1. **URL Validation**: Scans configuration URLs for the production domain `harakat.tech`.\n2. **Encoding Validation**: Detects UTF-8 BOM (Byte Order Mark) prefixes on environment keys. A BOM at the start of an env file corrupts the first key (e.g., `NEXT_PUBLIC_API_URL`), potentially causing silent misconfiguration.\n\nIf either check fails, the service aborts startup with a clear error message. In production, `HARAKAT_LOCAL` is unset, allowing normal booting.\n\n## Environment Configuration\n\nThe following table lists the canonical environment variables required for a valid local profile.\n\n| Service | Key | Local Value | Notes |\n|---|---|---|---|\n| **FE** | `NEXT_PUBLIC_API_URL` | `http://localhost:5000` | |\n| **Server** | `CLIENT_ORIGIN` | `http://localhost:3000` | |\n| **Server** | `STARTUP_PLATFORM_URL` | `http://localhost:3000` | |\n| **Server** | `CUSTOM_EMAIL_REDIRECT_URI` | `http://localhost:3000/oauth-callback` | |\n| **Server** | `WEBAUTHN_ORIGIN` | `http://localhost:3000` | |\n| **Server** | `HARAKAT_LOCAL` | `1` | **Required for guard** |\n| **AI** | `HARAKAT_API_URL` | `http://localhost:5000` | |\n| **AI** | `STARTUP_PLATFORM_URL` | `http://localhost:3000` | |\n| **AI** | `HARAKAT_LOCAL` | `1` | **Required for guard** |\n| **ERP** | `ERP_PER_EMPLOYEE` | `on` | |\n| **ERP** | `ERP_ANCHORING_ENABLED` | `on` | |\n\n### File Hierarchy\n\n* **Production Hosts**: Stored in the prod `.env` file.\n* **Local Overrides**: Stored in submodule-specific `.env.local` files (gitignored).\n* **Secrets**: Real secret values remain in `.env.local` and are not committed.\n* **Canonical Port**: The Frontend canonical port is `:3000` (where `next dev` runs). Earlier values like `:3002` caused mismatches (Gap E) and should not be used.\n\n## Bringup Procedure\n\nFollow this sequence to ensure a consistent local stack.\n\n### 1. Infrastructure & ERP\n\nStart the infrastructure and ERP services using the server's environment file.\n\n```bash\ndocker compose -p harakat -f docker-compose.yml --env-file HarakatOffices_Server/.env up -d\n```\n\n### 2. Odoo Port Verification\n\nEnsure the provisioned Odoo instance serves on port `:8069`. A stray `erp-erp_odoo-1` stack must be stopped to prevent port squatting with an empty addon-less DB (which causes `Object erp.gateway doesn't exist` errors).\n\n```bash\ndocker stop erp-erp_odoo-1 erp-erp_db-1 2>/dev/null\ndocker compose -p harakat -f docker-compose.yml --env-file HarakatOffices_Server/.env up -d erp\n```\n\n### 3. Server\n\nNavigate to the server directory and start the development server. It loads `.env` then `.env.local`.\n\n```bash\ncd HarakatOffices_Server && pnpm dev\n```\n\n### 4. AI Service\n\nNavigate to the AI directory and run uvicorn. It loads `.env` then `.env.local` with overrides.\n\n```bash\ncd Harakat_AI && <run uvicorn>\n```\n\n### 5. Frontend\n\nNavigate to the frontend directory.\n\n```bash\ncd StartupsPlatform && pnpm dev -p 3000\n```\n*Note: Use a prod build for crawls if required.*\n\n## Known Hazards\n\n### UTF-8 BOM Corruption\n\nA UTF-8 BOM at the top of an environment file corrupts the **first key** (e.g., `NEXT_PUBLIC_API_URL`). This was the root cause of the FE silently pointing at production.\n\n* **Detection**: The guard detects any BOM-prefixed env key.\n* **Prevention**: Always save `.env*` files as **UTF-8 without BOM**.\n\n### Port Conflicts\n\n* **ERP**: Ensure `erp-erp_odoo-1` is stopped before starting the ERP stack to avoid port `:8069` conflicts.\n* **FE**: Ensure `:3000` is free. The `:3002` port was previously used but caused passkey origin mismatches (Gap E).","deploy-nginx":"# deploy — nginx\n\n\n\n# deploy/nginx\n\nThis module contains the Nginx configuration for the `harakat.tech` infrastructure. It acts as a reverse proxy, SSL terminator, and load balancer for multiple internal services. The configuration is modularized to separate global settings, security parameters, and domain-specific routing.\n\n## Overview\n\nThe Nginx configuration manages traffic for four primary domains:\n* `harakat.tech` (Landing Page)\n* `app.harakat.tech` (Startups Platform)\n* `admin.harakat.tech` (Admin Dashboard)\n* `api.harakat.tech` (Backend API)\n\nThe system enforces HTTPS, implements rate limiting, and proxies requests to internal containers (`landing`, `startups`, `admin`, `server`, `ai`).\n\n## Architecture\n\nThe configuration follows a layered approach:\n1. **Global Settings:** Defined in `nginx.conf`.\n2. **Security & Proxy Defaults:** Defined in `ssl-params.conf` and `proxy-params.conf`.\n3. **Domain Routing:** Defined in `conf.d/` (HTTP and HTTPS blocks).\n\n```mermaid\ngraph TD\n Client[Client] -->|HTTPS/HTTP| Nginx[Nginx]\n Nginx -->|Proxy| Landing[landing:3000]\n Nginx -->|Proxy| Startups[startups:3000]\n Nginx -->|Proxy| Admin[admin:3000]\n Nginx -->|Proxy| Server[server:5000]\n Nginx -->|Proxy| AI[ai:8000]\n Nginx -->|Certbot| CertbotDir[/var/www/certbot]\n```\n\n## Configuration Files\n\n### `nginx.conf`\nThe main entry point. It defines global worker processes, logging, compression, and rate limiting zones.\n\n* **Workers:** `worker_processes auto` to utilize all CPU cores.\n* **Logging:** Access and error logs are written to `/var/log/nginx/`.\n* **Compression:** Gzip is enabled for text, JSON, and JS assets.\n* **Rate Limiting:** Two zones are defined:\n * `api`: 30 requests/second (10m memory).\n * `login`: 5 requests/minute (10m memory).\n* **Includes:** Loads all `.conf` files from `/etc/nginx/conf.d/`.\n\n### `00-http.conf`\nTemporary configuration used during SSL certificate issuance. It listens on port 80 and performs two functions:\n1. **Certbot Challenge:** Serves files from `/var/www/certbot` for ACME validation.\n2. **Redirect:** Returns a 301 redirect to the HTTPS version of the same host.\n\n**Domains Covered:**\n* `harakat.tech`\n* `app.harakat.tech`\n* `admin.harakat.tech`\n* `api.harakat.tech`\n\n### `01-https.conf`\nProduction configuration. This file is activated after SSL certificates are obtained. It listens on port 443 with HTTP/2 support.\n\n* **SSL Certificates:** Points to Let's Encrypt paths (`/etc/letsencrypt/live/harakat.tech/`).\n* **Proxying:** Routes traffic to internal services based on `server_name`.\n* **Special Routing (API):**\n * `/api/ai/`: Strips the prefix and proxies to `ai:8000` with a 120s timeout.\n * `/api/(auth|startup-auth)/login`: Rate limited to 5 requests/minute.\n * `/`: General API traffic rate limited to 30 requests/second.\n\n### `proxy-params.conf`\nShared proxy headers and timeouts applied to all proxied requests.\n\n* **Headers:** Sets `Host`, `X-Real-IP`, `X-Forwarded-For`, and `X-Forwarded-Proto` to ensure backend services see the original client details.\n* **Timeouts:** Default `proxy_read_timeout` and `proxy_send_timeout` are set to 60s.\n* **Upgrade:** Supports WebSocket upgrades (`Upgrade` and `Connection` headers).\n\n### `ssl-params.conf`\nShared SSL/TLS hardening settings.\n\n* **Protocols:** Enforces TLSv1.2 and TLSv1.3.\n* **Ciphers:** Restricts to ECDHE ciphers with AES-GCM.\n* **Session Management:** Caches sessions for 10 minutes, disables session tickets.\n* **HSTS:** Adds `Strict-Transport-Security` header with a 1-year max-age.\n* **OCSP Stapling:** Enabled for performance and privacy.\n\n## Service Routing\n\nThe following table maps external domains to internal services.\n\n| Domain | Internal Service | Port | Purpose |\n| :--- | :--- | :--- | :--- |\n| `harakat.tech` | `landing` | 3000 | Landing Page |\n| `app.harakat.tech` | `startups` | 3000 | Startups Platform |\n| `admin.harakat.tech` | `admin` | 3000 | Admin Dashboard |\n| `api.harakat.tech` | `server` | 5000 | Backend API |\n| `api.harakat.tech` | `ai` | 8000 | AI Proxy |\n\n## Security & Performance\n\n### SSL/TLS\nAll traffic is terminated at Nginx. The configuration enforces modern TLS standards:\n* **Protocols:** Only TLSv1.2 and TLSv1.3 are allowed.\n* **Ciphers:** Strong ECDHE ciphers are prioritized.\n* **HSTS:** Prevents downgrade attacks via the `Strict-Transport-Security` header.\n\n### Rate Limiting\nRate limiting is applied at the Nginx level to protect backend services from abuse.\n\n* **Login Endpoints:** `/api/(auth|startup-auth)/login` are limited to 5 requests per minute (`zone=login`).\n* **General API:** All other API routes are limited to 30 requests per second (`zone=api`).\n* **AI Proxy:** The `/api/ai/` route allows 20 requests per second (`zone=api`) but has extended timeouts for heavy computation.\n\n### Security Headers\nGlobal headers are applied to all responses:\n* `X-Content-Type-Options: nosniff`\n* `X-Frame-Options: SAMEORIGIN`\n* `X-XSS-Protection: 1; mode=block`\n* `Referrer-Policy: strict-origin-when-cross-origin`\n\n## Deployment Workflow\n\n### 1. Initial Setup\n1. Ensure `00-http.conf` is active.\n2. Run Certbot to obtain SSL certificates for `harakat.tech` (and subdomains).\n3. Certbot will place challenge files in `/var/www/certbot`.\n\n### 2. SSL Activation\n1. Once certificates are verified, ensure `01-https.conf` is active.\n2. Verify certificate paths in `01-https.conf` match the Let's Encrypt installation location.\n3. Reload Nginx: `nginx -s reload`.\n\n### 3. Maintenance\n* **Certificate Renewal:** Certbot handles renewal automatically. Nginx config paths remain valid.\n* **Service Updates:** If internal service ports change, update `proxy_pass` directives in `01-https.conf`.\n* **Rate Limiting:** Adjust `limit_req_zone` definitions in `nginx.conf` if traffic patterns change.\n\n## Troubleshooting\n\n* **502 Bad Gateway:** Check if the internal service (e.g., `landing:3000`) is running and accessible from the Nginx container/host.\n* **403 Forbidden:** Verify file permissions on `/var/www/certbot` if ACME challenges fail.\n* **SSL Errors:** Check `/var/log/nginx/error.log` for certificate path mismatches or protocol errors.\n* **Rate Limiting:** If legitimate traffic is blocked, increase the `rate` value in `nginx.conf` or adjust `burst` values in `01-https.conf`.","deploy-openbao":"# deploy — openbao\n\n\n\n# deploy/openbao\n\nThis module configures and manages the OpenBao instance used by Harakat for encrypt-at-rest (P1) of the corpus. It consists of a persistent configuration file, a Docker environment setup, and a sidecar script to handle the auto-unsealing process required for single-node deployments.\n\n## Overview\n\nOpenBao is deployed as a single-node instance to provide a persistent Key Management System (KMS). Unlike standard development setups, a production OpenBao instance starts in a **SEALED** state after every restart. This module ensures the instance remains unsealed automatically to allow continuous corpus reads and writes without manual intervention.\n\n## Components\n\nThe module comprises three primary files:\n\n1. **`config.hcl`**: The core OpenBao configuration defining storage, listeners, and lease TTLs.\n2. **`bao.env`**: Docker environment variables used to inject the configuration into the container.\n3. **`auto-unseal.sh`**: A shell script sidecar that monitors the OpenBao status and performs unsealing operations using stored keys.\n\n## Architecture\n\nThe deployment relies on a persistent volume for data storage and a sidecar pattern for lifecycle management.\n\n### Startup Flow\n\nWhen the container starts, OpenBao initializes in a sealed state. The `auto-unseal.sh` script runs concurrently, polling the status and attempting to unseal the instance if necessary.\n\n```mermaid\nsequenceDiagram\n participant Docker as Docker Compose\n participant OpenBao as OpenBao Container\n participant Sidecar as auto-unseal.sh\n participant Keys as /keys/unseal-keys.txt\n\n Docker->>OpenBao: Start Container\n OpenBao->>OpenBao: Initialize Storage\n OpenBao-->>OpenBao: State: SEALED\n Sidecar->>OpenBao: Check Status (bao status)\n OpenBao-->>Sidecar: Exit Code 2 (Sealed)\n Sidecar->>Keys: Read Unseal Keys\n Sidecar->>OpenBao: Unseal Key (bao operator unseal)\n OpenBao-->>OpenBao: State: UNSEALED\n Sidecar->>OpenBao: Verify Status\n OpenBao-->>Sidecar: Exit Code 0 (Unsealed)\n Sidecar->>Sidecar: Exit Loop\n```\n\n## Configuration Details\n\n### `config.hcl`\n\nThis file defines the operational parameters for the OpenBao instance.\n\n* **Storage**: Uses file-based storage at `/openbao/data`. This path is mounted via a Docker named volume to persist data across container restarts.\n* **Listener**: Binds to `0.0.0.0:8200`. TLS is disabled (`tls_disable = true`) for internal communication.\n* **Lease TTLs**:\n * `default_lease_ttl`: 168 hours (7 days).\n * `max_lease_ttl`: 720 hours (30 days).\n* **UI**: Disabled (`ui = false`) to reduce the attack surface.\n\n### `bao.env`\n\nThis file provides the configuration to the Docker container via environment variables.\n\n* `BAO_LOCAL_CONFIG`: A JSON string containing the configuration from `config.hcl`. This avoids the need to mount the HCL file directly in some Docker setups, though the source code indicates `config.hcl` is also mounted.\n* **Note**: The `config.hcl` file takes precedence or is used alongside this depending on the specific Docker entrypoint configuration in the parent `docker-compose.yml`.\n\n### `auto-unseal.sh`\n\nThis script runs as a sidecar process. It implements a polling loop to manage the unsealing state.\n\n* **Status Check**: Uses `bao status -address=\"$ADDR\"`.\n * Exit code `0`: Instance is unsealed.\n * Exit code `2`: Instance is sealed.\n * Other: Instance is unreachable.\n* **Unseal Logic**:\n * If sealed (code `2`) and the keys file exists, it iterates through `/keys/unseal-keys.txt`.\n * It calls `bao operator unseal` for each key until the status returns to unsealed.\n * It uses `|| true` to prevent a single failed key from stopping the loop, allowing it to try subsequent keys.\n* **Polling Interval**: Sleeps for 10 seconds between checks.\n\n## Key Files and Secrets\n\n### Unseal Keys\n\n* **Location**: `/keys/unseal-keys.txt` (mounted from `deploy/openbao/unseal-keys.local.txt`).\n* **Format**: One key per line.\n* **Management**: This file is **gitignored**. It must be backed up externally. Losing these keys orphans the corpus data, as the OpenBao instance cannot be unsealed without them.\n\n## Security Considerations\n\n### Local Key Storage\nThe unseal keys are stored on the host machine in `deploy/openbao/unseal-keys.local.txt`.\n* **Risk**: Any operator with access to the box can unseal the instance.\n* **Mitigation**: This is an acceptable interim measure for the current single pre-prod tenant.\n* **Future**: True operator-exclusion requires HSM, cloud-KMS auto-unseal, or confidential compute.\n\n### Network Exposure\n* The listener binds to `0.0.0.0:8200` but is only published via the Docker port mapping.\n* The UI is disabled to prevent direct web access.\n* TLS is disabled for internal communication; ensure the network path between the sidecar and OpenBao is trusted.\n\n## Operational Notes\n\n* **Backup**: Regularly back up `deploy/openbao/unseal-keys.local.txt` and the Docker volume containing `/openbao/data`.\n* **Restart Behavior**: If the container restarts, OpenBao will seal. The sidecar will automatically detect this and unseal the instance using the keys file.\n* **Dependencies**: Requires the OpenBao CLI (`bao`) to be available in the environment where `auto-unseal.sh` executes.","deploy-rooms":"# deploy — rooms\n\n\n\n# deploy/rooms — Harakat Rooms Relay Module\n\n## Overview\n\nThe `deploy/rooms` module implements **one relay per company** for Harakat's rooms substrate. It provisions and operates a dedicated Buzz relay instance (Harakat fork) for each tenant, with complete isolation from other tenants and platform services.\n\nThis module tracks:\n- **T2.1** — vendor-extraction build of the rooms substrate\n- **A1** — the rooms substrate itself\n- **B3, B5, B9** — fork patches applied to `vendor/buzz`\n\n### Core Decision\n\n**One relay per company**, not one shared relay with communities per company.\n\nThis decision was made on 2026-08-03 because `vendor/buzz` has no database-level tenant isolation. On a shared relay, the only thing keeping companies apart is application code we patch in. On the same day, our own per-route opt-in tenancy check was measured leaking on **112 of 130 routes**.\n\nThe cost is honest: one Rust binary plus Postgres, Redis, and MinIO **per company**.\n\n---\n\n## Architecture\n\n### Per-Tenant Stack\n\nEach tenant gets a complete, isolated stack:\n\n```mermaid\ngraph TB\n subgraph \"Tenant N Network (harakat-rooms-tN)\"\n DB[Postgres]\n REDIS[Redis]\n MINIO[MinIO]\n end\n \n subgraph \"Rooms Network (harakat_rooms-net)\"\n RELAY[Buzz Relay]\n SERVER[HarakatOffices_Server]\n end\n \n RELAY -->|DNS alias| DB\n RELAY -->|DNS alias| REDIS\n RELAY -->|DNS alias| MINIO\n RELAY -.->|authenticated API| SERVER\n```\n\n**Key isolation properties:**\n\n| Property | Implementation |\n|----------|----------------|\n| **Network isolation** | Tenant's Postgres/Redis/MinIO live only on `harakat-rooms-tN` network |\n| **DNS isolation** | Hostnames are `tN-db`, `tN-redis`, `tN-minio` — cannot resolve to platform services |\n| **No host ports** | Tenant data stores publish no host ports; unreachable from outside their network |\n| **Shared network** | Only `harakat_rooms-net` carries the relay; platform services are not on it |\n\n### Network Topology\n\n```mermaid\ngraph LR\n subgraph \"Platform Network (harakat_harakat-net)\"\n PLATFORM_DB[Platform Postgres]\n PLATFORM_REDIS[Platform Redis]\n PLATFORM_QDRANT[Platform Qdrant]\n PLATFORM_OPENBAO[Platform OpenBao]\n end\n \n subgraph \"Rooms Network (harakat_rooms-net)\"\n SERVER[HarakatOffices_Server]\n end\n \n subgraph \"Tenant N Network (harakat-rooms-tN)\"\n RELAY[Relay]\n TENANT_DB[Tenant Postgres]\n TENANT_REDIS[Tenant Redis]\n TENANT_MINIO[Tenant MinIO]\n end\n \n SERVER -->|container name| RELAY\n RELAY -->|DNS alias| TENANT_DB\n RELAY -->|DNS alias| TENANT_REDIS\n RELAY -->|DNS alias| TENANT_MINIO\n \n PLATFORM_DB -.->|NOT reachable| RELAY\n PLATFORM_REDIS -.->|NOT reachable| RELAY\n PLATFORM_QDRANT -.->|NOT reachable| RELAY\n PLATFORM_OPENBAO -.->|NOT reachable| RELAY\n```\n\n**Critical correction (2026-08-04):** Until this date, the relay joined `harakat_harakat-net`, where Docker DNS gives every service a bare-name alias (`db`, `redis`, `qdrant`, `openbao`). Measured from inside a tenant relay with its own database stopped, `db` resolved to the **platform's Postgres** (172.18.0.5). The relay failed closed only because no `buzz` role exists there — luck, not design.\n\nThe fix is structural:\n1. **`harakat_rooms-net`** — declared in meta `docker-compose.yml`, carries only `server` from the platform side\n2. **Per-tenant hostnames** — `tN-db`, `tN-redis`, `tN-minio` — the relay's configuration cannot name a platform service\n\n---\n\n## Components\n\n### Dockerfile.relay\n\nBuilds the relay image from `vendor/buzz` with two deliberate differences:\n\n1. **No web bundle** — `BUZZ_WEB_DIR` and `BUZZ_ADMIN_WEB_DIR` are never set. The invite landing page and admin SPA are not served. The npm supply chain is out of the build. The relay's WS + REST surface — the only thing Harakat talks to — is unchanged.\n\n2. **No cargo-chef** — Upstream uses it to cache dependency layers across CI builds. We build this image rarely and from a pinned fork, so the extra `cargo install` buys nothing here.\n\n**Build command:**\n```sh\ndocker build -f deploy/rooms/Dockerfile.relay -t harakat/buzz-relay:local vendor/buzz\n```\n\n**Exposed ports:**\n- `3000` — app (WS + REST)\n- `8080` — `/liveness`, `/readiness`\n- `9102` — `/metrics`\n\n### compose.relay.yml\n\nTemplate instantiated per tenant. Never a stack you bring up once. Every run is scoped by `HARAKAT_TENANT_ID`:\n\n```sh\nbash scripts/rooms-relay.sh up 19\n```\n\n**Services:**\n\n| Service | Purpose |\n|---------|---------|\n| `relay` | Buzz relay binary |\n| `db` | Postgres (tenant-specific) |\n| `redis` | Redis (tenant-specific) |\n| `minio` | MinIO S3 (tenant-specific) |\n| `minio-init` | Bucket initialization |\n\n**Volumes:** All four are per-tenant by virtue of the compose project name. Tenant 19's `pg-data` is `harakat-rooms-t19_pg-data` and cannot be mounted by tenant 20's stack.\n\n**Networks:**\n- `tenant` — private to this tenant; Postgres, Redis, MinIO live only here\n- `rooms` — external, declared in main `docker-compose.yml`; only `server` joins it\n\n### relay-probe.py\n\nBehavior probe that proves the three fork patches against a live relay.\n\n**Usage:**\n```sh\npython3 deploy/rooms/relay-probe.py \\\n --base-url http://127.0.0.1:3919 \\\n --host 127.0.0.1:3919 \\\n --owner-sk <64-hex> \\\n [--peer-base-url ... --peer-host ... --peer-owner-sk ...]\n```\n\n**What it asserts:**\n\n| Patch | Assertion |\n|-------|-----------|\n| **B9** | A registered Harakat kind (50001, 50010) is accepted |\n| **B9** | An unassigned number in our block (50002) is refused |\n| **B9** | A Harakat kind with no `h` tag is refused |\n| **B3** | A kind upstream knows but this community does not allowlist is refused |\n| **B5** | A channel Guest cannot post |\n| **B5** | The same Guest can still read |\n| **B5** | A Member can post (negative control) |\n| **Isolation** | A peer relay holds none of this tenant's room |\n\n**Implementation details:**\n- Uses pure Python BIP-340 Schnorr implementation (reference algorithm, stdlib only)\n- No dependencies — must run wherever the relay runs\n- Signs real Nostr events with NIP-98 auth\n\n### relay-load.py\n\nLoad testing script for measuring relay performance under known conditions.\n\n**Usage:**\n```sh\npython3 deploy/rooms/relay-load.py \\\n --base-url http://127.0.0.1:3901 \\\n --host 127.0.0.1:3901 \\\n --owner-sk <64-hex> \\\n --messages 2000 \\\n --media 20 \\\n --media-bytes 262144\n```\n\n**What it creates:**\n- A relay roster entry per synthetic member\n- `--rooms` channels owned by the relay owner\n- `--messages` kind-40002 room messages spread evenly over them\n- `--media` Blossom (BUD-02) blob uploads of `--media-bytes` each\n\n**Output metrics:**\n- `events_per_second_CLIENT_BOUND` — floor on the relay (bottleneck is the load generator)\n- `post_ms_p50`, `post_ms_p95`, `post_ms_max` — HTTP call latency\n- `bytes_uploaded` — honest storage number (random bytes, no deduplication)\n\n**Important:** Signing is the reference BIP-340 algorithm in pure Python — about 63 ms per signature on this box. At 10 worker processes, the client tops out near 80 events/second, pegging 10 cores.\n\n---\n\n## Fork Patches\n\nWe write **no relay code**. We run their binary and patch the fork, minimally and at seams, because every line we add is a line a future rebase can conflict on.\n\n### B9 — Register Harakat Event Kinds\n\nThe relay refuses any kind it does not know. Three kinds in a reserved `50000–50099` block, all channel-scoped by construction.\n\n**Registered kinds:**\n- `50001` — assistant turn\n- `50010` — grant announce\n\n**Unassigned numbers in the block are still unknown** — reserving ≠ registering.\n\n### B3 — Per-Community Ingest Kind Allowlist\n\nRoughly forty upstream kinds are channel-less and therefore readable community-wide. Per-company isolation makes that one company instead of the fleet; it does not make it right.\n\nEach community accepts the kinds its product uses and refuses the rest.\n\n**Configuration:** `BUZZ_HARAKAT_KIND_ALLOWLIST` — REQUIRED, with no default. The fence's whole point is that a community it was not configured for accepts nothing.\n\n### B5 — Guest Role is Read-Only\n\n`MemberRole::Guest` exists, is documented as read-only, and is dead weight: every write-path check is a `COUNT(*)` with no role filter, so a guest posts like a member.\n\nThis is what gives us observer seats.\n\n**Assertions:**\n- A Guest cannot post into the room\n- A Guest cannot react to a real message\n- A Guest can still read the room (read-only, not blocked)\n- A Member can post (negative control)\n\n---\n\n## Operations\n\n### Scripts\n\nAll operations are driven by `scripts/rooms-relay.sh`:\n\n```sh\nbash scripts/rooms-relay.sh build # build the image from our fork\nbash scripts/rooms-relay.sh test # the fork patches' unit tests\n\nbash scripts/rooms-relay.sh up 19 # provision + start company 19's relay\nbash scripts/rooms-relay.sh probe 19 # prove the patches against it\nbash scripts/rooms-relay.sh status # what is running\nbash scripts/rooms-relay.sh down 19 # stop it (add --volumes 19 to drop its data)\nbash scripts/rooms-relay.sh ports # print the port ledger\n```\n\n**Provisioning:** `up` generates `tenants/t<id>.env` on first run with fresh secrets and a fresh relay identity, validates it, and starts the stack. It refuses to start on a malformed owner pubkey — upstream only *warns* about one, which leaves a membership-enforcing relay with nobody able to administer it.\n\n### Validation\n\n- `scripts/rooms-substrate-check.sh` — asserts each isolation rule (27 mutations, all caught)\n- `relay-probe.py` — behavior probe for the three patches\n- `relay-load.py` — performance measurement\n\n---\n\n## Configuration Reference\n\n### Operator Variables (Shell)\n\nThese are read by `scripts/rooms-relay.sh` from your shell, not from any `.env`:\n\n| Variable | Default | What it decides |\n|----------|---------|-----------------|\n| `HARAKAT_ROOMS_NETWORK` | `harakat_rooms-net` | The relay-facing docker network. Only `server` and the tenant relays are on it. |\n| `HARAKAT_ROOMS_PORT_BASE` | `3900` | Bottom of the host-port range for tenant relays. A port is taken from what is actually free. |\n| `HARAKAT_ROOMS_PORT_CEILING` | `4999` | Top of that range. Exhausting it provisions nothing. |\n| `HARAKAT_ROOMS_LOG_MAX_SIZE` | `10m` | Per-container docker log bound. |\n| `HARAKAT_ROOMS_LOG_MAX_FILES` | `3` | Rotation depth. |\n| `HARAKAT_ROOMS_RELAY_LOG` | `buzz_relay=info,buzz_relay::handlers::event=warn` | The relay's own log filter. |\n\n**Note:** The two log variables are also interpolated by `compose.relay.yml`, so they appear in `.env.example` as well. The other four are the provisioner's alone.\n\n### Relay Environment Variables\n\n| Variable | Required | Purpose |\n|----------|----------|---------|\n| `BUZZ_BIND_ADDR` | No | `0.0.0.0:3000` |\n| `BUZZ_HEALTH_PORT` | No | `8080` |\n| `BUZZ_METRICS_PORT` | No | `9102` |\n| `DATABASE_URL` | Yes | `postgres://buzz:<password>@tN-db:5432/buzz` |\n| `REDIS_URL` | Yes | `redis://:<password>@tN-redis:6379` |\n| `RELAY_URL` | Yes | Tenant's community host (this IS the tenant key) |\n| `BUZZ_REQUIRE_AUTH_TOKEN` | No | `true` — requires NIP-98 on every REST call |\n| `BUZZ_S3_ENDPOINT` | No | `http://tN-minio:9000` |\n| `BUZZ_S3_ADDRESSING_STYLE` | No | `path` |\n| `BUZZ_S3_ACCESS_KEY` | Yes | MinIO access key |\n| `BUZZ_S3_SECRET_KEY` | Yes | MinIO secret key |\n| `BUZZ_S3_BUCKET` | No | `harakat-rooms-tN` |\n| `BUZZ_GIT_REPO_PATH` | No | `/data/git` |\n| `BUZZ_AUTO_MIGRATE` | No | `true` |\n| `BUZZ_HARAKAT_KIND_ALLOWLIST` | Yes | See `.env.example` |\n| `BUZZ_REQUIRE_RELAY_MEMBERSHIP` | No | `true` |\n| `BUZZ_RELAY_PRIVATE_KEY` | Yes | Relay's private key |\n| `RELAY_OWNER_PUBKEY` | Yes | Owner's xonly pubkey |\n| `RUST_LOG` | No | `buzz_relay=info,buzz_relay::handlers::event=warn` |\n\n### Tenant-Specific Environment\n\n| Variable | Required | Purpose |\n|----------|----------|---------|\n| `HARAKAT_TENANT_ID` | Yes | Tenant identifier (e.g., 19) |\n| `HARAKAT_RELAY_IMAGE` | No | `harakat/buzz-relay:local` |\n| `HARAKAT_ROOMS_PG_PASSWORD` | Yes | Postgres password |\n| `HARAKAT_ROOMS_REDIS_PASSWORD` | Yes | Redis password |\n| `HARAKAT_ROOMS_RELAY_URL` | Yes | Tenant's community host |\n| `HARAKAT_ROOMS_KIND_ALLOWLIST` | Yes | Kind allowlist |\n| `HARAKAT_ROOMS_RELAY_PRIVATE_KEY` | Yes | Relay's private key |\n| `HARAKAT_ROOMS_RELAY_OWNER_PUBKEY` | Yes | Owner's pubkey |\n| `HARAKAT_ROOMS_PORT` | Yes | Host port (allocated from range) |\n| `HARAKAT_ROOMS_RELAY_MEMORY` | No | `1g` |\n| `HARAKAT_ROOMS_DB_MEMORY` | No | `512m` |\n| `HARAKAT_ROOMS_REDIS_MEMORY` | No | `256m` |\n| `HARAKAT_ROOMS_MINIO_MEMORY` | No | `512m` |\n\n---\n\n## Security Model\n\n### What Isolation Actually Means\n\nPer company, everything is separate: the compose project, every container name, all four volumes, the private network, and the S3 bucket.\n\n**The shape that matters:**\n\n- The relay is the **only** service on the relay-facing network. The company's Postgres, Redis and MinIO are on the tenant network alone and publish **no host port at all** — \"tenant A reads tenant B's rows\" has no route to attempt, rather than a check to pass.\n- **That network is `harakat_rooms-net`, not the platform's own `harakat_harakat-net`** — see the section above, which is a correction.\n- Every hostname the relay dials is per-tenant (`tN-db`, `tN-redis`, `tN-minio`), so its configuration **cannot name** a platform service.\n- The relay's own port is published on `127.0.0.1` only. Public exposure is Nginx's job, on that company's rooms host.\n- Every secret is required, never defaulted. A default password on a per-company relay is a shared password on every company's relay.\n- Every container bounds its docker log. The json-file driver has no `max-size` by default, and one tenant filling the disk takes every other tenant with it.\n\n### What Is Still Reachable\n\n- The server's own API — which is authenticated, and which the relay never dials\n- Other tenants' relays — which refuse a caller holding no key on their roster (`rooms-relay.sh probe <a> --peer <b>` is the standing proof)\n\nNeither is a data store.\n\n---\n\n## Boundaries\n\n### Ephemeral Kinds\n\nPresence, typing (20000–29999) never reach the ingest scope map; they are a separate door in `handlers/event.rs`. They are neither registered nor fenced. They carry no durable content today.\n\n### Room Content Sealing\n\nPer-tenant KMS sealing of message bodies is fork patch B8, track T7.1.\n\n### Provisioning\n\nDeciding that tenant 19 needs a relay, creating its rooms, syncing membership — is the projector, track T2.2. This is the substrate it provisions onto.\n\n---\n\n## Testing\n\n### Unit Tests\n\n```sh\nbash scripts/rooms-relay.sh test\n```\n\nRuns the fork patches' Rust unit tests inside the builder stage, reusing the same compiled artifacts the shipped binary was produced from.\n\n### Behavior Probe\n\n```sh\npython3 deploy/rooms/relay-probe.py \\\n --base-url http://127.0.0.1:3919 \\\n --host 127.0.0.1:3919 \\\n --owner-sk <64-hex>\n```\n\nProves the three patches (B3, B5, B9) against a live relay.\n\n### Load Testing\n\n```sh\npython3 deploy/rooms/relay-load.py \\\n --base-url http://127.0.0.1:3901 \\\n --host 127.0.0.1:3901 \\\n --owner-sk <64-hex> \\\n --messages 2000 \\\n --media 20 \\\n --media-bytes 262144\n```\n\nMeasures throughput and latency under known conditions.\n\n### Substrate Check\n\n```sh\nbash scripts/rooms-substrate-check.sh --selftest\n```\n\nAsserts each isolation rule (27 mutations, all caught).\n\n---\n\n## Performance & Cost\n\n### Resource Usage\n\n| Component | Memory Limit |\n|-----------|--------------|\n| Relay | 1g |\n| Postgres | 512m |\n| Redis | 256m |\n| MinIO | 512m |\n\n### Log Bounds\n\n- `max-size: 10m` per container\n- `max-file: 3` rotation depth\n- Worst case: 30 MB per container, 150 MB per tenant\n\n### Throughput\n\nPure Python BIP-340 signing is about 63 ms per signature. At 10 worker processes, the load generator tops out near 80 events/second. The relay's own memory and CPU under a known event rate, and the bytes those events cost in Postgres and MinIO, are the trustworthy numbers.\n\n---\n\n## Contributing\n\n### Adding New Patches\n\nEvery line we add is a line a future rebase can conflict on. Document in `vendor/buzz/harakat/patches/`:\n- The upstream behavior\n- Why it is wrong for us\n- The exact seam\n- The rebase exposure\n- What it deliberately does not cover\n\n### Adding New Event Kinds\n\nRegister in `BUZZ_HARAKAT_KIND_ALLOWLIST` and update `relay-probe.py` to test the new kind.\n\n### Adding New Operators\n\nDocument in this README, not in `.env.example`. The variables the compose file beside it actually interpolates are the contract; a template entry nothing reads teaches the next operator to set it somewhere with no effect.\n\n---\n\n## Related Modules\n\n- `HarakatOffices_Server` — reads `ROOMS_RELAY_URL_TEMPLATE`, `SCHEDULE_RELAY_HEALTH`, `RELAY_HEALTH_ENABLED` from its `.env`\n- `vendor/buzz` — the upstream relay we fork\n- `scripts/rooms-relay.sh` — the provisioner\n- `scripts/rooms-substrate-check.sh` — the isolation checker","deploy-spire":"# deploy — spire\n\n\n\n# deploy/spire\n\nThis module implements the SPIRE-based identity infrastructure for the Harakat P1 stack. It manages the trust root, node attestation, and workload identity issuance for the `harakat-ai` and `harakat-server` containers.\n\nThe implementation consists of three primary components:\n1. **SPIRE Server**: The trust anchor and issuer of SVIDs/JWT-SVIDs.\n2. **SPIRE Agent**: The daemon running inside the infrastructure VM that attests workloads and serves the Workload API.\n3. **SVID Sidecar**: A custom container that fetches identity from the Agent and materializes files for application consumption.\n\n## Architecture\n\nThe identity flow follows a strict separation of concerns. The SPIRE Server signs identities. The SPIRE Agent validates workloads (via Docker labels) and issues identities. The SVID Sidecar acts as a bridge, fetching the identity and writing it to a shared volume in a format the application containers can read directly.\n\n```mermaid\nsequenceDiagram\n participant App as App Container<br/>(harakat-ai/server)\n participant Sidecar as SVID Sidecar\n participant Agent as SPIRE Agent\n participant Server as SPIRE Server\n\n App->>Sidecar: Mount Volume (Read-Only)\n Sidecar->>Agent: Fetch JWT (unix socket)\n Agent->>Server: Validate Workload (Docker Label)\n Server-->>Agent: Issue JWT-SVID + Bundle\n Agent-->>Sidecar: Return JSON Response\n Sidecar->>Sidecar: Validate & Write Files\n Sidecar->>App: Provide svid.jwt, bundle.jwks\n```\n\n## SPIRE Server Configuration\n\nLocated in `deploy/spire/server.conf`, the server acts as the root of trust for the `harakat.tech` trust domain.\n\n### Key Settings\n* **Trust Domain**: `harakat.tech`\n* **Data Store**: SQLite (`/opt/spire/data/server/datastore.sqlite3`).\n* **Key Management**: Disk-based (`/opt/spire/data/server/keys.json`).\n* **Node Attestor**: `join_token`. This is a single-use token mechanism. The server generates a token for the agent (`spiffe://harakat.tech/agent`) during bootstrap.\n* **TTLs**:\n * `ca_ttl`: 24h\n * `default_x509_svid_ttl`: 1h\n * `default_jwt_svid_ttl`: 5m\n\n### Bootstrap\nThe server requires an initial bootstrap step to generate the join token for the agent:\n```bash\ndocker exec harakat_spire_server \\\n /opt/spire/bin/spire-server token generate -spiffeID spiffe://harakat.tech/agent\n```\nThis token is passed to the agent as `SPIRE_JOIN_TOKEN`.\n\n## SPIRE Agent Configuration\n\nLocated in `deploy/spire/agent.conf`, the agent runs within the infrastructure VM and serves the Workload API.\n\n### Key Settings\n* **Server Address**: `spire-server:8081`\n* **Socket Path**: `/run/spire/agent/api.sock`\n* **Key Management**: Disk-based (`/opt/spire/data/agent`). This is critical for `restart: unless-stopped` policies. The agent renews its node SVID without needing a new join token on every restart.\n* **Workload Attestor**: `docker`. Matches containers by label selector (e.g., `docker:label:com.harakat.workload:harakat-ai`).\n* **Bootstrap**: `insecure_bootstrap = true`. Trusts the server's bundle on first contact. This is acceptable for the single-VM internal network but should be hardened with a pinned trust bundle if the agent is ever remote.\n\n## SVID Sidecar\n\nThe SVID Sidecar is a custom container defined in `deploy/spire/svid-sidecar/Dockerfile`. It is not a standard SPIRE component but a shim designed to match the application's existing identity loading logic.\n\n### Purpose\nThe applications (`HarakatOffices_Server` and `Harakat_AI`) were written to read identity from files (`svid.ts` and `spiffe_identity.py`). While `spiffe-helper` exists upstream, its output format (base64 map) does not match the JWKS format (`{\"keys\":[...]}`) required by these loaders. This sidecar fetches the identity and writes it in the exact format the apps expect.\n\n### Execution Flow\nThe sidecar runs as a long-lived process with the following logic:\n\n1. **Initialization**:\n * Reads required environment variables: `SPIFFE_WORKLOAD`, `SPIFFE_AUDIENCE`, `SPIFFE_TRUST_DOMAIN`.\n * Defaults: `SPIFFE_TRUST_DOMAIN=harakat.tech`, `SPIFFE_SOCKET_PATH=/run/spire/agent/api.sock`.\n * Constructs expected SPIFFE ID: `spiffe://${TRUST_DOMAIN}/workload/${WORKLOAD}`.\n\n2. **Fetch Loop**:\n * Calls `spire-agent api fetch jwt` via the unix socket.\n * **Validation**:\n * Checks that the response contains an SVID and a Bundle for the trust domain.\n * **Identity Assertion**: Verifies the returned `spiffe_id` matches `EXPECT_ID`. If mismatched, it refuses to write the file to prevent identity confusion.\n * **Bundle Validation**: Decodes the base64 bundle and ensures it parses as a valid JWKS with at least one key.\n * **Atomic Write**:\n * Writes `svid.jwt` and `bundle.jwks` to `$OUT_DIR` (default `/run/spiffe`).\n * Uses `mv -f` on a temp file to ensure atomicity.\n * Sets permissions to `0644`.\n * **Ready Flag**: Creates `$OUT_DIR/.ready` only after successful fetch.\n\n3. **Retry Logic**:\n * `fetch_with_retry` attempts up to `MAX_FAILURES` (default 12) times with `RETRY_SECONDS` (default 5) between attempts.\n * **Fail-Closed**: If `MAX_FAILURES` are exhausted, it removes `.ready` and exits non-zero. This triggers a container restart. It does *not* delete `svid.jwt` to avoid instant outage if the app holds a valid token.\n\n### Environment Variables\n| Variable | Required | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `SPIFFE_WORKLOAD` | Yes | - | `harakat-ai` or `harakat-server` |\n| `SPIFFE_AUDIENCE` | Yes | - | The peer this workload calls (e.g., `harakat-server`) |\n| `SPIFFE_TRUST_DOMAIN` | No | `harakat.tech` | Trust domain for the SPIFFE ID |\n| `SPIFFE_SOCKET_PATH` | No | `/run/spire/agent/api.sock` | Path to SPIRE Agent socket |\n| `OUT_DIR` | No | `/run/spiffe` | Output directory for identity files |\n| `REFRESH_SECONDS` | No | `120` | Interval between refresh attempts |\n| `RETRY_SECONDS` | No | `5` | Delay between fetch retries |\n| `MAX_FAILURES` | No | `12` | Max consecutive failures before exit |\n\n## Healthcheck\n\nLocated in `deploy/spire/svid-sidecar/svid-healthcheck.sh`, this script is used by Docker Compose to gate application startup.\n\n### Logic\n1. **File Existence**: Checks for `.ready`, `svid.jwt`, and `bundle.jwks`.\n2. **Bundle Validation**: Uses `jq` to ensure `bundle.jwks` contains a `keys` array with length > 0.\n3. **Expiry Check**:\n * Decodes the JWT payload from `svid.jwt`.\n * Extracts the `exp` claim.\n * Compares against current Unix timestamp.\n4. **Result**: Returns `0` (healthy) if all checks pass, `1` (unhealthy) otherwise.\n\n### Integration\nDocker Compose uses this healthcheck to ensure the sidecar is fully ready before starting the application:\n```yaml\ndepends_on:\n svid-sidecar:\n condition: service_healthy\n```\nThis prevents the application from booting into a state where `AGENT_CAPABILITY_ENFORCEMENT` is active but no trust bundle exists.\n\n## Application Integration\n\nThe sidecar writes to a shared volume mounted read-only into the application containers.\n\n* **HarakatOffices_Server**: Reads `svid.jwt` and `bundle.jwks` via `svid.ts::loadOwnSvid` and `loadTrustBundle`.\n* **Harakat_AI**: Reads `svid.jwt` and `bundle.jwks` via `spiffe_identity.py::load_svid` and `_load_bundle_set`.\n\nBoth applications perform their own expiry checks on the JWT payload. The sidecar ensures the files are valid and not expired at the time of write, but the application is responsible for refreshing or rejecting the token if it expires before the next sidecar refresh.\n\n## Security Considerations\n\n* **Fail-Closed**: The sidecar removes `.ready` on failure, forcing the application to restart or fail health checks. This prevents the app from running with a potentially stale or invalid identity.\n* **Atomic Writes**: Files are written to a temp file and moved. Readers never see a half-written SVID.\n* **Identity Scoping**: The sidecar is attested by the SPIRE Agent using the same Docker label as the application. It cannot obtain an SVID for a workload it is not registered for (`PermissionDenied: no identity issued`).\n* **Volume Isolation**: Each sidecar writes to its own volume. The `ai` container cannot read the `server`'s SVID and vice versa.\n* **TTLs**: Short-lived JWT-SVIDs (5m) limit the blast radius of a compromised token.","deploy":"# deploy\n\n\n\n# deploy\n\nThe `deploy` module orchestrates the infrastructure stack for the Harakat platform, managing secure identity, network routing, encryption, and model inference across production and local environments. It ensures that services are provisioned correctly, identities are verified, and data is encrypted at rest, while preventing configuration drift between development and production.\n\n## Architecture\n\nThe deployment stack follows a layered security and routing model:\n\n1. **Network Entry:** Traffic enters through `deploy/nginx`, which acts as the reverse proxy and load balancer for all public domains (`harakat.tech`, `app.harakat.tech`, `admin.harakat.tech`, `api.harakat.tech`).\n2. **Identity & Security:** Internal services (`harakat-ai`, `harakat-server`) authenticate via `deploy/spire` (SPIRE) for workload identity. Data at rest is encrypted using `deploy/openbao` (OpenBao), which manages the KMS and handles auto-unsealing for single-node deployments.\n3. **Inference & Logic:** `deploy/inference` provisions vLLM-based GPU workstations for multimodal model processing. `deploy/rooms` handles relay logic and cryptographic operations for data submission and loading.\n4. **Environment Safety:** `deploy/local` enforces a canonical local development profile using boot guards (e.g., `HARAKAT_LOCAL=1`) to prevent accidental production configuration drift.\n\n## Workflow\n\n```mermaid\ngraph TD\n Client[Client] -->|HTTPS| Nginx[deploy/nginx]\n Nginx -->|Proxy| Rooms[deploy/rooms]\n Nginx -->|Proxy| Inference[deploy/inference]\n Nginx -->|Proxy| Server[deploy/local]\n \n SPIRE[deploy/spire] -->|Identity| Inference\n SPIRE -->|Identity| Server\n \n OpenBao[deploy/openbao] -->|Encryption| Rooms\n OpenBao -->|Encryption| Inference\n \n LocalGuard[deploy/local] -->|Guard| Server\n```\n\n## Sub-modules\n\n* **[deploy/inference](deploy/inference.md)**: Manages vLLM-based inference services, GPU provisioning, and model configuration for document processing pipelines.\n* **[deploy/local](deploy/local.md)**: Enforces local development safety profiles and boot guards to prevent production configuration drift.\n* **[deploy/nginx](deploy/nginx.md)**: Configures reverse proxying, SSL termination, and load balancing for all public-facing domains.\n* **[deploy/openbao](deploy/openbao.md)**: Provides encrypt-at-rest (P1) capabilities via a single-node Key Management System with auto-unsealing.\n* **[deploy/spire](deploy/spire.md)**: Implements SPIRE-based identity infrastructure for workload attestation and SVID issuance.\n* **[deploy/rooms](deploy/rooms.md)**: Handles relay logic, cryptographic operations, and data loading for the application stack.","docs-architecture":"# docs — architecture\n\n\n\n# docs/architecture — Architectural Reference Module\n\n## Purpose\n\nThis module serves as the **single source of truth** for Harakat's architectural decisions, implementation specifications, and design patterns. It is not code itself, but the authoritative reference that guides all development work across the platform.\n\n**What this module is:**\n- Implementation specifications for architectural changes\n- Decision records with binding status\n- Performance profiling and optimization guidance\n- Integration plans for new capabilities\n\n**What this module is NOT:**\n- A sales document or vision statement\n- A critique or debate forum\n- A replacement for code comments\n\n## Document Inventory\n\n| File | Purpose | Status |\n|------|---------|--------|\n| `ARCHITECTURE_CHANGES.md` | Primary implementation spec for the per-assistant vessel + specialist + policy architecture | Phase A complete; Phase B/C in progress |\n| `caching_profile.md` | Performance profiling results and caching strategy recommendations | P0 complete; P2 items identified |\n| `decisions/2026-07-17-rescope-gap-m-entities-as-data.md` | Decision record: entities-as-data possible on Rust kernel | BINDING |\n| `decisions/2026-07-17-reversal-affine-verdict-odoo-20-years.md` | Decision record: Harakat exits Odoo for own Rust ERP kernel | BINDING |\n| `endgame_residuals_and_isolation_plan_2026-07-23.md` | Checkup verdicts and isolation plan for remaining technical debt | SIGNED |\n| `eri_bdh_integration_plan.md` | Integration plan for ERI (Evidence Retrieval & Intelligence) × BDH (Biologically-Directed Hebbian) mechanisms | Proposal |\n\n## Core Architectural Patterns\n\n### 1. Per-Assistant Vessel\n\nEach `UserAssistant` is a configured vessel with:\n- **Identity**: name, description, personality, access-key inputs\n- **Bindings**: Skills, MCPs, Procedures, LearnedCapabilities, Policies\n- **Per-user state**: Memory, Auto-load, ScenarioRuns, ScheduledTasks\n\n```typescript\n// Pattern: _resolve_assistant(assistant_id, claims) → BoundAssistantAgent\n// Replaces singleton AssistantAgent in assistant_workflow.py\n```\n\n### 2. Access Key as Derived Function\n\nThe Access Key is a pure function, never stored:\n\n```typescript\n// src/core/access-key/deriver.ts\nfunction deriveAccessKey(role: string, level: string, branchId: number, customOverrides: Json): AccessClaims {\n // Recomputed per request from server-canonical fields\n // Never stored as a blob\n}\n```\n\n**Default polarity**: `null = highest tier required (default-deny)`\n\n### 3. Policy as Constitutional Layer\n\nPolicy precedes all other constructs in the precedence chain:\n\n```\nPolicy (platform > tenant > department)\n > Department initialization skills\n > Tenant-admin skills\n > User-trained skills\n```\n\n### 4. Three Specialists for v1\n\n| Specialist | Owns retrieval over | Output |\n|------------|---------------------|--------|\n| Internal-state | Skills, Memory, Auto-load, ScenarioRuns, ScheduledTasks, PeerNotes, Policies | Context bundle |\n| Docs | PageIndex, Qdrant via DocumentIndex, ParsedDocument | Doc references + snippets |\n| Tools | MCPs (local + external), LearnedCapabilities | Callable tool catalog |\n\n### 5. Result Envelope Contract\n\nAll specialists emit results in a shared envelope format:\n\n```python\n{\n kind: \"context\" | \"action\" | \"procedure\" | \"doc_ref\" | \"capability_invocation\",\n data: any,\n source: { storeId: string, itemId: string },\n tier: string,\n confidence: number // 0..1\n}\n```\n\n## Cross-Cutting Invariants\n\nThese rules must be enforced in every module:\n\n### 3.1 `canSee(claims, item) → bool` is the single filter helper\n\nUsed in every store-read path (Postgres, Qdrant, PageIndex, KnowledgeNode, AssistantMemory, AutoloadEntry, ParsedDocument, AssistantPeerNote, LearnedCapability, ScenarioRun).\n\n```typescript\n// src/core/access-key/canSee.ts\nfunction canSee(claims: AccessClaims, item: TierableItem): boolean {\n const myLevel = tierLevel(claims.tier);\n const itemLevel = tierLevel(item.accessTier ?? HIGHEST_TIER);\n if (itemLevel > myLevel) return false;\n if (item.branchSuffix && claims.branchId !== item.branchId) return false;\n return true;\n}\n```\n\n### 3.2 Agent tool surface ≤ user UI surface\n\nEvery tool registered in the agent's tool catalog must be invocable by the user through the platform UI.\n\n### 3.3 Access key is derived per request, never stored\n\nThe Access Key is computed in middleware from JWT claims + DB on every request.\n\n### 3.4 Inter-assistant messages are content, not instructions\n\nWhen assistant B reads a note from A, B treats the note as **information** to reason over, never as **commands** to execute.\n\n### 3.5 Double-filter on cross-assistant note reads\n\nNotes from B → A pass through two filters:\n1. B's send filter (in B's send path)\n2. A's receive filter (in A's read path)\n\n## Data Models Overview\n\nAll models live in `HarakatOffices_Server/prisma/schema.prisma`:\n\n### Core Models\n\n| Model | Purpose | Key Fields |\n|-------|---------|------------|\n| `Policy` | Constitutional rules | `scope`, `authorRole`, `permittedCapabilityActions` |\n| `Skill` | Declarative thinking patterns | `origin`, `systemPromptFragment`, `precedence` |\n| `Procedure` | Imperative sequences | `steps`, `triggerTier`, `origin` |\n| `UserAssistant` | Per-assistant vessel | `userContextConfidence`, `personality`, `brandVoice` |\n| `AssistantMemory` | Per-user runtime state | `tierAtCapture`, `userId` |\n| `AssistantPeerNote` | Inter-assistant async comms | `intent`, `tierAtCapture`, `expiresAt` |\n| `LearnedCapability` | Trained predictors | `permittedActionTypes`, `status` |\n| `AuditEntry` | Unified audit log | `actorType`, `action`, `beforeStateHash`, `afterStateHash` |\n| `MutationProposal` | Approval queue | `subjectType`, `proposedChanges`, `status` |\n\n## Execution Flow\n\n```mermaid\ngraph TD\n A[User Request] --> B[Access Claims Middleware]\n B --> C[Per-Assistant Resolver]\n C --> D[Intent Classification]\n D --> E{Reflex Router}\n E -->|k=0| F[Reasoner Direct]\n E -->|k≥1| G[Specialist Fan-out]\n G --> H[Internal State Specialist]\n G --> I[Docs Specialist]\n G --> J[Tools Specialist]\n H --> K[Envelope Stitcher]\n I --> K\n J --> K\n F --> K\n K --> L[Tool Gateway]\n L --> M[Outcome Evaluator]\n M --> N[Learning Loop]\n N --> O[Audit Log]\n```\n\n## Build Order\n\nThe architecture is built in phases:\n\n### Phase A — Foundation (security and structure)\n1. Plaintext-secrets fix\n2. Access Key derivation + `canSee` helper\n3. Polarity flip on `knowledge.service.ts`\n4. `AuditEntry` model + writer\n5. `MutationProposal` model + approval queue API\n\n### Phase B — Vessel\n6. `UserAssistant` schema additions\n7. `Policy` model + CRUD + UI\n8. `Skill` model + collision detection\n9. `AutoloadEntry` model + approval queue\n10. `AssistantMemory` tier-at-capture migration\n\n### Phase C — Inter-assistant fabric\n11. `AssistantPeerNote` model + double-filter\n12. Sync inter-assistant calls\n13. Loop detection\n14. Per-assistant rate limits\n\n### Phase D — Specialists + composer\n15. Result envelope contract\n16. Internal-state specialist\n17. Docs specialist\n18. Tools specialist\n19. Composer node\n20. Per-assistant resolver\n\n## Integration Points\n\n### HarakatOffices_Server\n- `src/core/access-key/` — Access key derivation and filtering\n- `src/core/audit-log/` — Unified audit logging\n- `src/core/policy/` — Policy precedence and conflict detection\n- `src/modules/` — CRUD for all new models\n\n### Harakat_AI\n- `agents/specialists/` — Three specialist implementations\n- `agents/composer/` — Envelope stitching\n- `agents/supervisor/` — Loop detection\n- `capabilities/` — AutoML capability lifecycle\n\n### StartupsPlatform\n- `app/onboarding/` — 15-question wizard\n- `app/settings/` — Policy, skill, capability management\n- `app/dashboard/assistant/[id]/` — Extended chat surface\n\n## Performance Profile\n\nFrom `caching_profile.md`:\n\n| Layer | Status | Key Findings |\n|-------|--------|--------------|\n| AI agent path | Measured | p90 ≈ 135s, p99 ≈ 141s per turn; cost is input context size |\n| Frontend data layer | Measured | Zero caching library; 33/35 pages are `\"use client\"`; 29/35 fetch in `useEffect` |\n| Server/DB | Not measured | No general response/result cache; needs instrumentation |\n\n**Recommended fix order:**\n1. Instrument server first (request timing)\n2. Frontend caching (react-query or SWR)\n3. AI context reduction (P2)\n4. Server result cache (after #1 proves need)\n\n## Open Decisions\n\n| # | Decision | Recommended Default |\n|---|----------|---------------------|\n| 12.1 | Confidence-band thresholds | Pilot data calibration |\n| 12.2 | Loop soft/hard limits | Soft=4, Hard=8 |\n| 12.3 | Inter-assistant rate limit | 60 calls/hour/assistant |\n| 12.4 | Note TTL | 4 months default |\n| 12.5 | High-stakes threshold | $1,000 |\n\n## Verification & Acceptance Criteria\n\n### Access Key (Phase A)\n- Threat model walkthrough (5 scenarios)\n- Code coverage: `canSee` is the only path to filtered reads\n- Tool-surface check: every `ToolRegistration` has `equivalentUiPath` or `justification`\n\n### Vessel + Policy (Phase B)\n- Policy precedence test: tenant-scope policy wins over user-trained skill\n- Memory tier-demotion test: tier B memory filtered at tier D read\n- Auto-load approval queue test: agent proposals route to queue, not live store\n\n### Inter-assistant (Phase C)\n- Loop limit test: soft supervisor at depth N halts; hard ceiling at 2N\n- Content-not-instruction test: peer notes treated as content, not commands\n- Double-filter test: B's confidential note blocked by A's receive filter\n\n### Specialists + composer (Phase D)\n- Envelope contract test: all 3 specialists return matching schema\n- Filter coverage: each specialist's retrieval path goes through `canSee`\n\n## Contribution Guidelines\n\n### When to reference this module\n- Before implementing new features that affect architecture\n- When reviewing PRs that touch cross-cutting invariants\n- When designing new data models or execution flows\n\n### How to contribute\n1. **Read the relevant document** — `ARCHITECTURE_CHANGES.md` for implementation specs, decision records for binding decisions\n2. **Verify compliance** — Check against cross-cutting invariants (§3)\n3. **Update if needed** — If a decision needs revision, create a new decision record in `decisions/`\n4. **Test against acceptance criteria** — Verify your implementation passes the relevant tests\n\n### What NOT to do\n- Do not debate settled architecture decisions in PRs\n- Do not bypass `canSee` for any read path\n- Do not store access keys as blobs\n- Do not allow agent-proposed mutations without approval queue\n\n## Related Documentation\n\n- [CLAUDE.md](../../CLAUDE.md) — Current codebase guide\n- [FEATURE_UPDATE_AUTOML.md](../automl/FEATURE_UPDATE_AUTOML.md) — Self-building predictive capabilities\n- [docs/legacy/nexttodo_kernel.md](../../legacy/nexttodo_kernel.md) — Kernel program\n- [docs/architecture/decisions/](./decisions/) — All decision records\n\n## Maintenance\n\nThis module is maintained by the owner-user. Updates occur when:\n- New architectural decisions are made (new decision record)\n- Implementation phases complete (update status in `ARCHITECTURE_CHANGES.md`)\n- Performance profiles change (update `caching_profile.md`)\n\n**Last updated:** 2026-07-23 (endgame checkup)","docs-automl":"# docs — automl\n\n\n\n# docs/automl — AutoML Feature Design & Exploration\n\n## Overview\n\nThis module contains the design documentation and technology exploration for the **Self-Building Predictive Capabilities** feature. It defines how the assistant will identify learnable patterns in tenant data and offer to handle them automatically — without exposing ML terminology to users.\n\nThe module consists of two core documents:\n\n| File | Purpose |\n|------|---------|\n| `FEATURE_UPDATE_AUTOML.md` | End-to-end feature design, pipeline, security model, and build order |\n| `TIMESFM_EXPLORATION.md` | Technology evaluation for time-series forecasting capabilities |\n\n---\n\n## 1. Purpose & Scope\n\n### 1.1 What This Feature Does\n\nThe assistant continuously analyzes tenant data (documents, chats, connected systems) to identify recurring patterns it can act on. When a pattern is detected, the assistant offers to handle it automatically. Examples:\n\n- Flag invoices likely to be paid late\n- Score new leads by likelihood to convert\n- Surface contracts with unusual clauses\n\n**Critical constraint:** Users never see ML terminology. They see: *\"the assistant is learning\"*, *\"I noticed a pattern\"*, *\"I can now do X for you automatically\"*.\n\n### 1.2 What This Module Documents\n\nThis documentation module captures:\n\n1. **The complete pipeline** from pattern detection to live capability\n2. **Security architecture** for artifacts and approvals\n3. **Technology decisions** (classical estimators vs. foundation models)\n4. **Integration points** with existing codebase\n5. **Build order** for phased implementation\n\n---\n\n## 2. The Pipeline Architecture\n\n### 2.1 High-Level Flow\n\n```mermaid\nflowchart TD\n subgraph DataSources[Tenant Data Sources]\n PD[ParsedDocument]\n KN[KnowledgeNode]\n KE[KnowledgeEdge]\n CH[Chats]\n DS[Drive Sync]\n end\n \n subgraph Pipeline[8-Step Pipeline]\n PS[1. Pattern Scout] --> OP[2. Opportunity]\n OP --> SURF[3. Surfacing Layer]\n SURF --> DEF[4. Definition Chat]\n DEF --> SANDBOX[5. Ephemeral Sandbox]\n SANDBOX --> TRAIN[6. Train + Evaluate]\n TRAIN --> VALID[7. Human Validation]\n VALID --> LIVE[8. Live Operation]\n end\n \n subgraph Outputs[Outputs]\n DIGEST[Weekly Digest Email]\n BUBBLE[Contextual Suggestion]\n CHAT[Explicit Chat Request]\n end\n \n DataSources --> PS\n PS --> DIGEST\n PS --> BUBBLE\n PS --> CHAT\n LIVE --> MONITOR[Drift Monitoring]\n```\n\n### 2.2 Trigger Surfaces\n\nThree channels surface opportunities, chosen by confidence + value score:\n\n| Surface | When | Format |\n|---------|------|--------|\n| **Weekly digest email** | Once per week, up to 2 items | Email with [Yes/No/More] buttons |\n| **Contextual bubbles** | During chat or after file upload | In-app dismissable bubbles |\n| **Explicit chat request** | After users understand the pattern | Routes via `propose_predictive_capability` intent |\n\n**Build order:** 2.1 (digest) → 2.2 (bubbles) → 2.3 (explicit)\n\n---\n\n## 3. Key Components\n\n### 3.1 Pattern Scout (Step 1)\n\nRuns weekly and on ingest over tier-A nodes. Clusters by document type / entity / topic and scores each cluster on:\n\n- Signal strength\n- Recurrence\n- Business value\n\nOutputs `CapabilityOpportunity` records with `status = proposed`.\n\n**Integration point:** Walks `ParsedDocument`, `KnowledgeNode`, `KnowledgeEdge` from existing data sources.\n\n### 3.2 Definition Conversation (Step 3)\n\nLLM-led chat that resolves the prediction target in plain language:\n\n- *\"When you say 'risky invoice', do you mean late, unpaid, or…?\"*\n- *\"Should I look at amount, vendor, terms, or all three?\"*\n\nOutput: `CapabilitySpec` (target, signal columns, decision threshold, audit cadence).\n\n**Integration point:** Extends `Harakat_AI/workflows/assistant_workflow.py` with `propose_predictive_capability` intent.\n\n### 3.3 Ephemeral Training Sandbox (Steps 4-6)\n\n- Spins up isolated container with read-only data view\n- Pulls only columns named in `PredictionSpec`\n- Hashes manifest (record IDs + timestamps) → persisted\n- **Raw data NEVER leaves the sandbox**\n\n**Default estimators:** Logistic regression, gradient-boosted trees. Deep nets are off the menu for v1.\n\n### 3.4 Human-in-the-Loop Validation (Step 7)\n\nShows 10 high-scored and 10 low-scored items. User approves → capability goes live.\n\n**Security gate:** WebAuthn / passkey required for high-impact actions (see §5).\n\n### 3.5 Live Operation + Drift Monitoring (Step 8)\n\n- Scores new matching documents\n- Weekly accuracy check vs. recent ground truth\n- If accuracy drops > threshold → pause + notify user\n- User can disable capability at any time\n\n---\n\n## 4. Data Model\n\n### 4.1 Persistent Objects (Server)\n\nAll internal-only; not exposed to users.\n\n| Model | Purpose | Key Fields |\n|-------|---------|------------|\n| `LearnedCapability` | One row per accepted capability | `status`, `startupId`, `tier` |\n| `CapabilityOpportunity` | Pattern scout output | `score`, `dismissed`, `opportunity_id` |\n| `CapabilitySpec` | Structured `PredictionSpec` | JSON, tier-stamped |\n| `CapabilityArtifact` | Serialized trained object | **Encrypted at rest** |\n| `CapabilityRun` | Scoring/evaluation invocation | For audit and drift detection |\n| `CapabilityFeedback` | User labels | Per-user, tier-aware |\n| `CapabilityApproval` | WebAuthn-signed approval | Attestation + payload hash |\n\nAll carry `startupId` and respect the existing tier system from `src/modules/startup/assistant.service.ts`.\n\n### 4.2 Status Lifecycle\n\n```\nproposed → in_definition → training → validating → live → paused → retired\n```\n\nTransitions are gated by user approval and security checks.\n\n---\n\n## 5. Security Architecture\n\n### 5.1 Plaintext Secrets Fix (Already Shipped)\n\nBefore adding new sensitive surfaces, existing plaintext storage of `DriveIntegration.accessToken/refreshToken` and `Startup.zohoAccountPassword/customEmailRefreshToken/customEmailAccessToken` has been closed:\n\n- **`src/core/security/secret-cipher.ts`** — AES-256-GCM with versioned envelope (`enc:v1:` prefix)\n- **`src/core/config/prisma.ts`** — Prisma client extension that transparently encrypts on write\n- **`src/scripts/encrypt-existing-secrets.ts`** — One-shot migration script\n- **`env.ts`** — `SECRET_ENCRYPTION_KEY` is now required in production\n\n**Rollout steps:**\n1. Generate key: `node -e \"console.log(require('crypto').randomBytes(32).toString('base64'))\"`\n2. Set `SECRET_ENCRYPTION_KEY=…` in prod env\n3. Deploy code\n4. Run `npm run encrypt:secrets` once\n5. Back up `SECRET_ENCRYPTION_KEY` to separate secure store\n\n### 5.2 Encryption for Capability Surface\n\nLayered approach:\n\n1. **Envelope encryption for `CapabilityArtifact` blobs**\n - Each artifact gets its own data key (DEK)\n - DEK encrypted by tenant-scoped key (KEK)\n - KEK wrapped by `SECRET_ENCRYPTION_KEY` (or cloud KMS later)\n\n2. **Ephemeral training sandboxes**\n - Step 4–6 runs in isolated container\n - Raw training data **never** persisted outside it\n - Hashed manifest (record IDs + timestamps) persisted for reproducibility\n\n3. **Tier-gated artifacts**\n - `CapabilityArtifact` carries the tier of data it was trained on\n - Tools apply `tierLevel(nodeTier) <= myLevel` check (same as `src/modules/knowledge/knowledge.service.ts#L80`)\n - Finance-tier capability cannot be invoked from marketing-tier session\n\n4. **Differential privacy (later, opt-in)**\n - For high-sensitivity tenants\n - Training with DP noise prevents membership-inference attacks\n\n### 5.3 WebAuthn / Passkeys\n\n**Correction:** WebAuthn / FIDO2 is **authentication**, not encryption. It proves *who* is approving — it does not protect data at rest.\n\nApply surgically at high-impact gates:\n\n- **Go-live approval** (`CapabilityApproval`): user signs payload hash with passkey\n- **Capability deletion / re-training**: same passkey re-prompt\n- **Connecting new high-risk data source** (banking, payroll): passkey required\n\n**What we do NOT gate with passkey:**\n- Weekly digest email\n- In-chat suggestion bubbles\n- \"Would you flag this?\" labelling questions\n- Day-to-day chat\n\n**Implementation:** SimpleWebAuthn (server) + WebAuthn browser API. Passkey enrolment can reuse existing 2FA scaffolding on `Admin` (`QUICK_START_2FA.md`) — but also add to `StartupUser`, which currently has **no second factor at all**.\n\n### 5.4 What We Are Not Claiming\n\n- We are not claiming \"your data never leaves your store\" — it transits the sandbox. We *are* claiming it is not duplicated to a permanent store.\n- We are not claiming the artifact itself is non-sensitive. It is. It is encrypted, tier-gated, and access-logged.\n- WebAuthn does not encrypt anything. It only signs an approval.\n\n---\n\n## 6. Synthetic Augmentation\n\n### 6.1 The Correct Framing\n\nThe naive approach (\"here's a synthetic invoice I made — what do you think?\") fails because users nitpick the artifact instead of giving us the label we need.\n\n**Correct framing:**\n> *\"If this invoice arrived tomorrow, would you flag it as late-risk? **yes / no / not sure**\"*\n\nThis sidesteps the uncanny-valley problem and harvests the human's decision boundary.\n\n### 6.2 Operating Rules\n\n- Cluster real examples first; generate variants per cluster, never globally\n- Two questions per user per week, max. Route by tier (CFO sees finance variants; sales lead sees lead variants)\n- Show progress: *\"3 more validations and I can start flagging late-payers.\"*\n- Track inter-rater agreement when two users see the same item — disagreement is a useful signal\n- Be honest about N: a capability built on 40 real + 200 synthetic should ship with the message *\"I'm still learning — my early calls may need your eye.\"*\n\n---\n\n## 7. Technology Decisions\n\n### 7.1 Classical Estimators vs. Foundation Models\n\n**Design stance:** Default to small, interpretable estimators (logistic regression, gradient-boosted trees). Deep nets are off the menu for v1.\n\n**Rationale:**\n- Most planned capabilities are **classification / scoring** over documents and text\n- Deep nets are harder to interpret and debug\n- Small estimators work well with limited data\n\n### 7.2 TimesFM Exploration\n\nGoogle TimesFM is a **time-series forecasting** foundation model. It is an excellent fit for the **forecasting** slice (cash-flow, demand, stock-out timing) but **not** for the classification majority.\n\n**Fit by capability:**\n\n| Capability | Time-Series? | Best Engine |\n|------------|--------------|-------------|\n| Late-payment flagger | No | Small estimator |\n| Lead-conversion scorer | No | Small estimator |\n| Unusual-clause detection | No | LLM + small estimator |\n| Duplicate-supplier detection | No | LLM + similarity |\n| Cash-flow forecast | Yes | **TimesFM** |\n| Demand forecast | Yes | **TimesFM** |\n| Stock-out timing | Yes | **TimesFM** |\n\n**Recommendation:**\n1. Don't position TimesFM as \"the AutoML feature\" — it covers forecasting only\n2. Adopt it for forecasting capabilities via managed BigQuery/AlloyDB path\n3. Sequence it after Odoo integration (forecasting needs structured operational time series)\n4. Keep classical estimator path for classification capabilities\n\n**Architecture:**\n\n```mermaid\nflowchart TD\n subgraph Definition[Definition Chat]\n DEF[Resolve target] --> SPLIT{Forecasting<br/>or classification?}\n end\n \n SPLIT -->|classification / scoring| EST[Small estimator<br/>in ephemeral sandbox]\n SPLIT -->|time series| TFM[TimesFM via BigQuery / AlloyDB<br/>zero-shot, no training]\n \n EST --> VAL[Validation UI]\n TFM --> VAL\n VAL --> LIVE[Live capability]\n```\n\n---\n\n## 8. Integration Points\n\n### 8.1 Existing Codebase\n\n| Component | Integration | Notes |\n|-----------|-------------|-------|\n| `src/modules/startup/assistant.service.ts` | Tier system | All capabilities respect existing tier checks |\n| `src/modules/knowledge/knowledge.service.ts` | Tier gating | Use same `tierLevel(nodeTier) <= myLevel` check |\n| `Harakat_AI/workflows/assistant_workflow.py` | Intent routing | Add `propose_predictive_capability` intent |\n| `src/core/security/secret-cipher.ts` | Encryption | Reuse existing AES-256-GCM envelope |\n| `src/core/config/prisma.ts` | Prisma extension | Add capability models to encrypted fields |\n| `QUICK_START_2FA.md` | Passkey enrolment | Extend to `StartupUser` |\n| `PLATFORM_OVERVIEW.md` | Feature status | Update §8, §11 to reflect capability pipeline |\n| `odoo.md` | Data source | Structured data makes forecasting worthwhile |\n| `CLAUDE.md` | Tier handling | Fix inverted-polarity check before shipping |\n\n### 8.2 Data Sources\n\n- `ParsedDocument` — existing document parsing\n- `KnowledgeNode` / `KnowledgeEdge` — existing knowledge graph\n- `Chats` — existing chat history\n- `Drive sync` — existing file sync\n- **Future:** Odoo integration for structured operational data\n\n---\n\n## 9. Build Order\n\n1. **Plaintext-secrets fix** — done in this branch\n2. **Pattern scout v0.1** — runs weekly over `ParsedDocument`, emits `CapabilityOpportunity` rows. Internal-only, validate signal\n3. **Digest email** — top opportunity per tenant, with one-click feedback. Measure click-through and \"not interested\" rates\n4. **Definition chat + spec generation** — extend `assistant_workflow.py` with `propose_predictive_capability` intent + structured output\n5. **Ephemeral training sandbox** — one container per training run, scikit-learn baseline. Persist artifact (encrypted) + manifest hash + metrics\n6. **Validation UI** — the \"10 high, 10 low\" review screen. Approval click goes live (no WebAuthn yet — gate that on §8)\n7. **Suggestion bubbles** — once the digest loop is trusted\n8. **WebAuthn enrolment for `StartupUser`** — and gate go-live approvals on it\n9. **Synthetic augmentation loop** — only after evidence the bottleneck is data scarcity\n10. **Drift monitoring + auto-pause**\n\n---\n\n## 10. Open Questions\n\n| Question | Status | Notes |\n|----------|--------|-------|\n| Where does the sandbox run in single-VM topology? | Open | Likely separate container in compose stack with no network egress. Move to GCP Cloud Run / Vertex Custom Jobs when leaving bare VM |\n| KMS choice: envelope encryption vs. GCP KMS | Open | Envelope simpler, current state. KMS better blast-radius story, more operational overhead. Recommend KMS once >5 paying tenants |\n| Inverted-polarity tier check on `KnowledgeNode.accessTier = null` | Open | Fix before capability surface ships. Otherwise null-tier capability invocable by anyone |\n| Passkey enrolment UX for non-technical founders on mobile-only flows | Open | Needs design |\n\n---\n\n## 11. Glossary\n\n| Internal Term | User-Facing Language |\n|---------------|---------------------|\n| Predictive capability / model | \"Something the assistant can do for you\" |\n| Training | \"The assistant is learning\" |\n| Opportunity / proposal | \"I noticed something\" |\n| Spec / target / features | (never shown — resolved through plain conversation) |\n| Accuracy / metric | \"How often I'd be right on data that looks like yours\" |\n| Synthetic augmentation | \"Quick check — here's an example, what would you do?\" |\n| Drift detection | \"Something has changed — I want to recheck before I keep going\" |\n| Approval / passkey gate | \"Confirm with your security key to turn this on\" |\n\n---\n\n## 12. Related Documentation\n\n- `PLATFORM_OVERVIEW.md` §8, §11 — where capabilities sit and why they're not live\n- `odoo.md` — the structured-data integration that makes forecasting capabilities worthwhile\n- `QUICK_START_2FA.md` — existing 2FA scaffolding for passkey enrolment\n- `CLAUDE.md` — tier handling and inverted-polarity considerations\n- `src/core/security/secret-cipher.ts` — existing encryption implementation\n- `src/modules/startup/assistant.service.ts` — tier system implementation\n- `src/modules/knowledge/knowledge.service.ts` — tier gating implementation\n- `Harakat_AI/workflows/assistant_workflow.py` — intent routing implementation\n\n---\n\n## 13. Contributing\n\nWhen contributing to this module:\n\n1. **Maintain the user-facing language rule** — never expose ML jargon in user-facing text\n2. **Respect the tier system** — all capabilities must respect existing tier checks\n3. **Follow the security model** — artifacts are encrypted, approvals are signed, sandboxes are ephemeral\n4. **Track the build order** — don't implement step 9 before step 6 is validated\n5. **Document integration points** — clearly show how new code connects to existing systems\n\nFor questions about the feature design, refer to `FEATURE_UPDATE_AUTOML.md`. For technology decisions about forecasting, refer to `TIMESFM_EXPLORATION.md`.","docs-capacity":"# docs — capacity\n\n\n\n# docs/capacity — Token Economics & Hardware Capacity Analysis\n\n## Overview\n\nThe `docs/capacity` module provides capacity planning documentation for the Harakat Offices multi-agent workspace platform. It contains two core artifacts:\n\n1. **`estimate.md`** — The capacity estimation task specification and methodology\n2. **`harakat_capacity_comparison.md`** — The deliverable analysis comparing Apple Silicon hardware against candidate models\n\nThis module serves as the single source of truth for hardware procurement decisions, helping teams determine which machine configuration can support their target user count given the actual token economics of the live runtime.\n\n---\n\n## Purpose\n\nThe module answers three critical questions for infrastructure planning:\n\n| Question | Answer Source |\n|---|---|\n| How many concurrent users can each machine serve? | `harakat_capacity_comparison.md` §4 |\n| Which model should run on which hardware? | `harakat_capacity_comparison.md` §5 |\n| What are the real token costs per inference step? | `harakat_capacity_comparison.md` §2 |\n\nThe analysis is grounded in **measured** token counts from the actual codebase, not theoretical estimates. Every token number cites a specific file and line location.\n\n---\n\n## Architecture\n\n```mermaid\ngraph TD\n A[Harakat Runtime] -->|generates| B[Token Economics]\n B -->|feeds| C[Capacity Analysis]\n C -->|outputs| D[Hardware Recommendations]\n D -->|guides| E[Procurement Decisions]\n \n subgraph \"Codebase Sources\"\n A -->|prompts/schemas| F[docs/capacity/estimate.md]\n A -->|runtime behavior| G[docs/capacity/harakat_capacity_comparison.md]\n end\n \n subgraph \"Analysis Components\"\n F -->|Phase 1| H[Token Measurement]\n F -->|Phase 2| I[Hardware Facts]\n F -->|Phase 3| J[Comparison Matrix]\n end\n```\n\n---\n\n## Key Components\n\n### 1. Token Economics Measurement (`estimate.md` Phase 1)\n\nThe module measures real token usage from the codebase across these components:\n\n| Component | Measured Value | Source Location |\n|---|---|---|\n| System prompts | 1,022 tokens (PLANNER) + 470 tokens (Hermes base) | `system_prompt.py:7`, `run_agent.py:2582-2656` |\n| Tool schemas | 5,166–6,296 tokens | `core/tools.py:906` |\n| Retrieved units | 600–2,160 tokens per result | `core/tools.py:406-418`, `core/tools.py:479` |\n| Prefetch block | 1.3k–2.8k tokens | `harakat_memory_provider.py:255-302` |\n| Max output | 4,096 tokens | `aia_runner.py:215` |\n\n**Per-step working set** (the key metric):\n- **p50**: ~8.5k tokens\n- **p90**: ~14k tokens \n- **worst-case**: ~24–28k tokens\n\n### 2. Hardware Facts (`estimate.md` Phase 2)\n\nFour candidate machines with verified specifications:\n\n| Machine | Usable RAM | GPU Cores | Bandwidth |\n|---|---|---|---|\n| M4 Pro | ~38 GB | 20 | ~273 GB/s |\n| M4 Max | ~54 GB | 40 | ~410 GB/s |\n| M3 Ultra (60c) | ~84 GB | 60 | ~819 GB/s |\n| M3 Ultra (80c) | ~84 GB | 80 | ~819 GB/s |\n\n**Critical constraint**: MLX on Apple Silicon uses **pre-allocated KV per slot** (no paged attention). Memory must be sized for peak concurrent contexts, not average.\n\n### 3. Model Comparison (`harakat_capacity_comparison.md` §4)\n\nThe master comparison matrix shows max concurrent humans per machine/model pair:\n\n```\nQwen3.6-35B-A3B (hybrid) dominates due to KV efficiency:\n- M4 Max: 129 contexts / 64 humans (4k output)\n- M3 Ultra 80c: 269 contexts / 134 humans (4k output)\n\nFull-attention models are KV-bound:\n- Qwen3-14B: 25 contexts / 12 humans (M4 Max, 4k output)\n- ~6× fewer than hybrid despite smaller weights\n```\n\n### 4. Recommendations (`harakat_capacity_comparison.md` §7)\n\n| Decision | Recommendation |\n|---|---|\n| Pilot (≤5 users) | M4 Max 64 GB + Qwen3.6-35B-A3B @ Q4 |\n| Scale (tens of users) | M3 Ultra 96 GB 80-core + Qwen3.6-35B-A3B |\n| Tiered architecture | Router Qwen3-4B + Worker 35B-A3B |\n| Serving | MLX 4-bit (`unsloth/Qwen3.6-35B-A3B-UD-MLX-4bit`) |\n\n---\n\n## How It Works\n\n### Token Measurement Method\n\n1. **Tokenizer**: `tiktoken cl100k_base` proxy (Qwen tokenizer unavailable)\n2. **Heuristic**: ±10% margin for all token counts\n3. **Citations**: Every number links to specific file:line\n\n### KV Memory Calculation\n\n**Hybrid models** (Qwen 35B-A3B, 3.5-9B):\n```\nKV_bytes/token = 2 × n_layers_kv × n_kv_heads × head_dim × bytes_elem\nQwen 35B-A3B: 10 layers × 2 heads × 256 dim × 1 byte = 10 KB/token\n```\n\n**Full-attention models** (Qwen 14B, Mistral 7B):\n```\nQwen 14B: 40 layers × 8 heads × 128 dim × 1 byte = 80 KB/token\n```\n\n**KV budget formula**:\n```\nKV_budget = usable_RAM × 0.80 − weights_Q4\ncontexts = KV_budget ÷ (KV_per_token × working_set_size)\nhumans = (contexts − 1) ÷ 2\n```\n\n### Execution Flow\n\n```mermaid\nsequenceDiagram\n participant Dev as Developer\n participant Doc as Capacity Docs\n participant Code as Harakat Codebase\n participant Hardware as Hardware Selection\n \n Dev->>Doc: Needs capacity estimate\n Doc->>Code: Measure token costs\n Code-->>Doc: Returns file:line citations\n Doc->>Doc: Calculate KV per model\n Doc->>Hardware: Recommend machine/model\n Hardware->>Dev: Procurement decision\n```\n\n---\n\n## Integration Points\n\n### With Harakat Runtime\n\nThe capacity analysis directly references runtime behavior:\n\n| Runtime Component | Capacity Impact |\n|---|---|\n| `aia_runner.py` | Sets `max_tokens=4096`, iteration budgets |\n| `core/tools.py` | 38 tool definitions = 5.2–6.3k tokens per call |\n| `harakat_memory_provider.py` | Prefetch block assembly logic |\n| `native_config.py` | Compression triggers at 50% context window |\n\n### With Model Serving Stack\n\n| Stack Component | Constraint |\n|---|---|\n| MLX | Pre-allocates KV per slot, no overcommit |\n| llama.cpp | `--ctx-checkpoints 128` recommended for multi-session |\n| Ollama | No Qwen3.6 vision projector support |\n| vLLM | Not available on Metal |\n\n---\n\n## Contributing\n\n### Adding New Models\n\n1. Measure KV head config from model card\n2. Add to `harakat_capacity_comparison.md` §3 table\n3. Update master matrix in §4\n4. Document assumptions clearly\n\n### Updating Token Measurements\n\n1. Run `tiktoken` on actual code snippets\n2. Update `harakat_capacity_comparison.md` §2 tables\n3. Add file:line citations\n4. Flag any assumptions with `[ASSUMPTION]`\n\n### Hardware Changes\n\nIf new Apple Silicon hardware is released:\n1. Add to `estimate.md` Phase 2 table\n2. Recalculate KV budget with same formula\n3. Update master comparison matrix\n4. Note bandwidth vs core count tradeoffs\n\n---\n\n## Known Limitations\n\n| Limitation | Impact |\n|---|---|\n| Token counts use `cl100k_base` proxy | ±10% margin on all numbers |\n| Gemma-4 KV config assumed | Must verify before purchase |\n| Qwen3.5-9B KV heads assumed | 4 heads assumed; verify from model card |\n| No speculative decoding | Current stack doesn't support it |\n| Vision projector not in Ollama | Use llama.cpp/MLX-VLM for multimodal |\n\n---\n\n## Related Documentation\n\n| Document | Purpose |\n|---|---|\n| `docs/capacity/estimate.md` | Task specification and methodology |\n| `docs/capacity/harakat_capacity_comparison.md` | Analysis deliverable |\n| `docs/architecture/` | System architecture overview |\n| `docs/models/` | Model cards and specifications |\n\n---\n\n## Quick Reference\n\n| Metric | Value |\n|---|---|\n| Working set p90 | 14k tokens |\n| Tool schema bundle | 5.2–6.3k tokens |\n| Max output | 4,096 tokens |\n| Safety margin | 20% of usable RAM |\n| KV default | 8-bit |\n| Weights default | Q4 |\n| Recommended model | Qwen3.6-35B-A3B |\n| Recommended machine | M3 Ultra 96 GB 80-core |","docs-deployment":"# docs — deployment\n\n\n\n# Deployment Module\n\n## Overview\n\nThe `docs/deployment` module serves as the operational reference for deploying the `HarakatOffices_Server` container environment. It defines the procedures required to transition the production VM beyond a standard Docker rebuild.\n\nUnlike standard application modules, this module does not contain executable logic. It tracks **manual intervention points** and **schema evolution** that must occur on the deployed VM. While the Docker image handles code and schema updates automatically, specific data integrity checks and security migrations require human oversight.\n\n## Automated Deployment Pipeline\n\nA standard `docker-compose up --build server` triggers the following automated actions within the `HarakatOffices_Server` container:\n\n1. **Schema Migration**: `npx prisma migrate deploy` executes on container start.\n2. **Client Generation**: `prisma generate` regenerates the typed Prisma client.\n3. **Module Activation**: Backend modules for access-key, audit-log, mutation queue, policy CRUD, skill collision, auto-load, scenario-template, scheduled-task, peer notes, and sync-call loops become available.\n\n### Schema Migrations\n\nThe following migrations are applied automatically during the rebuild process:\n\n| Migration | Description |\n| :--- | :--- |\n| `20260517000000_add_audit_entry` | Creates `AuditEntry`, `AuditActor`, indexes, and foreign keys. |\n| `20260517010000_add_mutation_proposal` | Creates the proposal queue for agent-proposed durable mutations. |\n| `20260517020000_phase_b_vessel_foundation` | Adds `Policy`, `Skill`, `UserAssistantSkill`, `AutoloadEntry`, `ScenarioTemplate`, `AssistantScenarioRun`, `AssistantScheduledTask`, Phase-B `UserAssistant` fields, and `AssistantMemory.tierAtCapture`. |\n| `20260517030000_phase_c_inter_assistant_fabric` | Adds `AssistantPeerNote`, `AssistantSyncCall`, note intent/status enums, and indexes for async notes plus guarded sync calls. |\n\n**Verification**: After a rebuild, verify the generated Prisma client contains references for `AuditEntry`, `MutationProposal`, `Policy`, `Skill`, and `AutoloadEntry`.\n\n## Manual Intervention Points\n\nCertain operations cannot be automated due to data sensitivity or security requirements. These must be executed manually on the VM.\n\n### 1. KnowledgeNode Polarity Audit\n\nThe `knowledge.service.ts` polarity flip changes `accessTier = NULL` from effectively public to default-deny. Before shipping this change, a read-only audit must be performed on production data.\n\n**Procedure:**\n1. Execute the audit script against the running container.\n2. Review the output for total null-tier rows, breakdowns by `startupId` and `kind`, and 20 recent samples.\n3. Decide whether to backfill explicit tiers or accept the new invisibility.\n\n```bash\ndocker compose exec server npx tsx src/scripts/audit-null-tier-nodes.ts\ndocker compose exec server npx tsx src/scripts/audit-null-tier-nodes.ts --json\n```\n\n**Constraints:**\n- The script makes **no writes**.\n- Must be run before the new image goes live.\n\n### 2. Plaintext Secrets Migration\n\nLegacy plaintext secrets must be encrypted using a one-shot migration. This is idempotent but requires a secure `SECRET_ENCRYPTION_KEY`.\n\n**Procedure:**\n1. Generate a new key if not already set:\n ```bash\n node -e \"console.log(require('crypto').randomBytes(32).toString('base64'))\"\n ```\n2. Set `SECRET_ENCRYPTION_KEY` in the production environment variables.\n3. Run the encryption script:\n ```bash\n docker compose exec server npm run encrypt:secrets\n ```\n\n**Constraints:**\n- Legacy plaintext rows continue to work until this runs (via `decryptSecret` pass-through).\n- **Critical**: Back up `SECRET_ENCRYPTION_KEY` to a separate secure store. Losing this key results in permanent loss of all encrypted connected-account tokens.\n\n## Build-Time Constraints & Troubleshooting\n\n### Windows Development Environment\nLocal Windows development may encounter `EPERM` errors during `prisma generate` if another Node process holds `query_engine-windows.dll.node`.\n\n**Resolution:**\n- Stop `tsx watch` processes.\n- Allow Docker to regenerate the client in a clean container.\n\n### Migration Drift\nIf `prisma migrate deploy` reports drift on the VM:\n1. **Snapshot** production data immediately.\n2. Do **not** run `migrate resolve` blindly.\n3. Investigate the drift source before proceeding.\n\n## Rollback Strategy\n\nThe new migrations are additive, with the exception of `AssistantMemory.tierAtCapture`, which performs a non-null backfill of existing `accessTier` values.\n\n**Primary Rollback Scenario:** KnowledgeNode Polarity Flip\nIf the polarity flip causes runtime issues:\n1. Revert the polarity changes in `knowledge.service.ts`.\n2. Rebuild and redeploy the Docker image.\n3. Leave the additive migrations in place (do not revert schema migrations).\n\n## Deployment Lifecycle\n\nThe following sequence illustrates the interaction between automated container actions and manual deployment steps.\n\n```mermaid\nsequenceDiagram\n participant Dev as Developer\n participant Docker as Docker Compose\n participant DB as Prisma DB\n participant Script as Manual Scripts\n participant Env as Env Config\n\n Dev->>Docker: docker-compose up --build\n Docker->>DB: npx prisma migrate deploy\n Docker->>DB: prisma generate\n Docker-->>Dev: Container Ready\n Dev->>Script: Run audit-null-tier-nodes.ts\n Script-->>Dev: Report Null-Tier Rows\n Dev->>Env: Set SECRET_ENCRYPTION_KEY\n Dev->>Script: Run encrypt:secrets\n Script-->>DB: Encrypt Legacy Secrets\n Dev->>Dev: Verify Rollback Plan\n```\n\n## Key Components Reference\n\n| Component | Location | Purpose |\n| :--- | :--- | :--- |\n| `Dockerfile` | `HarakatOffices_Server/Dockerfile` | Defines container entrypoint and migration triggers. |\n| `audit-null-tier-nodes.ts` | `src/scripts/` | Read-only report for `KnowledgeNode` access tier data. |\n| `encrypt:secrets` | `package.json` | Script to migrate plaintext secrets to encrypted storage. |\n| `knowledge.service.ts` | `src/` | Service containing polarity flip logic for `KnowledgeNode`. |\n| `prisma` | `node_modules/` | ORM managing schema migrations and client generation. |","docs-docs":"# docs — docs\n\n\n\n# docs Module — Control Plane Documentation System\n\n## Overview\n\nThe `docs/` module is the **control plane documentation system** for the Harakat platform. It is not a code module in the traditional sense — it is the **knowledge base and operational control layer** that governs how work is tracked, logged, and handed off across sessions (human, AI, or automated).\n\nThis module contains:\n- **Control plane files** (`ai_inst.md`, `next_todo.md`, `logs/`) — the \"how to work\" and \"where are we\"\n- **Knowledge base** (`architecture/`, `product/`, `pipeline/`, etc.) — the \"what the system is\"\n- **Working logs** (`.sdd-work/`) — session artifacts and probes\n\nThe docs module is the **single source of truth** for session continuity. Every session must read it before starting, and every session must update it before ending.\n\n---\n\n## Architecture\n\n```mermaid\ngraph TD\n A[docs/ Module] --> B[Control Plane]\n A --> C[Knowledge Base]\n A --> D[Working Logs]\n \n B --> B1[ai_inst.md - How to Work]\n B --> B2[next_todo.md - Where We Are]\n B --> B3[logs/YYYY-MM-DD.md - Session History]\n \n C --> C1[architecture/ - Runtime & Integrations]\n C --> C2[product/ - Platform Overview]\n C --> C3[pipeline/ - Search & Document Processing]\n C --> C4[security/ - Audit & Compliance]\n \n D --> D1[.sdd-work/ - Session Artifacts]\n D --> D2[specs/ - Program Specifications]\n```\n\n---\n\n## Control Plane Files\n\n### `ai_inst.md` — AI Operating Instructions\n\n**Purpose:** The **first file every session reads**. It defines the control plane workflow, hard rules, and session lifecycle.\n\n**Key Sections:**\n\n| Section | Content |\n|---|---|\n| 0. Project in one line | Multi-tenant SaaS with Hermes-native AI assistants |\n| 1. Session start | Read order: `ai_inst.md` → `CLAUDE.md` → `next_todo.md` → `logs/` → `PLATFORM_OVERVIEW.md` |\n| 2. Session end | Append log entry, update `next_todo.md`, leave repo buildable |\n| 3. Hard rules | No stale docs, submodule commit order, reuse before writing, `PLANNER_ROUTING_RULE` preservation |\n| 4. Logging format | `docs/logs/<YYYY-MM-DD>.md` with timestamped entries |\n| 5. Todo format | `[ ]` / `[~]` / `[x]` status markers |\n| 6. Workspace map | Directory structure and submodule locations |\n\n**Hard Rules (Load-Bearing):**\n\n```markdown\n- No stale docs — ever. If a doc claims something the code no longer does, fix the doc in the same session.\n- Submodules are git submodules. To change app code: commit inside the submodule first, then bump the pointer in the meta-repo.\n- `PLANNER_ROUTING_RULE` is load-bearing and preserved verbatim. Never rewrite it during a refactor.\n- `startupId` is re-derived from the JWT in every controller — never trusted from the request body.\n- Tier letters are derived server-side (`deriveAccessTier`). Never trust `accessTier` from client.\n```\n\n**Session Start Order:**\n\n```\n1. Read ai_inst.md\n2. Read CLAUDE.md (codebase guide)\n3. Read next_todo.md (current state)\n4. Read docs/logs/<today>.md (previous session)\n5. Skim PLATFORM_OVERVIEW.md (product context)\n6. Start work\n```\n\n### `next_todo.md` — Current State Snapshot\n\n**Purpose:** The **single source of \"where are we\"**. Contains only work that has not been done.\n\n**Structure:**\n\n```markdown\n## Ground rules (non-negotiable)\n1. Never \"fix\" a pipeline problem by re-running a sync or backfill on startup 19\n2. No mocks. \"200 returned\" ≠ \"feature works\"\n3. Startup-19 is staging, not the product\n4. Live verification needs the stack lock\n5. Any ERP UI work loads the `erp-design-grammar` skill first\n\n## The pipeline items\n| # | Item | Status |\n|---|---|---|\n| 12 | 29 tree builds stuck in `building` | NOT STARTED |\n| 13 | 81 documents in `ocr_batch_pending` | NOT STARTED |\n| 14 | 2,867 folders without `PageIndexTree` | NOT STARTED |\n\n## Owed on the Admin platform\n| # | Item | Status |\n|---|---|---|\n| S1 | Activity surface + MCP operator view | SPECCED |\n| S2 | Pack console | SPECCED |\n| S3 | Suite recommendations | SPECCED |\n```\n\n**Status Markers:**\n- `[ ]` — Not started\n- `[~]` — In progress\n- `[x]` — Done\n\n**Program Files (Archived):**\n- `legacy/nexttodo_kernel.md` — Odoo exit\n- `legacy/nexttodo_b.md` — COMPANY blocker\n- `legacy/next_todo.md` — Governance refusal\n\n### `logs/` — Session History\n\n**Purpose:** Append-only archive of what changed each session.\n\n**File Naming:** `docs/logs/YYYY-MM-DD.md`\n\n**Entry Template:**\n\n```markdown\n## HH:mm — <short title>\n- **What:** <what you changed/did>\n- **Where:** <submodule + file paths>\n- **Why:** <reason / which next_todo thread>\n- **State:** <builds? tested? left mid-task? next immediate step>\n- **Notes:** <gotchas, decisions, things discovered> (optional)\n```\n\n**Rules:**\n- One file per day\n- Append, never overwrite\n- Timestamp to the minute (24h, local)\n- Note which submodule was changed\n\n---\n\n## Knowledge Base Structure\n\n### `architecture/` — Runtime & Integrations\n\n| File | Purpose |\n|---|---|\n| `hermesxharakat.md` | Plan to run every request through vendored Hermes `AIAgent` |\n| `ARCHITECTURE_CHANGES.md` | Constitutional model: Policies > Skills > Procedures, access keys, MCP fabric |\n| `odoo.md` | Plan to integrate Odoo's open-source apps |\n| `erp_integration.md` | Odoo CE/Enterprise gap, OCA add-ons, ERPNext, Isomorphic UI-mirror layer |\n| `endgame_residuals_and_isolation_plan_2026-07-23.md` | Security isolation plan |\n\n**Key Architectural Decisions:**\n\n```markdown\n- The vendored upstream Hermes `AIAgent` is the live path; legacy `agents/hermes_runtime/` + LangGraph trees are rollback-only dead code.\n- Harakat has a Shell coordinator path, but it is not yet the always-on tenant brain.\n- Harakat uses Hermes' agent loop while recreating the operating layer in Harakat.\n```\n\n### `product/` — Platform Overview\n\n| File | Purpose |\n|---|---|\n| `PLATFORM_OVERVIEW.md` | Plain-language tour of the live platform |\n| `startups-platform-agents.md` | Code-derived inventory of tenant app's agent/governance surfaces |\n| `design-refs/` | Design references from sessions |\n\n**Platform Components:**\n- Shell vs Assistant Hermes\n- Internal/external skills\n- Branches & tiers\n- The toolbox, vectorless retrieval, governance, meetings, apps, operations suite (ERP)\n\n### `pipeline/` — Search & Document Processing\n\n| File | Purpose |\n|---|---|\n| `GPU-SESSION-RUNBOOK.md` | Operating document for GPU processing lanes |\n| `mounts/onedrive.md` | Mount contract for OneDrive connector |\n| `search-v3/REGISTER.md` | 560 rows, three passes for search V3 |\n\n**GPU Lanes (Staged 2026-08-05):**\n\n| Lane | Work | GPU Cost | State |\n|---|---|---|---|\n| 3 | Problem file types (2,181 docs) | ≤109 min | Staged |\n| 2 | Bilingual Arabic cards (1,468) | ≤73 min | Staged |\n| 1 | Pageless cards (46) | ~2 min | Staged |\n| 4 | DWG resume (3,723) | ~4.7 h | Staged |\n\n### `security/` — Audit & Compliance\n\n| File | Purpose |\n|---|---|\n| `codex_security_audit.md` | Most recent security audit (2026-05-24) |\n| `AUDIT_REPORT.md` | Earlier sync/graph/cybersecurity audit (2026-04-25) |\n\n**Open Security Issues:**\n- Plaintext-secrets finding still open — Drive/custom-email tokens remain plaintext in `schema.prisma`\n- Tracked in `next_todo.md`\n\n### `retrieval/` — Search & RAG\n\n| File | Purpose |\n|---|---|\n| `EMBEDDINGS_QDRANT.md` | Exploration: Gemini Embedding 2 + Qdrant |\n| `HANDOVER_RETRIEVAL_V2.md` | Retrieval V2 design (FTS + PageIndex + agentic loop) |\n\n### `capacity/` — Token Economics & Hardware\n\n| File | Purpose |\n|---|---|\n| `estimate.md` | Research plan for measuring real token usage |\n| `harakat_capacity_comparison.md` | Capacity comparison matrix (machines × models → concurrent users) |\n\n---\n\n## Working Logs (`.sdd-work/`)\n\n**Purpose:** Session artifacts, probes, and temporary work. Never at repo roots.\n\n**Structure:**\n\n```\n.sdd-work/\n├── INDEX.md\n├── vendor-extraction/\n│ ├── gates.sh\n│ └── verify-pins.sh\n├── gpu-lane-prep/\n│ └── MANIFESTS.md\n├── surface-pass-2026-07-28/\n│ └── probe-readiness.ts\n├── rust-kernel/\n│ └── k3-reconcile/\n│ └── HARNESS-REPORT.md\n├── search-v3/\n│ └── REGISTER.md\n└── CONTINUE-*.md (session handovers)\n```\n\n**Rules:**\n- Every session artifact lives ONLY in `.sdd-work/<program>/`\n- Never a repo root, never a submodule root, never `docs/`\n- Keepers (reports, reusable probes) stay with an INDEX.md line\n- Transient probes, scratch logs, superseded outputs are deleted\n- Generated dirs (`.next/`, `dist/`, `__pycache__/`) are disposable\n\n---\n\n## Git Rules Integration\n\nThe docs module is tightly integrated with the Git workflow defined in `git-rules.md`.\n\n### Commit Rules\n\n```markdown\n- Commit at every green checkpoint: selftests pass, live verify done, a sub-slice lands.\n- Never end a session with uncommitted work — if interrupted mid-task, make a `wip:` commit.\n- Message format: `type(scope): summary` — types `feat|fix|chore|docs|test|wip`.\n- \"Gated\" work (default-OFF flags, unapplied SQL) is still committed work.\n```\n\n### Per-Merge Battery Receipt\n\n```bash\nbash .sdd-work/vendor-extraction/gates.sh server # writes a receipt\nDATABASE_URL=… bash .sdd-work/vendor-extraction/gates.sh db\nbash scripts/gate-receipt.sh list # what this machine has\nbash scripts/gate-receipt.sh selftest # 9/9, also in `gates.sh meta`\nHARAKAT_SKIP_GATE_RECEIPT=1 git push … # declared bypass, records WAIVED\n```\n\n**Required suites:**\n- `HarakatOffices_Server` → `server` **and `db`**\n- `StartupsPlatform` → `sp`\n- `Harakat_AI` → `ai`\n- `meta` → `meta`\n\n### Submodule Invariant\n\n```markdown\nA meta-repo commit may only pin submodule commits that already exist on the submodule's remote.\n\nPush order, always:\n1. commit in the submodule →\n2. push the submodule branch →\n3. commit the pointer bump in the meta-repo →\n4. push the meta-repo.\n```\n\n---\n\n## Connection to Codebase\n\n### Submodules\n\nThe docs module references and tracks these submodules:\n\n```\nHarakatOffices_Server — Tenant app (Express+Prisma)\nHarakat_AI — FastAPI Hermes service\nHarakat_Kernel — Rust kernel\nHarakat_Admin — Admin platform\nHarakat_Landing — Landing page\nOfficeProvider — Office integration\nStartupsPlatform — Tenant app (submodule)\n```\n\n**Cross-Repo:** The Integrated-ERP effort lives in a sibling repo at `C:\\Users\\LENOVO\\Desktop\\odoo` with its own control plane. When work touches that seam, update both repos' `next_todo` and cross-link.\n\n### Key Code References\n\n| Docs File | Code Reference |\n|---|---|\n| `ai_inst.md` | `agents/hermes_native/system_prompt.py:7` (PLANNER_ROUTING_RULE) |\n| `prodtodo_kernel.md` | `Harakat_Kernel/crates/kernel-storage/src/migrator.rs` (migrator steps) |\n| `search-command-and-erp-fix.md` | `erp-mcp-dispatch.ts` (normalizeErpArgs/coerceJsonish) |\n| `hermes_harakat_analysis.md` | `build_native_session()` → `build_shell_agent_context()` |\n\n### Environment Variables\n\n**Dev/Staging:**\n```bash\nKERNEL_ROUTED_APPS=...\nKERNEL_PINNED_ODOO_MODELS=...\nODOO_REMOVED=on\nDOC_CHILD_RETRIEVAL_ONLY=on\n```\n\n**Prod Activation:**\n```bash\nKERNEL_GATEWAY_SECRET=<prod-value>\nKERNEL_SYSTEM_SECRET=<prod-value>\nKERNEL_AUTHORITY=<prod-value>\n```\n\n---\n\n## Session Workflow\n\n### Start\n\n```\n1. Read docs/ai_inst.md\n2. Read docs/next_todo.md\n3. Read docs/logs/<today>.md\n4. Read docs/CLAUDE.md (architecture guide)\n5. Skim docs/product/PLATFORM_OVERVIEW.md\n6. Pick up from next_todo.md\n```\n\n### During\n\n```\n- Append to docs/logs/<today>.md\n- Update docs/next_todo.md as items complete\n- Keep repo buildable\n- Note submodule changes in log\n```\n\n### End\n\n```\n1. Append log entry to docs/logs/<today>.md\n2. Update docs/next_todo.md — tick off finished items\n3. Leave repo buildable, or mark in log + todo\n4. If submodule changed, note which one in log\n```\n\n---\n\n## Open Items (from `next_todo.md`)\n\n### Pipeline Items\n\n| # | Item | Status |\n|---|---|---|\n| 12 | 29 tree builds stuck in `building` | NOT STARTED |\n| 13 | 81 documents in `ocr_batch_pending` | NOT STARTED |\n| 14 | 2,867 folders without `PageIndexTree` | NOT STARTED |\n\n### Admin Platform\n\n| # | Item | Status |\n|---|---|---|\n| S1 | Activity surface + MCP operator view | SPECCED |\n| S2 | Pack console | SPECCED |\n| S3 | Suite recommendations | SPECCED |\n\n### Search V3\n\n- `onedrive::` connector + sync — OWED, not dropped\n- Per-assistant mailbox pipeline — IN FLIGHT\n\n### GPU Lanes (Staged 2026-08-05)\n\n| Lane | Work | State |\n|---|---|---|\n| 3 | Problem file types | Staged |\n| 2 | Bilingual Arabic cards | Staged |\n| 1 | Pageless cards | Staged |\n| 4 | DWG resume | Staged |\n\n---\n\n## Production Activation Debt (`prodtodo_kernel.md`)\n\n### Runtime\n\n- [ ] Kernel into the prod compose + nginx\n- [ ] Kernel Postgres = prod-grade\n- [ ] Prod OpenBao for the kernel\n- [ ] Windows→Linux binary\n- [ ] Backups + restore drill\n\n### Routing Promotion\n\n- [ ] Copy `KERNEL_ROUTED_APPS` / `KERNEL_PINNED_ODOO_MODELS` block from dev\n- [ ] `KERNEL_ROUTED_STARTUPS` = explicit prod batch list\n- [ ] Per newly-routed tenant: kernel entitlements → claims push → app enablement → backfill\n\n### Gates for Phase G (Odoo off)\n\n- [ ] Books-reconcile Odoo oracle leg\n- [ ] Owner browser walks (batched B/E)\n- [ ] `hr.version` → payroll K3\n- [ ] kernel→Odoo legacy-projection consumer\n- [ ] Odoo decommission runbook\n\n---\n\n## Summary\n\nThe `docs/` module is the **operational control plane** for the Harakat platform. It is not code that runs — it is code that **governs how work is done**. Every session must read it, update it, and leave it accurate.\n\n**Key Principles:**\n1. **Never stale** — if the code changed, the docs must change in the same session\n2. **Single source of truth** — `next_todo.md` is the only place for \"where are we\"\n3. **Append-only history** — `logs/` is the archive of what changed\n4. **Session continuity** — every session picks up from where the last left off\n\n**For Developers:**\n- Read `ai_inst.md` first, every session\n- Update `next_todo.md` before ending\n- Append to `logs/<today>.md` with timestamps\n- Keep the repo buildable\n- Note submodule changes\n\nThis module is the **control plane** — the rest of the codebase is the **product**.","docs-formal":"# docs — formal\n\n\n\n# Tenant-Resolution Formal Model\n\nThis module provides a bounded executable model for verifying tenant-resolution ordering in `HarakatOffices_Server`. It implements a state machine that enumerates all possible interleavings of tenant resolution steps against concurrent world events (switches, refreshes, concurrent requests) to assert seven security invariants.\n\nThe model is designed to satisfy Register **A8** requirements for \"bounded executable models + mutation testing\" without relying on TLA+ or external services. It consists of three core files: `model.py` (state machine), `exhaustive.py` (verification engine), and `mutation.py` (harness).\n\n## Architecture\n\nThe verification process is split into a state machine definition and an execution engine.\n\n```mermaid\nflowchart TD\n Main[main] --> Explore[explore]\n Explore --> Interleave[interleavings]\n Interleave --> RunS1[run_s1]\n Interleave --> RunS2[run_s2]\n RunS1 --> Apply[apply_step]\n RunS2 --> Apply\n Apply --> Model[model.py]\n Apply --> World[World State]\n World --> Judge[judge]\n Judge --> Invariants[check_invariants]\n Judge --> Mutants[mutation.py]\n Mutants --> Policy[Policy Mutants]\n```\n\n### Components\n\n* **`model.py`**: Defines the state machine. It contains the `World`, `Request`, `Policy`, and `Observation` dataclasses. It implements the logic for the four resolution steps (`claim`, `row`, `bind`, `read`) and world events (`switch`, `refresh`).\n* **`exhaustive.py`**: The verification engine. It uses `itertools.permutations` to enumerate orderings of steps and events, filters them by program order, and asserts invariants against the resulting `World` state.\n* **`mutation.py`**: The mutation harness. It modifies `Policy` clauses to break the model and requires specific invariants to fail, proving the clauses are guarded.\n\n## The State Machine\n\nThe model simulates the tenant resolution pipeline found in `src/middleware/startup-auth.ts` and `src/middleware/access-claims.ts`. Every tenant-scoped request executes four observable steps in program order:\n\n1. **`claim`**: Extracts `startupId` from the JWT (`req.startupId`).\n2. **`row`**: Reads the authoritative `StartupUser.startupId` from Postgres.\n3. **`bind`**: Compares `claim` vs `row`. Refuses if they disagree.\n4. **`read`**: Executes tenant-scoped reads (caches included).\n\n### Key Data Structures\n\n* **`Policy`**: A dataclass defining the clauses of the resolution rule.\n * `check_claim_against_row`: Enables the cross-check at `bind`.\n * `unchecked_shape`: Defines behavior if cross-check is disabled (`claim` vs `split`).\n * `tenant_in_cache_key`: Ensures tenant is part of cache keys.\n * `per_request_binding`: Ensures bound tenant is not process-global.\n * `bind_snapshot_is_stable`: Ensures `read` uses the `bind` snapshot.\n* **`World`**: The mutable shared state. Tracks `row` (authoritative tenant), `tokens` (session tokens), `cache`, and `observations`.\n* **`Request`**: Represents an in-flight request. Tracks `pinned_token` (whether the client re-reads the token) and step state (`refused`, `bound_row_side`).\n* **`Observation`**: Captures the result of a `read` step, including `read_row_side`, `read_claim_side`, and cache hit details.\n\n## Scenarios\n\nThe model enumerates two specific scenarios to cover isolation properties.\n\n### Scenario 1: Single Request vs Moving World\n* **Events**: 4 steps + 1 switch + 1 refresh.\n* **Logic**: One request (`R1`) runs against a world where the user's tenant changes (`switch`) and a new token is minted (`refresh`) concurrently.\n* **Permutations**: 30 valid orderings (filtered from 720).\n* **Cube**: 16 parameter combinations.\n* **Total Runs**: 480.\n\n### Scenario 2: Two Concurrent Requests + Switch\n* **Events**: 4+4 steps + 1 switch.\n* **Logic**: Two requests (`R1`, `R2`) for different tenants run concurrently, sharing one cache. A switch occurs for one of the users.\n* **Permutations**: 630 valid orderings (filtered from 362,880).\n* **Cube**: 64 parameter combinations.\n* **Total Runs**: 40,320.\n\n**Total Coverage**: 40,800 ordering × parameter combinations checked in ~0.5s.\n\n## Invariants\n\nThe model asserts seven invariants over the `Observation` data.\n\n| Invariant | Description |\n| :--- | :--- |\n| **I1** | **No wrong-tenant read**: The read executes under exactly the tenant `bind` authorized. |\n| **I2** | **No cross-tenant cache observation**: A cache hit only returns a value written for the same tenant. |\n| **I3** | **Bind refuses, never repairs**: `claim ≠ row` produces no read at all. |\n| **I4** | **Stability**: A switch landing strictly inside the `bind`→`read` window does not retarget the request. |\n| **I5** | **No process-global tenant**: Two concurrent requests for two tenants each read their own. |\n| **I6** | **Bounded staleness**: Once a switch is visible to a request's own `row` read, that request can never read the pre-switch tenant. |\n| **I7** | **No split-brain**: `req.startupId` consumers and `req.accessClaims` consumers inside ONE request resolve to the same tenant. |\n\n## Mutation Harness\n\n`mutation.py` ensures the invariants are meaningful by breaking the model clauses.\n\n### Strategy\nEach clause in `model.Policy` is turned off to create a mutant. The harness requires a **witness** (a specific counterexample) for each mutant. A mutant that remains green is a failure, indicating the clause is unguarded.\n\n### Mutants\n\n| Mutant | Clause Dropped | Required Witness |\n| :--- | :--- | :--- |\n| **M1** | `check_claim_against_row` (claim-only) | **I6** (4,082×) |\n| **M2** | `check_claim_against_row` (split) | **I7** (40,544×) |\n| **M3** | `tenant_in_cache_key` | **I2** (5,040×) |\n| **M4** | `per_request_binding` | **I5** (2,880×) |\n| **M5** | `bind_snapshot_is_stable` | **I4** (2,042×) |\n\n### Execution Flow\n1. **Control Run**: `explore(SPEC, collect=True)` must produce **zero** witnesses.\n2. **Mutant Run**: `explore(mutant_policy, collect=True)` must produce witnesses for the specific invariant guarding that clause.\n3. **Failure**: If a mutant produces no witnesses, or if the wrong invariant fires, the harness exits with code 1.\n\n## Execution\n\nRun the scripts from the module root:\n\n```bash\npython3 exhaustive.py # Verifies invariants (exits 0 on success)\npython3 mutation.py # Verifies invariants have teeth (exits 0 only if all caught)\nbash scripts/isolation-proof.sh # Runs both plus A/B lane gate\n```\n\n**Note**: `exhaustive.py` alone is not meaningful; a model with weak assertions is green for every implementation. `mutation.py` provides the verification that the assertions actually guard the code.\n\n## Integration with Codebase\n\nThe model abstracts specific implementation details but maps directly to shipped code:\n\n* **`claim`**: `src/middleware/startup-auth.ts:45,89`\n* **`row`**: `src/middleware/access-claims.ts:42-53`\n* **`bind`**: `src/middleware/access-claims.ts:63`\n* **`read`**: `retrieval-cache.ts:40`, `folder-cache.ts:46`\n* **Routes**: The model assumes a single resolution pipeline. The shipped server has two pipelines (one with cross-check, one without). The A/B isolation lane (`testpipe/isolation/`) reproduces the witness this model predicts for the route without the cross-check.\n\n## Honest Limits\n\nThis model enumerates bounded abstract transitions, not SQL schedules or full system behavior.\n\n* **SQL Isolation**: Postgres isolation levels, transaction boundaries, and connection-pool reuse are outside the model.\n* **Concurrency Scope**: Isolation is a pairwise property. Two tenants is the limit; three simultaneous requests or two switches are not enumerated.\n* **Cache Model**: The cache is modelled as one key with one entry. TTL expiry, LRU eviction, and `authorityVersion` are not modelled.\n* **JWT Verification**: The model assumes `jwt.verify` succeeds. Signature forgery and algorithm confusion are out of scope.\n* **Clock Skew**: Token expiry and clock skew are not modelled. The model proves the ordering property, not the duration.\n* **Middleware Mounting**: The model does not know which routes mount which middleware. It models one resolution pipeline.","docs-legacy":"# docs — legacy\n\n\n\n# docs/legacy — Retired Program Files Archive\n\n## Overview\n\nThe `docs/legacy` directory is an **archived documentation repository** containing retired program files from the Harakat development lifecycle. It was created on **2026-07-28** via `git mv` to preserve the full history of program files that were replaced by `docs/next_todo.md`.\n\n**Critical distinction:** This is a **documentation archive**, not a code module. It contains status-tracking documents that reference actual code repositories and serve as historical records of development programs.\n\n---\n\n## Purpose\n\nThis module serves three primary functions:\n\n1. **Historical Preservation** — Every file moved here retains its complete git history via `git mv`, ensuring audit trails remain intact.\n\n2. **Program State Tracking** — Documents track the status of major development initiatives (AI capabilities, Admin platform, ERP kernel, etc.) with explicit \"still live\" and \"archived\" markers.\n\n3. **Developer Context** — Provides the \"why\" behind architectural decisions, blockers, and open work items that developers need to understand when contributing to the active codebase.\n\n---\n\n## Directory Structure\n\n```\ndocs/legacy/\n├── README.md # Archive index and conventions\n├── next_todo.md # AI section program (bottom-up capability)\n├── nexttodo_admin.md # Admin platform program\n├── nexttodo_b.md # Types · primitives · packs program\n└── nexttodo_kernel.md # Rust ERP kernel program (Odoo exit)\n```\n\n---\n\n## What Was Archived (2026-07-28)\n\n| File | Program | State at Archive |\n|---|---|---|\n| `next_todo.md` | AI section | Phase 1 largely built; governance-refusal thread OPEN; Phases 4–5 never started |\n| `nexttodo_kernel.md` | Rust ERP kernel | Odoo retired on dev; Phase F prod-ops in full; Phase G residuals (delete dead Odoo code) |\n| `nexttodo_b.md` | Types · primitives · packs | P1 custom entities COMPLETE; P3 allocation/fund code-complete; record-ownership blocker |\n| `nexttodo_admin.md` | Admin platform | A1 typed provisioning shipped; A2/A3 blocked on wave-2 primitives |\n| `nexttodo_office.md` | Office platform | O2 vendors v1 live gate passed; O3/O4 blocked by design |\n\n---\n\n## What Was NOT Archived (Still Live)\n\nThese files remain in their original locations and are **not** part of this archive:\n\n| Path | Reason |\n|---|---|\n| `../prodtodo_kernel.md` | Operational punch list for kernel prod-ops (Phase F), not a program file |\n| `../logs/` | Append-only day history, untouched |\n| `../archive/next_todo-rounds2-4-closed-2026-07-10.md` | Earlier closed round, already archived under `docs/archive/` |\n\n---\n\n## Status Conventions\n\nWhen reading these documents, apply these conventions:\n\n| Term | Meaning |\n|---|---|\n| **gated** | Built and committed behind a default-OFF flag or unapplied SQL file; activation step, not unfinished work |\n| **merged** | Gate passed; code is on `main` |\n| **Cutover** | Routing a real tenant; always a separate, final phase |\n| **VERIFIED** | Traced in code on the documented date; not quality-proven |\n| **OPEN** | Work item still active; must be carried into new program files |\n\n---\n\n## Key Program Files\n\n### `next_todo.md` — AI Section Program\n\n**Scope:** Bottom-up capability, access, and quality program for the AI assistant.\n\n**Structure:**\n- **Phase 1** — The agent alone (ai→user)\n- **Phase 2** — Access & identity (binding agent to user)\n- **Phase 3** — Connected platform (user↔user, ai↔user, ai↔ai)\n- **Phase 4** — Use-case companies + owner report\n- **Phase 5** — DeepInfra model bench\n\n**Key Components Referenced:**\n- `Harakat_AI/core/tools.py` — Tool registry (57 tools)\n- `Harakat_AI/agents/hermes_native/aia_runner.py` — Agent runner configuration\n- `Harakat_AI/agents/hermes_native/honcho_user_memory.py` — User personalization\n- `HarakatOffices_Server/src/modules/policy/` — Policy enforcement\n- `testpipe/` — Testing harness (grading, suite, harness)\n\n**Current State (2026-07-28):**\n- ✅ Phase 1.0–1.7 largely built and live-proven\n- ⚠️ Governance-refusal thread OPEN (prompt hardening failed three times)\n- ⚠️ Phases 4–5 never started\n\n**Execution Flow:**\n```mermaid\ngraph TD\n A[Phase 1: Agent Alone] --> B[Phase 2: Access & Identity]\n B --> C[Phase 3: Connected Platform]\n C --> D[Phase 4: Use-Case Companies]\n D --> E[Phase 5: Model Bench]\n E --> F[Owner Report]\n \n subgraph \"Ground Rules\"\n G[No code rollback]\n H[Log every task]\n I[Build at most twice]\n J[No mocks]\n end\n \n G & H & I & J -.-> A\n```\n\n### `nexttodo_kernel.md` — Rust ERP Kernel Program\n\n**Scope:** Complete exit from Odoo to a native Rust ERP kernel.\n\n**Program Phases (A→G):**\n- **A** — Land wave-2 (merged main, nothing routed)\n- **B** — Prove CRM slice live\n- **C** — Stack app backends\n- **D/K3** — Finance ledger + posting\n- **E** — Make it live (routing flipped)\n- **F** — Prod-ops (checklist in `prodtodo_kernel.md`)\n- **G** — Cutover + Odoo off\n\n**Key Components Referenced:**\n- `AhmadYasin2/Harakat_Kernel` — Kernel repository\n- `kernel-storage/src/kernel.rs:781` — Record ownership seam\n- `apps/accounts/src/posting.rs` — Cross-app posting (RFC 007)\n- `apps/crm/src/lead.rs` — x2many write-validation\n\n**Current State (2026-07-28):**\n- ✅ All 32 app crates on main\n- ✅ Odoo retired on dev (2026-07-26)\n- ⚠️ Phase F prod-ops checklist in `prodtodo_kernel.md`\n- ⚠️ Phase G residuals (delete dead Odoo code, 36 pinned-model stubs)\n\n**Architecture:**\n```mermaid\ngraph LR\n subgraph \"Odoo (Fallback)\"\n O[Odoo :8069]\n end\n \n subgraph \"Rust Kernel (Primary)\"\n K[Kernel :8300]\n S[Server :5000]\n F[FE]\n end\n \n subgraph \"Tenant 19\"\n T[Tenant 19]\n end\n \n T -->|kernel = MAIN| K\n T -.->|fallback| O\n K --> S\n S --> F\n```\n\n### `nexttodo_b.md` — Types · Primitives · Packs\n\n**Scope:** Platform master plan for typed tenants and customization grammar.\n\n**Waves:**\n- **Wave 1** — Purge + Admin redo (typed provisioning A1)\n- **Wave 2** — Primitives (P1→P3→P2→P5→P4→P6)\n- **Wave 3** — NGO type + Awqaf pack\n- **Wave 4** — Vendors v1\n- **Wave 5** — Operator console\n- **Wave 6** — VENTURE posture (deferred)\n\n**Primitives:**\n| Primitive | Status |\n|---|---|\n| P1 Custom entities | ✅ COMPLETE + merged |\n| P3 Allocation/fund | ✅ CODE-COMPLETE + merged; live walk owed |\n| P2 Computed fields | Not started |\n| P5 Page documents | Built + gated |\n| P4 Guided flows | Built + gated |\n| P6 Report templates | Built + gated |\n\n**Key Components Referenced:**\n- `kernel-storage/src/kernel.rs:781` — Record ownership seam (COMPANY blocker)\n- `apps/accounts/src/` — Finance primitives\n- `modules/reports/report-template.ops.ts` — Template operations\n\n**Current State (2026-07-28):**\n- ✅ P1 exit gate passed\n- ✅ P3 code-complete + merged\n- ⚠️ Record-ownership default decided but not built (single blocker for all four COMPANY verticals)\n\n### `nexttodo_admin.md` — Admin Platform\n\n**Scope:** Internal operator layer, wave 1 of platform master plan.\n\n**Phases:**\n- **A1** — Typed provisioning (shipped + live-gated)\n- **A2** — Pack apply/upgrade/revert console (blocked on wave-2 primitives)\n- **A3** — Operator management (owner-fenced behind Office-v2)\n\n**Current State (2026-07-28):**\n- ✅ A1 shipped (typed provisioning is the ONLY birth path)\n- ✅ Office-tier purge executed + merged\n- ⚠️ A2/A3 blocked on wave-2 primitives\n\n---\n\n## Integration with Active Codebase\n\nThese documents **do not contain executable code**. They reference and track:\n\n1. **Actual repositories** — `Harakat_Kernel`, `HarakatOffices_Server`, `Harakat_AI`, `StartupsPlatform`\n2. **Live code paths** — Tool registries, policy modules, agent runners, testing harnesses\n3. **Database schemas** — Prisma migrations, SQL dumps, model definitions\n4. **Configuration flags** — `ODOO_REMOVED`, `ALLOCATION_ENGINE`, `REPORT_TEMPLATES`, etc.\n\n**For developers:** Use these documents to understand:\n- What work is still owed\n- What blockers exist\n- What the architectural intent is\n- Where to find the actual implementation\n\n---\n\n## Dead Links (Known Issues)\n\nFour links in this archive are dead and were already dead before the move:\n\n1. `superpowers/specs/…` — Two spec paths (directory never existed under `docs/`)\n2. `.sdd-work/bench-nemotron-super/REPORT.md` — Cleaned up artifact\n3. `.sdd-work/CONTINUE-kernel-finance-cluster-2026-07-21.md` — Cleaned up artifact\n\nThese were left as-is rather than silently repointed to preserve historical accuracy.\n\n---\n\n## Notes for Contributors\n\n- **Relative links were rewritten** for the extra directory level (`architecture/…` → `../architecture/…`)\n- **Links between the five files still work** — they moved together\n- **Status conventions hold** — \"gated\" means built behind a flag, \"merged\" means on `main`\n- **Open work must be carried forward** — Anything still owed has to be deliberately carried into new `docs/next_todo.md`, or it is lost\n\n---\n\n## Related Documentation\n\n| Document | Purpose |\n|---|---|\n| `docs/prodtodo_kernel.md` | Operational punch list for kernel prod-ops (Phase F) |\n| `docs/logs/` | Append-only day history |\n| `docs/archive/` | Earlier closed rounds |\n| `docs/architecture/` | Design decisions and blueprints |\n| `docs/specs/` | Technical specifications |","docs-personas":"# docs — personas\n\n\n\n# Personas Module\n\n## Overview\n\nThe **Personas Module** serves as the single source of truth for all test tenants and user accounts within the Harakat platform. It replaced legacy registries (`journeys/fixtures/personas.ts`, `tenants.json`) to ensure consistency across pipelines. Every test persona is defined in a dedicated `persona.md` file within `docs/personas/`.\n\nDevelopers must load personas from these files. Do not re-declare user data next to pipeline scripts.\n\n## File Structure & Schema\n\nEach persona resides in a dedicated folder under `docs/personas/`. The structure is:\n\n```text\ndocs/personas/\n├── README.md # Module overview, global rules, password trap\n├── aqarat/\n│ └── persona.md # Tenant 99\n├── dar/\n│ └── persona.md # Tenant 96\n├── ibtisama/\n│ └── persona.md # Tenant 97\n├── lifespace/\n│ └── persona.md # Tenant 19 (Staging)\n├── wahaj/\n│ └── persona.md # Tenant 98\n└── waqf/\n └── persona.md # Tenant 95\n```\n\n### The 8-Section Standard\n\nEvery `persona.md` file must contain exactly eight sections to ensure pipelines are writable from the file alone:\n\n1. **Header Block:** Tenant ID, org type, backend status, re-seeding commands.\n2. **Business Context:** A sentence describing the business and its specific data problem.\n3. **The Cast:** Name, login, level, reporting line, and daily responsibilities.\n4. **Enabled Apps:** List of installed applications.\n5. **Identity Flip:** Custom field requirements and terminology changes (e.g., `Project` → `Patient`).\n6. **Journeys:** Specific flows (J1, J2...) to drive during testing.\n7. **Assistant Queries:** Behavioral asks to assert on records/tool calls.\n8. **Known Baseline:** Regressions or specific traps to measure against.\n\n## Tenant Lifecycle\n\n### Seeding and Resetting\n\nFor **Company Tenants (96–99)**, the system is designed to be re-converged idempotently.\n\n1. **Seed Company Personas:**\n ```sh\n pnpm dotenv -e .env.local -e .env -- tsx src/scripts/seed-company-personas.ts --apply\n ```\n2. **Push Kernel Claims:**\n Every write made by these users is a kernel write. The kernel denies unknown user IDs. You must push claims for each tenant ID sequentially:\n ```sh\n pnpm kernel:claims-push 96\n pnpm kernel:claims-push 97\n pnpm kernel:claims-push 98\n pnpm kernel:claims-push 99\n ```\n *Note: Do not use `--` before the ID.*\n\n### Staging Tenant (19)\n\n**Tenant 19 (LifeSpace)** is a long-lived staging environment with real synced data. It is **not** a vertical persona.\n\n* **Hard Rule:** Never re-seed or wipe Tenant 19.\n* **Hard Rule:** Never re-run the Drive sync or document backfill.\n* **Usage:** Use only for data-safety guardrails, deep org tree testing, and longitudinal regressions.\n* **Maintenance:** Additive only.\n\n## Authentication & Security\n\n### The Password Trap\n\nTest accounts do **not** share a single password. Using a shared constant will result in 401 errors that look like product bugs.\n\n| Password | Applies To |\n| :--- | :--- |\n| `HarakatTest!2607` | `coordiantor@lifespace.ae` (Tenant 19) and `e5admin@harakat.local` (Tenant 95) |\n| `Harakat#Test2026` | All other accounts (Tenants 96–99, and Tenant 19 non-admins) |\n\n### Access Control\n\nAccess rules are tested against the **org tree**, not just rank.\n* **Reporting Lines:** A user's visibility is determined by who they report to.\n* **Branches:** Users in different branches (e.g., Marketing vs. Procurement) must not see each other's scoped material.\n* **Levels:** Level 0 (Owner) sees everything; Level 2+ must be restricted from admin surfaces.\n\n## Tenant Catalog\n\n| Tenant ID | Name | Type | Backend | Password | Key Characteristic |\n| :--- | :--- | :--- | :--- | :--- | :--- |\n| **95** | Al Khair Waqf | NGO | Kernel + Awqaf | `HarakatTest!2607` | Arabic-first, Fund Accounting, Awqaf Pack |\n| **19** | LifeSpace | Staging | Mixed | `HarakatTest!2607` | Deep org tree, real corpus, never re-seed |\n| **96** | Dar Interiors | Company | Kernel | `Harakat#Test2026` | Fan-out/join stress (Design → Build) |\n| **97** | Ibtisama Dental | Company | Kernel | `Harakat#Test2026` | Identity flip (Project → Patient) |\n| **98** | Wahaj Marketing | Company | Kernel | `Harakat#Test2026` | Retainer/Campaign connectivity |\n| **99** | Aqarat Realty | Company | Kernel | `Harakat#Test2026` | Brokerage (No Property Model) |\n\n## Pipeline Integration\n\n### Loading Personas\n\nPipelines must load persona data from `docs/personas/` rather than hardcoding credentials. The `persona.md` files act as the configuration contract.\n\n### Execution Flow\n\n```mermaid\nflowchart TD\n A[Pipeline Start] --> B{Load Persona}\n B -->|Tenant 96-99| C[Seed Company Script]\n B -->|Tenant 19| D[Skip Seed / Read State]\n C --> E[Push Kernel Claims]\n D --> E\n E --> F[Run Test Walks]\n F --> G{Assertion Check}\n G -->|Pass| H[Report Success]\n G -->|Fail| I[Check Known Baseline]\n I -->|Known Bug| H\n I -->|New Bug| J[Update persona.md]\n```\n\n### Integration Points\n\n* **Seed Script:** `src/scripts/seed-company-personas.ts`\n* **Kernel Command:** `pnpm kernel:claims-push <ID>`\n* **Test Harness:** `journeys/` (uses loaded persona data)\n* **Assistant Harness:** `testpipe/` (drives assistant across retrieval/email)\n\n## Maintenance Rules\n\n1. **Facts are Verified:** Every level, role, and user ID in these files must come from a real login. If the server disagrees, fix the file before trusting results.\n2. **Move Capabilities, Don't Delete:** If a walk proves a user cannot do something, move it to the `cannot` section with the reason. Do not silently drop the line.\n3. **Tenant 19 Safety:** Never re-seed Tenant 19. Losing the startup-19 corpus is not recoverable.\n4. **Known Baselines:** Update the `Known baseline` section when a regression is fixed or a new trap is discovered. This prevents rediscovering known issues.\n\n## Known Technical Constraints\n\n* **Tenant 19 Backend:** Runs on its own ERP container (`harakat-erp-t19-g2`). Cloned from shared DB, carries residue.\n* **Passkeys:** Origin-bound to `http://localhost:3000`. Credentials registered elsewhere are rejected.\n* **Odoo Dependency:** Tenant 95 (Waqf) has a remaining dependency on `/erp/session` returning 500 for `res.company`.\n* **Language:** Tenant 95 requires `dir=\"rtl\" lang=\"ar\"` to survive reload.","docs-pipeline":"# docs — pipeline\n\n\n\n# docs/pipeline — Document Processing Pipeline Documentation\n\n## Overview\n\nThe `docs/pipeline` module contains the durable record of the document processing pipeline development program (2026-07-29 → 2026-08-05). This documentation captures the design decisions, benchmark results, and operational procedures for a multi-stage document ingestion and retrieval system that processes 14,739+ documents across 152 projects.\n\nThe pipeline transforms raw documents into searchable records through:\n1. **Multi-format parsing** (PDF, CAD/DWG, spreadsheets, office docs)\n2. **Vision-based text extraction** (Qwen-vision replacing OCR models)\n3. **Search-optimized description generation** (entity-dense search cards)\n4. **Semantic embedding** (F2LLM-v2-14B for meaning-based retrieval)\n5. **Fused retrieval** (keyword + semantic + name matching)\n\n---\n\n## Architecture\n\n```mermaid\ngraph TD\n A[Raw Documents] --> B[Parse Stage]\n B --> C{Document Type}\n C -->|PDF/Scanned| D[Qwen-Vision OCR]\n C -->|CAD/DWG| E[LibreDWG Harvest]\n C -->|Office| F[Native Parser]\n D --> G[Content + Search Card]\n E --> G\n F --> G\n G --> H[Embedding]\n H --> I[F2LLM-v2-14B]\n I --> J[Qdrant Vectors]\n G --> K[Meili FTS]\n K --> L[Search Fusion]\n J --> L\n L --> M[LLM Rerank]\n M --> N[Final Results]\n```\n\n---\n\n## Core Components\n\n### 1. Document Parsing Layer\n\nThe parsing layer handles multiple document formats through specialized workers:\n\n| Format | Handler | Notes |\n|--------|---------|-------|\n| PDF (born-digital) | `pymupdf` | Extracts text layer directly |\n| PDF (scanned) | `Qwen-vision` | Vision-based OCR, no OCR models |\n| CAD/DWG | `dwg_harvest` | LibreDWG → DXF → ezdxf → text |\n| Spreadsheets | `type_vision.py` | LibreOffice conversion + vision |\n| Office docs | `type_prep.py` | Native format parsing |\n\n**Key Finding:** OCR models (DeepSeek, Gemini) were removed entirely in v2. Qwen-vision reads page images directly, producing verbatim transcription with ~3× slower latency than DeepSeek but significantly better fidelity.\n\n### 2. Record Structure\n\nEach document produces a structured record:\n\n| Field | Source | Purpose |\n|-------|--------|---------|\n| `content` | Qwen-vision page reads | Verbatim transcription |\n| `description` | Search card (variant-D) | Retrieval-optimized text |\n| `context` | Proposal-gated events | Growing vocabulary/usage |\n| `path` | Source scheme | `drive:/`, `workspace:/`, future `erp::` |\n| `facts` | Title-block KV | Parties, amounts, dates |\n\n**Search Card Format:** Entity-dense, includes tenant context for role labels (us/client/supplier), generated from text + images.\n\n### 3. Embedding Stack\n\n| Component | Model | Dimensions | Location |\n|-----------|-------|------------|----------|\n| Query Embedder | F2LLM-v2-14B | 2560 (Q8_0) | CPU (i5-13500) |\n| Document Embedder | F2LLM-v2-14B | 2560 | GPU (RTX 6000 WS) |\n| Corpus Vectors | F2LLM-v2-14B | 2560 | Qdrant |\n\n**Quantization:** Q8_0 and Q4_K_M both retrieve identically to fp16. Q8_0 recommended for 14 GB footprint vs 28 GB fp16, with 0.97s query latency vs 8.26s fp16.\n\n**Critical Finding:** The instruct prefix (`\"Instruct: …\\nQuery: \"`) costs ~0.030 Hit@1. Removed for production.\n\n### 4. Retrieval Fusion\n\nThe search pipeline uses a three-leg fusion:\n\n1. **Name leg** (Meili FTS on description) — strong keyword matching\n2. **Meaning leg** (Qdrant semantic search) — F2LLM embeddings\n3. **Keyword boosters** — Meili doc rows as keyword signals\n\n**Performance:** v2 stack achieves 2.5× more top-3 hits on English queries (90/200 vs 36/200) with 4× faster latency (78ms vs 300ms).\n\n---\n\n## Benchmark Results\n\n### Text Extraction Quality\n\n| Metric | OLD | V2 |\n|--------|-----|-----|\n| Exact identifiers (1k) | 722 | 828 |\n| Fabrications (1k) | 634 | 284 |\n| Missing (1k) | 235 | 132 |\n| Verbatim rate | 89.1% | 89.1% |\n\n**Key Finding:** V2 fabricates 2.2× less than OLD. OLD's signature failure is silent fabrication (invented pipe specs, wrong drawing numbers). V2's signature failure is repetition loops (e.g., `CH-02` ×2436 lines), which are self-evidently broken.\n\n### Embedding Performance\n\n| Model | Hit@1 | Hit@3 | MRR@10 | Latency (p50) |\n|-------|-------|-------|--------|---------------|\n| F2LLM-v2-14B (Q8_0) | 0.475 | 0.670 | 0.571 | 0.97s |\n| Qwen3-Embedding-8B | 0.250 | 0.455 | 0.367 | ~2s |\n| Gemini (incumbent) | 0.160 | 0.325 | 0.269 | ~0.3s |\n\nF2LLM-v2-14B achieves 3× the incumbent's Hit@1 and is the only model strong on identifier-style queries (0.40 vs 0.11).\n\n### Full Pipeline\n\n| Metric | Deployed | V2 Stack |\n|--------|----------|----------|\n| Hit@1 (EN) | 0.095 | 0.245 |\n| Hit@3 (EN) | 0.180 | 0.450 |\n| Latency | 299ms | 78ms |\n| Hit@1 (AR) | 0.098 | 0.122 |\n\n**Arabic Caveat:** v2 cards are English-only for Arabic-content docs. Bilingual cards required before cutover.\n\n---\n\n## Operational Procedures\n\n### GPU Session Runbook\n\nThe GPU box (Vast instance, RTX 6000 WS) is used for vision-based document reading. Key procedures:\n\n```bash\n# Box provisioning\nbash deploy/inference/provision.sh <instance_id>\n\n# Tunnel setup (port 18010-18013)\nbash scripts/inference-tunnel.sh up <instance_id>\n\n# Lane execution order\npython3 scripts/pipeline-v2/type_vision.py # Lane 3: problem types\npython3 scripts/pipeline-v2/build_manifests.py # Lane 2: Arabic cards\npython3 scripts/pipeline-v2/dwg_vision_worker.py # Lane 4: CAD\n```\n\n**Lane Order Rationale:**\n1. Lane 3 first (overwrites 411 cards from Lane 1)\n2. Lane 2 second (requires rebuild after Lane 3)\n3. Lane 1 residual (46 docs)\n4. Lane 4 last (longest, ~18.6h on 1 GPU)\n\n**Throughput:** ~1,200 docs/hour on 4 GPUs, ~200 DWG docs/hour. Full corpus (~14.7k docs) costs ~$60-70.\n\n### Safety Procedures\n\n**Critical Rules:**\n1. Never `pkill -f` by pattern (matches invoking shell)\n2. ETA by PAGES, not documents (document mix varies 40×)\n3. 64 concurrent sequences = decode saturation\n4. Four independent vLLM instances beat tensor parallel (3.9× vs ~2.5×)\n5. Supervise tunnel AND shards (tunnel death costs 20 minutes)\n\n**Stop Safely:**\n```bash\nssh <box> bash /workspace/stop.sh # PID files, EngineCore children\nbash scripts/inference-tunnel.sh down\n```\n\n---\n\n## Known Issues & Open Items\n\n### Unresolved Problems\n\n1. **Runtime Parity Gap:** vLLM fp16 vectors vs llama.cpp Q8 query vectors disagree (~0.175 Hit@1). Cause: instruct prefix formatting. Self-retrieval test (40/40 documents find themselves) proves vector spaces are identical.\n\n2. **Arabic Cards:** All 948 Arabic-content docs have English-only search cards. Bilingual cards required before cutover.\n\n3. **Owed Work Rails:** Four GPU lanes are standalone scripts, not database-backed obligations. State lives on disk, not Postgres.\n\n### Wiring Items\n\n| Item | Status | Notes |\n|------|--------|-------|\n| Bilingual cards | Open | Required for Arabic cutover |\n| Loose-file dedupe | Open | HEIC/JPG twins cause duplicate results |\n| Email routing planner | Open | Hook exists, not wired |\n| ERP retrieval source | Open | Parked in next_todo |\n| Context proposal gate | Built | Gated, SQL dump-first activation |\n\n---\n\n## File Structure\n\n```\ndocs/pipeline/\n├── README.md # Module overview\n├── DIAGNOSIS-2026-07-29.md # Pipeline defects root cause\n├── PIPELINE-V2-SPEC-2026-07-30.md # Owner-agreed design\n├── AB-TEST-REPORT-2026-07-30.md # OCR model comparison\n├── AUDIT-100-PAGES-2026-07-30.md # Image-judged audit\n├── DIFF-AUDIT-1000-2026-07-31.md # Identifier fidelity\n├── BLIND-AUDIT-1000-2026-07-31.md # Source-of-truth audit\n├── BENCH-EMBED-2026-07-31.md # Embedder benchmark\n├── CAD-DISCOVERY-2026-07-31.md # DWG toolchain\n├── SEARCH-BENCH-2026-07-31.md # Full pipeline benchmark\n├── QUERY-EMBEDDING-HOSTING.md # CPU vs GPU decision\n├── QUANTIZATION-REPORT-2026-08-02.md # Quantization results\n├── GPU-SESSION-RUNBOOK.md # Operational procedures\n└── scripts/pipeline-v2/ # Harness code\n```\n\n---\n\n## Integration Points\n\n### External Services\n\n| Service | Purpose | Configuration |\n|---------|---------|---------------|\n| Meilisearch | FTS on descriptions | `MEILI_MASTER_KEY` env var |\n| Qdrant | Semantic vectors | `startup_19` collection |\n| Vast API | GPU provisioning | Rate-limited ~3 req/min |\n| DeepInfra | Nemotron agent | API-only, not local |\n\n### Database Schema\n\n```prisma\n// EnrichBatchItem (obligation shape)\nmodel EnrichBatchItem {\n id String @id @default(uuid())\n startupId String\n nodeId String\n kind String\n sectionKey String\n status String @default(pending)\n contentHash String\n @@unique([startupId, nodeId, kind, sectionKey])\n}\n```\n\n### Code References\n\n- `modules/parsing/enrich-batch.service.ts` — Vertex AI batch consumer\n- `retrieval-v2.service.ts` — Search fusion orchestrator\n- `docs_orchestrator.py` — Agentic retrieval rounds (14s wall clock)\n- `scripts/pipeline-v2/_env.py` — Path/endpoint resolution\n\n---\n\n## Cost Analysis\n\n| Component | Cost | Notes |\n|-----------|------|-------|\n| Full corpus (14.7k docs) | ~$60-70 | 4× RTX 6000 WS, ~9.5h |\n| Single re-run (old pipeline) | ~$600 | Google Cloud comparison |\n| GPU box (1× RTX 6000 WS) | $0.757/h | Vast instance |\n| Quantization (Q8_0) | Free | No accuracy loss |\n| Bilingual cards (1.4k docs) | ~$1.60 | 2.1h on live box |\n\n**ROI:** v2 pipeline costs ~10× less than re-running old pipeline, with 2.5× better retrieval and 4× faster latency.\n\n---\n\n## Decision Log\n\n### Owner Decisions (2026-07-30 → 2026-08-05)\n\n1. **Qwen-vision replaces OCR** — No OCR models in v2, vision reads images directly\n2. **F2LLM-v2-14B for embeddings** — 3× Hit@1 over incumbent, 2560 dims\n3. **CPU for query embedder** — 8.26s p50 latency, 28 GB resident\n4. **Arabic threshold ≥50 chars** — 1,468 documents, ~$3.70\n5. **Lane order: 3 → 2 → 1 → 4** — Dependency and risk-driven\n6. **Remove instruct prefix** — +0.030 Hit@1 improvement\n7. **Context gated by proposal** — No automatic learning from retrieval\n\n### Technical Decisions\n\n1. **Four independent vLLM instances** — 3.9× throughput vs tensor parallel\n2. **Q8_0 quantization** — Free accuracy, 14 GB vs 28 GB\n3. **Marker-resumable per document** — Lane state survives interruption\n4. **Unavailability ≠ failure** — Dead tunnel queues work, doesn't fail\n\n---\n\n## Contributing\n\n### Adding New Document Types\n\n1. Create handler in `scripts/pipeline-v2/`\n2. Add `parseMethod` to `parse_workflow`\n3. Wire to `build_manifests.py`\n4. Test with `preflight.sh`\n\n### Adding New Embedding Model\n\n1. Benchmark with `quant_bench3.py`\n2. Verify runtime parity (self-retrieval test)\n3. Update `QUERY-EMBEDDING-HOSTING.md`\n4. Re-embed corpus if needed\n\n### Adding New Retrieval Leg\n\n1. Define leg in `retrieval-v2.service.ts`\n2. Add to `docs_orchestrator.py` round budget\n3. Test with `search_bench.py`\n4. Update `PIPELINE-V2-SPEC-2026-07-30.md`\n\n---\n\n## See Also\n\n- [../next_todo.md](../next_todo.md) — Open wiring items\n- [../architecture/selfhosted_inference_runbook.md](../architecture/selfhosted_inference_runbook.md) — Inference architecture\n- [../.sdd-work/gpu-lane-prep/MANIFESTS.md](../../.sdd-work/gpu-lane-prep/MANIFESTS.md) — Lane derivation\n\n---\n\n*Last updated: 2026-08-05*","docs-product":"# docs — product\n\n\n\n# docs — product Module\n\n## Overview\n\nThe `docs/product` module is a **verification and gap-tracking documentation system** that records end-to-end product testing, capability verification, and vision-vs-reality analysis. It serves as the single source of truth for what the platform does, what it doesn't do yet, and what needs to be built next.\n\nThis is not a code module in the traditional sense — it's a **living documentation repository** that tracks:\n- Vision gaps discovered through user-facing testing\n- AI capability verification and tier boundaries\n- Customer flow variations and friction points\n- ERP backend customization gaps\n- Vertical use case coverage (dental, real estate, interior design, marketing, awqaf)\n\n## Purpose\n\nThe module exists to answer three questions for the development team:\n\n1. **What does the platform actually do today?** (verified through live testing)\n2. **What does the vision claim it should do?** (from PLATFORM_OVERVIEW.md and related docs)\n3. **What's the gap, and how do we fill it?** (prioritized action items)\n\nEvery document in this module is **evidence-backed** — claims are supported by test runs, database reads, screenshots, and execution transcripts.\n\n## Structure\n\n```\ndocs/product/\n├── 2026-06-30-vision-gaps.md # ERP test pipeline gaps\n├── AI_SECTION_REPORT_2026-07-10.md # AI capabilities verification\n├── ASSISTANT_CAPABILITY_MATRIX.md # Tool visibility by tier/role\n├── COMPANY-TYPES-REPORT-2026-07-26.md # Real company testing\n├── CUSTOMER_FLOW_VARIATIONS_2026-07.md # Daily work stories\n├── ERP_BACKEND_CUSTOMIZATION_GAPS_2026-07-02.md # Vertical use-case probes\n├── ERP_USECASE_AWQAF_2026-07-11.md # Awqaf vertical coverage\n└── [additional verification docs]\n```\n\n### Document Types\n\n| Document | Purpose | Key Content |\n|----------|---------|-------------|\n| `*-vision-gaps.md` | Vision vs reality analysis | Gap IDs, severity, root cause, fix status |\n| `AI_SECTION_REPORT_*.md` | AI capability verification | Tool counts, tier boundaries, quality benchmarks |\n| `ASSISTANT_CAPABILITY_MATRIX.md` | Tool visibility reference | 58 tools × tier × role × preview |\n| `COMPANY-TYPES-REPORT_*.md` | Real business testing | 4 companies, 16 people, measured metrics |\n| `CUSTOMER_FLOW_VARIATIONS_*.md` | Daily work stories | 20 flows, friction list, fix wave status |\n| `ERP_BACKEND_CUSTOMIZATION_GAPS_*.md` | Vertical use-case probes | 8 gap axes, probe evidence, fill status |\n| `ERP_USECASE_*.md` | Vertical coverage check | App sweep, incumbent parity, new gaps |\n\n## How It Works\n\n### Verification Standard\n\nEvery claim in these documents must be **live-verified** through the real running stack:\n\n```\nClaim → Live Test → Database Read → Evidence File\n```\n\nExample from `CUSTOMER_FLOW_VARIATIONS_2026-07.md`:\n\n```\nClaim: \"Custom fields persist but don't display back\"\nTest: Owner fills custom field on lead form\nEvidence: `aq4-lead-custom-fields.png` (screenshot)\nDB Read: `psql` confirms value exists in `ir_model_fields`\nVerdict: `handled-with-friction`\n```\n\n### Evidence Organization\n\nEach verification session has a dedicated evidence directory:\n\n```\n.sdd-work/customer-flows-2026-07/\n├── evidence/\n│ ├── residue-inventory.md\n│ └── fix-verify/\n├── ibtisama/\n│ ├── IB-1.json\n│ └── IB-1b-custom-fields-filled.png\n├── aqarat/\n│ └── AQ-4.json\n└── wahaj/\n └── WH-2.json\n```\n\n### Gap Tracking\n\nGaps are assigned IDs and severity levels:\n\n```markdown\nG1 — Governance was broken platform-wide: passkey approvals failed for everyone *(§8 Governance — Mutations; HIGH)*\n```\n\n- **G** = Gap ID\n- **Severity**: HIGH, MED, LOW, P0, P1, P2, P3\n- **§** = Vision section reference (e.g., §8 = Governance)\n\n## Key Components\n\n### 1. Gap Registry\n\nThe `vision-gaps.md` document maintains the master gap list:\n\n```\nG1 — Passkey governance (FIXED)\nG2 — Agent ERP reach (OPEN)\nG3 — Peer notes (OPEN)\nG4 — Win→project bridge (OPEN)\nG5 — Owner project creation (OPEN)\nG6 — Tenant isolation nuance (OPEN)\nG7 — Documentation accuracy (OPEN)\nG8 — Config hygiene (OPEN)\n```\n\nEach gap includes:\n- Vision section reference\n- Reality finding\n- Status (fixed/open)\n- Fix direction\n\n### 2. Capability Matrix\n\n`ASSISTANT_CAPABILITY_MATRIX.md` is auto-generated from live registry:\n\n```python\n# Generated by: Harakat_AI/scripts/generate_capability_matrix.py\n# Source: core/tools.py TOOLS + agents/specialists/tools.py derive_tools\n```\n\n| Column | Meaning |\n|--------|---------|\n| `tool` | Tool name (e.g., `send_email`, `create_task`) |\n| `category` | Tool category (email, tasks, erp, documents) |\n| `min_tier` | Minimum tier to see this tool (A=0, Z=25) |\n| `role` | Role requirement (admin, member) |\n| `preview` | Requires human approval before execution |\n| `external` | Makes external API calls |\n| `credits` | Token cost |\n\n### 3. Fix Wave Tracking\n\n`CUSTOMER_FLOW_VARIATIONS_2026-07.md` tracks fix waves:\n\n```markdown\n## Addendum — fix wave (2026-07-04): findings 1–5 FIXED\n\n1. **Assistant \"ERP is down\" cascade — FIXED.**\n - Odoo caller faults rewritten into agent-actionable errors\n - Circuit breaker no longer counts tool-level failures\n - AI surfaces real reason instead of bare 502\n - Evidence: `evidence/fix-verify/`\n```\n\n### 4. Vertical Use-Case Axes\n\n`ERP_BACKEND_CUSTOMIZATION_GAPS_2026-07-02.md` defines 8 gap axes:\n\n```\nGAP-T — Procedure triggers (P0)\nGAP-R — UI propagation (P1)\nGAP-V — Versioned changesets (P1)\nF-P7-boundary — Employees-core exclusion (P0)\nGAP-S — Stock-element reshaping (P1)\nGAP-C — Tenant-defined connections (P2)\nGAP-D — Procedure data flow (P2)\nGAP-F — Field-type coverage (P2)\nGAP-M — New record types (P3)\n```\n\nEach axis has:\n- Severity (P0-P3)\n- Description\n- Fill direction\n- Status (CLOSED/OPEN)\n\n## Connection to Codebase\n\n### Direct Code References\n\nDocuments reference actual code paths:\n\n```markdown\n- `webauthn.service.ts` stores credential id verbatim (G1 fix)\n- `core/tools.py` exposes `notify_user` but no `send_peer_note` (G3)\n- `crates/kernel-storage/src/kernel.rs:781` (G5 owner project creation)\n- `agents/specialists/tools.py derive_tools` (capability matrix)\n- `erp.service vitest 4/4` (fix wave verification)\n```\n\n### Execution Flow Integration\n\nThe verification process follows this pattern:\n\n```mermaid\nflowchart TD\n A[User Story] --> B[Live Test]\n B --> C{Pass/Fail}\n C -->|Pass| D[Evidence: JSON + Screenshots]\n C -->|Fail| E[Root Cause Analysis]\n E --> F[Fix Direction]\n F --> G[Fix Wave]\n G --> H[Re-verify]\n H --> C\n```\n\n### Tool Gateway Integration\n\nAll tool verification goes through the Tool Gateway:\n\n```\nAssistant → Tool Gateway → Policy Check → Tool Execution → Result\n```\n\nDocuments verify:\n- Tier boundaries (12/12 probes passed)\n- Preview discipline (approve/decline/modify)\n- Policy enforcement (structural + semantic)\n- File creation (create_document/edit_document)\n\n## How to Use\n\n### For Developers\n\n1. **Find the gap**: Search `vision-gaps.md` for your area\n2. **Check status**: Is it fixed, open, or dispositioned?\n3. **Read evidence**: Go to the evidence directory for test runs\n4. **Understand fix direction**: Each gap has a \"To fill the vision\" section\n\n### For Testing\n\n1. **Run a flow**: Use the harness scripts in `.sdd-work/`\n2. **Capture evidence**: JSON logs + screenshots + DB reads\n3. **Update document**: Add to the appropriate verification report\n4. **Link to gap**: Reference the gap ID in your findings\n\n### For Planning\n\n1. **Priority order**: Use the \"Priority order to fill the vision\" sections\n2. **Fix waves**: Track what's in the current fix wave\n3. **Dependencies**: Note which gaps block others (e.g., G1 blocks §8 governance)\n\n## How to Contribute\n\n### Adding a New Verification Report\n\n```markdown\n# [Topic] — [Date]\n\n**Date:** 2026-XX-XX\n**Scope:** [what was tested]\n**Verification standard:** [how it was verified]\n\n## Executive summary\n\n- [Key finding 1]\n- [Key finding 2]\n\n## [Section]\n\n[Content with evidence links]\n```\n\n### Updating Gap Status\n\nWhen a gap is fixed:\n\n1. Update the gap document with fix details\n2. Add evidence to the evidence directory\n3. Link to the fix wave\n4. Update the priority order if needed\n\n### Adding Evidence\n\n```\n.sdd-work/[topic]/\n├── [flow-name].json\n├── [flow-name]-screenshot.png\n├── [flow-name]-transcript.txt\n└── evidence/\n └── [sub-evidence]\n```\n\n## Integration Points\n\n### With PLATFORM_OVERVIEW.md\n\nThe vision gaps document is a companion to `PLATFORM_OVERVIEW.md`:\n\n```markdown\n> Companion to PLATFORM_OVERVIEW.md (\"what the platform does today\").\n> This maps the gaps between the stated vision and the live behaviour.\n```\n\n### With Next Todo\n\nGap fills feed into the next todo:\n\n```markdown\n## Plan-coverage audit (2026-07-11)\n\n| Gap | Covered by an existing plan? | Where / what's missing |\n|-----|------------------------------|------------------------|\n| AW-1 reports | ~⅔ covered | next_todo 1.2 + Hermes code-exec |\n| AW-3 fund arithmetic | ~½ covered | P7 sandbox + standing-procedure step |\n```\n\n### With Architecture Decisions\n\nGap fills may require architecture decisions:\n\n```markdown\n## GAP-M decision (2026-07-17)\n\n**DECIDED: no generic new-model op.**\nNew record TYPES stay out of scope — the platform composes existing\ncatalog models instead of minting Odoo models per tenant.\n[Decision record](../architecture/decisions/2026-07-17-rescope-gap-m-entities-as-data.md)\n```\n\n## Maintenance\n\n### Auto-Generated Documents\n\nSome documents are auto-generated:\n\n- `ASSISTANT_CAPABILITY_MATRIX.md` — run `Harakat_AI/scripts/generate_capability_matrix.py`\n- Tier boundary probes — run `.sdd-work/ai-section/probe-tier-capabilities.cjs`\n\n### Cleanup\n\n```bash\n# Cleanup vertical tenants\npnpm dotenv -e .env.local -e .env -- tsx src/scripts/cleanup-vertical-tenants.ts --apply\n\n# Verify residue = none\npnpm dotenv -e .env.local -e .env -- tsx src/scripts/cleanup-vertical-tenants.ts --verify\n```\n\n### Versioning\n\nEach document includes a date and version:\n\n```markdown\n# Vision Gaps — what the 2026-06-30 ERP test pipeline exposed\n> Companion to PLATFORM_OVERVIEW.md\n```\n\n## Common Patterns\n\n### Gap Discovery Pattern\n\n```\n1. User-facing test (real UI, DB-verified)\n2. Evidence captured (JSON + screenshots)\n3. Gap identified (G# — description; severity)\n4. Root cause found\n5. Fix direction specified\n6. Fix implemented\n7. Re-verified\n8. Document updated\n```\n\n### Fix Wave Pattern\n\n```\n1. Findings identified\n2. Fix spec written\n3. Code changes made\n4. Re-verification run\n5. Evidence collected\n6. Document updated\n7. Next wave planned\n```\n\n## Related Modules\n\n| Module | Relationship |\n|--------|--------------|\n| `docs/platform/` | Platform architecture (companion) |\n| `docs/next_todo.md` | Next work items (gap fills feed here) |\n| `.sdd-work/` | Evidence directory (raw test data) |\n| `Harakat_AI/` | AI service (capability verification) |\n| `HarakatOffices_Server/` | Server (tool gateway, ERP) |\n| `architecture/` | Architecture decisions (gap fills may require) |\n\n## Summary\n\nThe `docs/product` module is the **verification backbone** of the platform. It:\n\n- Tracks what works and what doesn't\n- Provides evidence for every claim\n- Prioritizes what needs to be built\n- Connects testing to code changes\n- Feeds into the next todo\n\nEvery developer should read the relevant gap documents before starting work in that area. Every fix should be documented here. Every verification should have evidence in the `.sdd-work/` directory.","docs-reference":"# docs — reference\n\n\n\n# docs/reference Module\n\n## Overview\n\nThe `docs/reference` module contains three distinct types of documentation artifacts:\n\n1. **Implementation examples** - Working code samples for external integrations\n2. **Procedure specifications** - Human-readable workflow definitions for business processes\n3. **Skill documentation** - Technical guides for document manipulation capabilities\n\nThis module serves as a reference library for developers who need to understand or extend these specific capabilities.\n\n---\n\n## File: `google-liveapi-example.py`\n\n### Purpose\n\nA complete implementation example for Google's LiveAPI (real-time multimodal AI) integration. This script demonstrates how to:\n\n- Connect to Google's LiveAPI with audio/video streaming\n- Handle bidirectional audio communication\n- Stream camera or screen capture to the model\n- Process real-time responses from the model\n\n### Key Components\n\n#### Configuration Constants\n\n```python\nMODEL = \"models/gemini-3.1-flash-live-preview\"\nSEND_SAMPLE_RATE = 16000 # Audio sent to model\nRECEIVE_SAMPLE_RATE = 24000 # Audio received from model\nCHUNK_SIZE = 1024 # Audio buffer size\nDEFAULT_MODE = \"camera\" # Default video source\n```\n\n#### Client Setup\n\n```python\nclient = genai.Client(\n http_options={\"api_version\": \"v1beta\"},\n api_key=os.environ.get(\"GEMINI_API_KEY\"),\n)\n\nCONFIG = types.LiveConnectConfig(\n response_modalities=[\"AUDIO\"],\n media_resolution=\"MEDIA_RESOLUTION_MEDIUM\",\n speech_config=types.SpeechConfig(\n voice_config=types.VoiceConfig(\n prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=\"Zephyr\")\n )\n ),\n context_window_compression=types.ContextWindowCompressionConfig(\n trigger_tokens=104857,\n sliding_window=types.SlidingWindow(target_tokens=52428),\n ),\n)\n```\n\n#### AudioLoop Class Architecture\n\nThe `AudioLoop` class orchestrates all real-time communication tasks using `asyncio.TaskGroup`.\n\n```mermaid\ngraph TD\n A[AudioLoop.run] --> B[asyncio.TaskGroup]\n B --> C[send_text_task]\n B --> D[send_realtime_task]\n B --> E[listen_audio_task]\n B --> F[get_frames_task]\n B --> G[get_screen_task]\n B --> H[receive_audio_task]\n B --> I[play_audio_task]\n \n C --> J[User Input Queue]\n D --> K[Media Queue]\n E --> L[Audio In Queue]\n F --> M[Camera Stream]\n G --> N[Screen Stream]\n H --> O[Audio In Queue]\n I --> L\n```\n\n### Task Breakdown\n\n| Task | Method | Purpose |\n|------|--------|---------|\n| User input | `send_text()` | Accepts text commands from user |\n| Media streaming | `send_realtime()` | Sends camera/screen frames to model |\n| Microphone input | `listen_audio()` | Captures audio from microphone |\n| Camera capture | `get_frames()` | Streams camera video (1 FPS) |\n| Screen capture | `get_screen()` | Streams screen capture (1 FPS) |\n| Model response | `receive_audio()` | Receives audio/text from model |\n| Audio playback | `play_audio()` | Plays received audio to speakers |\n\n### Critical Implementation Details\n\n#### Async Thread Safety\n\nAll blocking operations use `asyncio.to_thread()`:\n\n```python\n# Camera capture - blocks if not in thread\nframe = await asyncio.to_thread(self._get_frame, cap)\n\n# Audio stream setup - blocks if not in thread\nself.audio_stream = await asyncio.to_thread(\n pya.open,\n format=FORMAT,\n channels=CHANNELS,\n rate=SEND_SAMPLE_RATE,\n input=True,\n frames_per_buffer=CHUNK_SIZE,\n)\n```\n\n#### Queue Management\n\n```python\nself.audio_in_queue = asyncio.Queue() # Model → Speaker\nself.out_queue = asyncio.Queue(maxsize=5) # User → Model\n```\n\nThe `out_queue` has a maxsize of 5 to prevent overflow when the model is slow to respond.\n\n#### Image Processing Pipeline\n\n```python\ndef _get_frame(self, cap):\n ret, frame = cap.read()\n if not ret:\n return None\n \n # BGR → RGB conversion (OpenCV default is BGR)\n frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)\n img = PIL.Image.fromarray(frame_rgb)\n img.thumbnail([1024, 1024])\n \n # Encode to JPEG\n image_io = io.BytesIO()\n img.save(image_io, format=\"jpeg\")\n image_io.seek(0)\n \n return {\n \"mime_type\": \"image/jpeg\",\n \"data\": base64.b64encode(image_io.read()).decode()\n }\n```\n\n### Error Handling\n\n```python\nasync def run(self):\n try:\n async with (\n client.aio.live.connect(model=MODEL, config=CONFIG) as session,\n asyncio.TaskGroup() as tg,\n ):\n # ... task setup ...\n await send_text_task\n raise asyncio.CancelledError(\"User requested exit\")\n \n except asyncio.CancelledError:\n pass\n except ExceptionGroup as EG:\n if self.audio_stream is not None:\n self.audio_stream.close()\n traceback.print_exception(EG)\n```\n\n### Usage\n\n```bash\n# Camera mode (default)\npython google-liveapi-example.py\n\n# Screen capture mode\npython google-liveapi-example.py --mode screen\n\n# Disable video\npython google-liveapi-example.py --mode none\n```\n\n---\n\n## File: `material-request-form-procedure.md`\n\n### Purpose\n\nA complete procedure specification for the Material Request Form workflow. This document maps directly to database schema fields and defines the complete business logic for procurement requests.\n\n### Architecture Mapping\n\n| Document Section | Database Entity |\n|-----------------|-----------------|\n| Procedure registration | `Procedure` row |\n| Step definitions | `Procedure.steps` JSON |\n| Run execution | `ProcedureRun` rows |\n| Capability scores | `CapabilityRun` rows |\n| Audit trail | `AuditEntry` rows |\n| Policy conflicts | `PolicyConflict` rows |\n\n### Procedure Flow\n\n```mermaid\nflowchart TD\n Start[User triggers procedure] --> S1[s1: Intake & Validation]\n S1 --> S2[s2: Estimation]\n S2 --> S3[s3: In Contract Decision]\n \n S3 -->|score ≥ 0.7| S4[s4: Issue PO]\n S3 -->|score ≤ 0.3| S7[s7: Manager Decision]\n S3 -->|\"0.3 < score < 0.7\"| Pause[PAUSED_AWAITING_APPROVAL]\n \n S7 -->|Client Approval| S8[s8: Client Approval]\n S7 -->|Variation| S9[s9: Estimation/Variation]\n S7 -->|Reject| Abort[Abort]\n \n S8 --> S4\n S9 --> S8\n S4 --> S5[s5: Procurement]\n S5 --> S6[s6: Delivery]\n S6 --> Complete[Procedure Completed]\n \n S4 -->|PO blocked| Escalate[Escalate]\n S5 -->|No quotes| Escalate\n S6 -->|Delivery delayed| Escalate\n```\n\n### Step Definitions\n\n| Step ID | Type | Assignee | Key Tools |\n|---------|------|----------|-----------|\n| s1 | `human_input` | Requester | OCR, Document extraction |\n| s2 | `assistant_action` | Estimation | Rate card lookup, cost calculation |\n| s3 | `decision` | Auto/Manager | `in_contract_classifier` |\n| s4 | `assistant_action` | Finance | `finance/issue_po` MCP |\n| s5 | `assistant_action` | Procurement | RFQ, quote ranking |\n| s6 | `wait` | Site | Email polling |\n| s7 | `human_input` | Manager | Branch selection |\n| s8 | `assistant_action` | Client Liaison | Email, approval tracking |\n| s9 | `assistant_action` | Estimation | Variation calculation |\n\n### Capability Training\n\nTwo predictive capabilities are proposed for this procedure:\n\n| Capability | Training Data | Features |\n|------------|---------------|----------|\n| `in_contract_classifier` | Manager decisions at s3 | Total cost, BOQ coverage, rate deviation |\n| `quote_ranker` | Vendor selections at s5 | Quote price, vendor history, lead time |\n\n### Policy Gates\n\n| Policy | Effect |\n|--------|--------|\n| `externalCommsAllowed: false` | Email steps require human approval |\n| `highStakesThresholdUsd: 5000` | POs > $5K require WebAuthn passkey |\n| `permittedCapabilityActions` | Blocks disallowed AI actions |\n| `dataRetention: 13 months` | Auto-archive after 13 months |\n\n---\n\n## File: `writing_docs_skill.md`\n\n### Purpose\n\nTechnical documentation for the DOCX document creation and manipulation skill. This guide covers the `docx-js` library usage patterns and common pitfalls.\n\n### Core Architecture\n\nDOCX files are ZIP archives containing XML. The skill provides two main approaches:\n\n1. **Programmatic creation** - Using `docx-js` to generate new documents\n2. **XML editing** - Unpack → edit XML → repack for modifications\n\n### Document Creation Patterns\n\n#### Page Size Configuration\n\n```javascript\nsections: [{\n properties: {\n page: {\n size: {\n width: 12240, // US Letter in DXA\n height: 15840\n },\n margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }\n }\n }\n}]\n```\n\n**DXA Unit Reference:** 1440 DXA = 1 inch\n\n| Paper | Width (DXA) | Height (DXA) |\n|-------|-------------|--------------|\n| US Letter | 12,240 | 15,840 |\n| A4 | 11,906 | 16,838 |\n\n#### Critical Rules\n\n| Rule | Reason |\n|------|--------|\n| Never use `WidthType.PERCENTAGE` | Breaks in Google Docs |\n| Always use `WidthType.DXA` | Consistent rendering |\n| Set `outlineLevel` on headings | Required for TOC |\n| Use `LevelFormat.BULLET` for bullets | Unicode bullets don't work |\n| Set `type` on `ImageRun` | Required parameter |\n| Use `ShadingType.CLEAR` | Prevents black backgrounds |\n\n#### Table Implementation\n\n```javascript\nnew Table({\n width: { size: 9360, type: WidthType.DXA },\n columnWidths: [4680, 4680],\n rows: [\n new TableRow({\n children: [\n new TableCell({\n width: { size: 4680, type: WidthType.DXA },\n borders: { top: border, bottom: border, left: border, right: border },\n children: [new Paragraph({ children: [new TextRun(\"Cell\")] })]\n })\n ]\n })\n ]\n})\n```\n\n**Table width rules:**\n- Table `width` must equal sum of `columnWidths`\n- Each cell `width` must match its column\n- Cell `margins` are internal padding (not added to width)\n\n### Document Editing Workflow\n\n```mermaid\nflowchart LR\n A[Input .docx] --> B[Unpack XML]\n B --> C[Edit XML files]\n C --> D[Pack .docx]\n D --> E[Validate]\n E -->|Fail| F[Auto-repair]\n F --> D\n E -->|Pass| G[Output .docx]\n```\n\n#### Step 1: Unpack\n```bash\npython scripts/office/unpack.py document.docx unpacked/\n```\n\n#### Step 2: Edit XML\n- Use `comment.py` for tracked changes\n- Use smart quotes: `’` (apostrophe), `“` (opening quote)\n- Replace entire `<w:r>` blocks for tracked changes\n\n#### Step 3: Pack\n```bash\npython scripts/office/pack.py unpacked/ output.docx --original document.docx\n```\n\n### Dependencies\n\n| Tool | Purpose |\n|------|---------|\n| `docx` (npm) | Programmatic document creation |\n| `pandoc` | Text extraction from DOCX |\n| `LibreOffice` | PDF conversion |\n| `Poppler` | Image extraction from PDF |\n\n---\n\n## Module Integration\n\n### How These Files Connect\n\n```mermaid\ngraph LR\n A[docs/reference] --> B[google-liveapi-example.py]\n A --> C[material-request-form-procedure.md]\n A --> D[writing_docs_skill.md]\n \n B --> E[genai.Client]\n B --> F[asyncio.TaskGroup]\n \n C --> G[ProcedureRun]\n C --> H[CapabilityRun]\n C --> I[AuditEntry]\n \n D --> J[docx-js]\n D --> K[XML editing]\n```\n\n### When to Use Each File\n\n| Use Case | Reference File |\n|----------|----------------|\n| Real-time AI with audio/video | `google-liveapi-example.py` |\n| Procurement workflow implementation | `material-request-form-procedure.md` |\n| DOCX document generation | `writing_docs_skill.md` |\n\n### Contributing Guidelines\n\n1. **google-liveapi-example.py**: Update only when API changes require breaking modifications\n2. **material-request-form-procedure.md**: Update when SOP changes or new capabilities are added\n3. **writing_docs_skill.md**: Update when `docx-js` library changes or new patterns emerge\n\nAll changes should maintain backward compatibility with existing implementations.","docs-retrieval":"# docs — retrieval\n\n\n\n# Retrieval Module — Technical Documentation\n\n## Overview\n\nThe Harakat retrieval system is a **vectorless, hierarchical, agentic retrieval pipeline** that combines Meilisearch full-text search with AI-generated document summaries and an agentic orchestrator. The system is intentionally designed without embedding models or vector databases to minimize operational complexity and cost.\n\nA hybrid vector option (Gemini Embedding 2 + Qdrant) is available as an optional add-on for specific use cases: multilingual retrieval, scanned-image PDFs, and paraphrase matching.\n\n---\n\n## Current Architecture\n\n### Core Design Principles\n\n| Principle | Rationale |\n|-----------|-----------|\n| **Vectorless by default** | No embedding bill, no re-indexing on model changes, minimal VM footprint |\n| **Hierarchical indexing** | PageIndex trees split documents into sections with AI descriptions |\n| **Agentic loop** | Search → open tree → read pages → refine |\n| **Tier-filtered** | All retrieval respects tenant access boundaries |\n\n### System Components\n\n```mermaid\nflowchart TB\n subgraph Client\n QRY[User Query]\n end\n \n subgraph Server\n API[Knowledge Routes]\n ORCH[Docs Orchestrator]\n RETRIEVAL[Retrieval Budget Specialist]\n end\n \n subgraph AI Service\n CLEAN[clean_workflow.py]\n ENRICH[enrichment_workflow.py]\n MULTIMODAL[multimodal_description.py]\n end\n \n subgraph Queue Workers\n CLEAN_DOC[clean-document.worker.ts]\n ENRICH_DOC[enrich-document.worker.ts]\n SEARCH_IDX[search-index.worker.ts]\n TOC_REBUILD[toc-rebuild.worker.ts]\n end\n \n subgraph Storage\n MEILI[(Meilisearch)]\n DB[(Postgres)]\n end\n \n QRY --> API\n API --> ORCH\n ORCH --> RETRIEVAL\n RETRIEVAL -.-> CLEAN\n RETRIEVAL -.-> ENRICH\n RETRIEVAL -.-> MULTIMODAL\n CLEAN --> CLEAN_DOC\n ENRICH --> ENRICH_DOC\n ENRICH --> SEARCH_IDX\n SEARCH_IDX --> MEILI\n TOC_REBUILD --> DB\n MEILI --> API\n DB --> API\n```\n\n---\n\n## Data Models\n\n### Prisma Schema Extensions\n\nThe retrieval system adds 5 new tables and 3 columns to `ParsedDocument`:\n\n```prisma\n// New tables\nmodel DocumentAbstract {\n id Int @id @default(autoincrement())\n parsedDocumentId Int\n text String\n tierLevel Int\n createdAt DateTime @default(now())\n}\n\nmodel MultimodalDescription {\n id Int @id @default(autoincrement())\n parsedDocumentId Int\n text String\n tierLevel Int\n createdAt DateTime @default(now())\n}\n\nmodel FolderRollup {\n id Int @id @default(autoincrement())\n folderId Int\n text String\n tierLevel Int\n createdAt DateTime @default(now())\n}\n\nmodel TenantTOC {\n id Int @id @default(autoincrement())\n startupId Int\n text String\n tierLevel Int\n createdAt DateTime @default(now())\n}\n\nmodel Entity {\n id Int @id @default(autoincrement())\n parsedDocumentId Int\n text String\n tierLevel Int\n createdAt DateTime @default(now())\n}\n\n// Extended ParsedDocument\nmodel ParsedDocument {\n cleanedAt DateTime?\n docDescription String?\n // ... existing fields\n}\n```\n\n### Tier Filtering Logic\n\nTier filtering is enforced at multiple layers:\n\n```typescript\n// search.service.ts\nfunction tierStringToLevel(tier: string): number {\n // null = MOST_RESTRICTIVE_TIER (polarity flip)\n}\n\n// Every Meilisearch filter clause includes tier level\n// Every derived row inherits tier from source node\n// TenantTOC is redacted at read\n```\n\n---\n\n## Queue Architecture\n\n### Six BullMQ Queues\n\nAll workers self-skip when `ENABLE_RETRIEVAL_V2 !== \"true\"`:\n\n| Queue | Worker | Purpose |\n|-------|--------|---------|\n| `clean-document` | `clean-document.worker.ts` | Deterministic 9-rule pipeline (no LLM) |\n| `enrich-document` | `enrich-document.worker.ts` | AI-generated abstracts, multimodal descriptions |\n| `entity-extract` | `entity-extract.worker.ts` | Entity extraction with taxonomy constraints |\n| `search-index` | `search-index.worker.ts` | Meilisearch indexing |\n| `folder-rollup` | `folder-rollup.worker.ts` | Folder-level summaries |\n| `toc-rebuild` | `toc-rebuild.worker.ts` | Tenant table of contents |\n\n### Worker Registration\n\n```typescript\n// queues.ts\nexport const queues = {\n cleanDocument: 'clean-document',\n enrichDocument: 'enrich-document',\n entityExtract: 'entity-extract',\n searchIndex: 'search-index',\n folderRollup: 'folder-rollup',\n tocRebuild: 'toc-rebuild',\n}\n```\n\n### Worker Self-Skip Pattern\n\n```typescript\n// All workers in src/core/queue/workers/\nif (process.env.ENABLE_RETRIEVAL_V2 !== 'true') {\n return; // Skip execution\n}\n```\n\n---\n\n## AI Endpoints\n\n### Clean Workflow (Deterministic)\n\n```python\n# Harakat_AI/workflows/clean_workflow.py\n# 9-rule pipeline, no LLM\n# Input: raw document bytes\n# Output: cleaned text\n```\n\n### Multimodal Description\n\n```python\n# Harakat_AI/workflows/multimodal_description.py\n# Per-MIME dispatch:\n# - Excel → LibreOffice → PDF → vision\n# - PDF → PyMuPDF → vision\n# - Images → OCR + vision\n# - Text → fallback\n```\n\n### Enrichment Workflow (LLM)\n\n```python\n# Harakat_AI/workflows/enrichment_workflow.py\n# Four structured-JSON endpoints:\n# - /parse/abstract\n# - /parse/folder-rollup\n# - /parse/tenant-toc\n# - /parse/entities (taxonomy-constrained)\n```\n\n---\n\n## Agentic Retrieval Loop\n\n### Orchestrator Configuration\n\n```python\n# Harakat_AI/agents/specialists/retrieval_budget.py\n# Caps: 30k tokens, 14s wall-clock, 4 rounds\n# Env-overridable limits\n```\n\n### Retrieval Flow\n\n```python\n# Harakat_AI/agents/specialists/docs_orchestrator.py\n# 4-round agentic loop with retrieval_trace envelopes\n```\n\n### Server Entry Point\n\n```typescript\n// HarakatOffices_Server/src/modules/knowledge/knowledge.routes.ts\nPOST /api/startup/knowledge/retrieve\nModes: toc | search | abstract\n```\n\n### Retrieval Trace Envelope\n\n```python\n# Harakat_AI/envelopes/result_envelope.py\n# retrieval_trace kind added to envelopes\n```\n\n---\n\n## Hybrid Vector Option (Qdrant + Gemini Embedding 2)\n\n### When to Add Vectors\n\n**Add vectors if:**\n- Users hit the **paraphrase wall** (search words not in documents)\n- **Multilingual retrieval** matters (Arabic↔English)\n- **Scanned/image PDFs** are common and OCR fails\n- Willing to operate one more stateful service\n\n**Skip vectors if:**\n- Current retrieval quality meets needs\n- Document volume is modest and born-digital\n- Want minimal VM footprint and ops surface\n\n### Cost Analysis\n\n#### Storage/RAM (Qdrant)\n\n| Chunks | Raw Vectors | RAM (768-dim) | With Quantization |\n|--------|-------------|---------------|-------------------|\n| 100k | ~0.3 GB | ~0.5 GB | ~0.1 GB |\n| 1M | ~3 GB | ~4-6 GB | ~0.7-1.5 GB |\n| 5M | ~15 GB | ~20-30 GB | ~3-7 GB |\n\n**Recommendation:** Start at 768 dimensions with quantization enabled.\n\n#### Embedding Bill\n\n```\none-time backfill ≈ (total tokens) / 1,000,000 × $0.20\nincremental ≈ tokens per new/changed doc (batch = 50% off)\n```\n\n### Hybrid Architecture\n\n```mermaid\nflowchart LR\n DOC[Document] --> TXT[Text + PageIndex]\n TXT --> FTS[(Meilisearch)]\n TXT -.-> EMB[Gemini Embedding 2]\n EMB -.-> Q[(Qdrant)]\n QRY[User Query] --> FTS\n QRY -.-> EMB\n FTS --> MERGE[Merge + Rerank + Tier Filter]\n Q -.-> MERGE\n MERGE --> ANS[Answer with Citations]\n```\n\n### Implementation Path\n\n1. Keep Meilisearch as precision/keyword/tier-filter layer\n2. Add Gemini Embedding 2 + Qdrant as recall booster\n3. Start at 768 dimensions with quantization on\n4. Use batch API (50% off)\n5. Reuse existing tier filter for vector hits\n\n---\n\n## Operations & Maintenance\n\n### Environment Variables\n\n```bash\n# HarakatOffices_Server/.env\nMEILI_URL=http://meilisearch:7700\nMEILI_MASTER_KEY=<32-char random>\nENABLE_RETRIEVAL_V2=false # Default: keep off\n```\n\n### Migration Deployment\n\n```bash\ncd HarakatOffices_Server\nnpx prisma migrate deploy\n# Applies:\n# - 20260521000000_retrieval_v2_models\n# - 20260521010000_retrieval_v2_tenant_flag\n```\n\n### Per-Tenant Rollout\n\n```bash\n# List all tenants\npnpm flip:retrieval-v2 --list\n\n# Backfill pilot tenant (dry-run first)\npnpm backfill:retrieval-v2 --startup 19 --operator-email <email> --dry-run\npnpm backfill:retrieval-v2 --startup 19 --operator-email <email> --limit 200\n\n# Flip tenant on\npnpm flip:retrieval-v2 --startup 19 --on --operator-email <email>\n\n# Rollout to remaining tenants\npnpm flip:retrieval-v2 --startup <id> --on --operator-email <email>\n```\n\n### Rollback\n\n```bash\n# Force-disable for any tenant\npnpm flip:retrieval-v2 --startup <id> --off --operator-email <email>\n# AI service falls back to legacy LIKE path\n```\n\n### Monitoring\n\nWatch these metrics via PostHog:\n- `retrieval.latency_ms`\n- `retrieval.rounds`\n\n---\n\n## Security Invariants\n\n- ✅ `canSee` on every server read path\n- ✅ `writeAudit` per mutation\n- ✅ Polarity-flip (`null = MOST_RESTRICTIVE_TIER`) honored\n- ✅ Tier inheritance on derived rows\n- ✅ No client-side tierLevel input\n- ✅ Workers are SYSTEM actors (no agent-owned table writes)\n\n---\n\n## Troubleshooting\n\n### Failed PageIndex Build\n\n```typescript\n// retryFailedPageIndexBuilds(startupId, opts)\n// Re-enqueue parse-tree with force=true and smaller chunk-size\n// Fallback: deterministic outline from regex-detected headings\n```\n\n### Empty docDescription\n\n```typescript\n// Fallback: hash of first 1k cleaned chars\n// Ensures Meilisearch never sees null\n```\n\n### Worker Not Running\n\n```bash\n# Check queue registration\ngrep \"enqueueParseTree\" src/core/queue/queues.ts\n\n# Verify worker file exists\nls src/core/queue/workers/parse-tree.worker.ts\n```\n\n---\n\n## Related Documentation\n\n- `PLATFORM_OVERVIEW.md` §7 — vectorless retrieval explanation\n- `odoo.md` — semantic search over tabular data (separate question)\n- `docs/retrieval/EMBEDDINGS_QDRANT.md` — vector addition decision framework\n\n---\n\n## Version History\n\n| Version | Date | Status |\n|---------|------|--------|\n| v2 | 2026-05-21 | Phases A–I complete on disk |\n| v1 | N/A | Legacy LIKE-based retrieval |\n\n**Current State:** Gated behind `ENABLE_RETRIEVAL_V2=false`. Legacy retrieval remains unchanged until explicitly enabled.","docs-reviews":"# docs — reviews\n\n\n\n# docs/reviews — Code Review Reports Module\n\n## Overview\n\nThe `docs/reviews/` module is a collection of structured code review reports for the Harakat platform. These documents capture security, deployment, and scalability findings from periodic code audits. They serve as:\n\n- **Audit trails** for platform posture over time\n- **Actionable checklists** for engineering teams\n- **Reference material** for onboarding and incident response\n- **Compliance documentation** for security and deployment standards\n\nThis module does not contain executable code. It is documentation that documents the codebase.\n\n## Module Structure\n\n```\ndocs/reviews/\n├── 2026-07-06-deployment-scalability-review.md\n├── 2026-07-06-security-review.md\n├── 2026-07-07-addendum-a-throttling.md (security addendum)\n└── 2026-07-07-addendum-b-deep-dive.md (security addendum)\n```\n\nEach review follows a consistent format:\n\n| Section | Purpose |\n| --- | --- |\n| Executive Summary | High-level verdict and scope |\n| Severity Key | P0/P1/P2 classification definitions |\n| Findings | Detailed issues with evidence and fixes |\n| Verified Strengths | Positive patterns to preserve |\n| Prioritized Fix Table | Actionable remediation roadmap |\n\n## Review Classification\n\n### P0 (Critical)\nInternet-exposed unauthenticated access, default-secret exposure, or high-cost/high-privilege public bypass. Requires immediate remediation.\n\n### P1 (High)\nHigh-impact risks requiring valid tenant/admin tokens, XSS vectors, internal footholds, or operational conditions that could lead to P0.\n\n### P2 (Medium)\nHardening, defense-in-depth, supply-chain hygiene, or roadmap items that should be fixed but are not emergency stop conditions.\n\n## Key Architectural Patterns Documented\n\n### 1. Multi-Tenant Data Scoping\n\nThe reviews document how tenant isolation is implemented across services:\n\n```mermaid\ngraph TD\n A[Request] --> B{Auth Layer}\n B -->|JWT| C[StartupId]\n C --> D[Postgres]\n C --> E[Meilisearch]\n C --> F[Qdrant]\n D --> G[Tenant-Scoped Tables]\n E --> H[Tenant/Tier Filters]\n F --> I[Per-Tenant Collections]\n```\n\n**Key Files:**\n- `HarakatOffices_Server/src/modules/search/search.service.ts` - Meilisearch filtering\n- `HarakatOffices_Server/src/modules/search/vector.service.ts` - Qdrant collection routing\n- `HarakatOffices_Server/src/modules/conversation-memory/checkpoint-vector.service.ts` - Per-tenant vector storage\n\n### 2. Queue and Worker Architecture\n\nThe platform uses BullMQ for background job processing with tenant-aware job payloads:\n\n```mermaid\ngraph LR\n A[HTTP Request] --> B[Express API]\n B --> C{Job Type}\n C -->|Parse/Index| D[BullMQ Queue]\n C -->|AI/Embed| E[FastAPI AI Service]\n C -->|Meeting| F[Meeting Worker Pool]\n D --> G[Worker Concurrency]\n E --> H[DeepInfra Budget]\n F --> I[External Compute]\n```\n\n**Key Files:**\n- `HarakatOffices_Server/src/core/queue/queues.ts` - Queue definitions\n- `HarakatOffices_Server/src/core/queue/workers/*.worker.ts` - Worker implementations\n- `Harakat_AI/Dockerfile` - AI service worker configuration\n\n### 3. Authentication Flow\n\nMultiple token types exist with different lifetimes and purposes:\n\n```mermaid\ngraph TD\n A[User Login] --> B[Startup JWT - 7 days]\n A --> C[Session Cookie - HttpOnly]\n A --> D[Refresh Token - Rotated]\n B --> E[API Requests]\n C --> F[Frontend Auth]\n D --> G[Token Refresh]\n E --> H[Internal Services]\n F --> I[Client-Side]\n```\n\n**Key Files:**\n- `HarakatOffices_Server/src/middleware/startup-auth.ts` - Startup JWT verification\n- `HarakatOffices_Server/src/modules/startup-auth/startup-auth.service.ts` - Token issuance\n- `StartupsPlatform/lib/auth-context.tsx` - Client-side token storage\n\n## Critical Findings Summary\n\n### Deployment & Scalability (P0)\n\n| Finding | Impact | Fix Priority |\n| --- | --- | --- |\n| P0-1: User uploads stored in API container | Data loss on rebuild/scale | 1 |\n| P0-2: Manual/incomplete backups | Unrecoverable data loss | 2 |\n| P0-3: Single VM without resource limits | Noisy-neighbor failures | 4 |\n| P0-4: Split database topology | Wrong-DB deployments | 3 |\n\n### Security (P0)\n\n| Finding | Impact | Fix Priority |\n| --- | --- | --- |\n| P0-1: Host-published DB ports with fallback secrets | Direct DB access | Immediate |\n| P0-2: Public `/api/ai/` routes | Cost DoS, tenant bypass | Immediate |\n| P0-3: Public `/erp` god-mode access | ERP data exposure | Immediate |\n\n## How to Use These Reviews\n\n### For New Developers\n\n1. **Read the Executive Summary** of each review to understand current platform posture\n2. **Check the Prioritized Fix Table** to see what work is in progress\n3. **Review Verified Strengths** to understand patterns to preserve\n4. **Search for referenced files** to understand implementation details\n\n### For Security Audits\n\n1. **Cross-reference P0/P1 findings** with current code state\n2. **Verify fixes** by checking referenced file paths\n3. **Update severity** if findings have been remediated\n4. **Add new findings** following the same format\n\n### For Deployment Planning\n\n1. **Review P0 deployment findings** before production rollout\n2. **Check backup/restore procedures** in P0-2\n3. **Verify resource limits** are applied in `docker-compose.yml`\n4. **Confirm healthchecks** exist for all services\n\n## Contributing to Reviews\n\n### Adding a New Finding\n\n```markdown\n### P1-X. [Title]\n\nBehavior: [What the issue does]\n\nEvidence:\n- `file:line` [description]\n- `file:line` [description]\n\nFix:\n- [Actionable remediation steps]\n```\n\n### Updating Fix Status\n\nWhen a fix is implemented:\n1. Update the **Prioritized Fix Table** to mark as complete\n2. Add a **Verified OK** section entry\n3. Update the **Executive Summary** if severity changed\n4. Reference the commit/PR that fixed it\n\n## Integration with Codebase\n\n### CI/CD Pipeline\n\nReviews should be:\n- **Versioned** with dates in filenames\n- **Linked** to deployment branches\n- **Referenced** in release notes\n- **Scanned** for literal credentials (P2-5)\n\n### Monitoring & Alerting\n\nThe reviews identify gaps in observability:\n- No security event logging (B-8)\n- No alerting on query-string token usage (P1-1)\n- No tenant cost/usage dashboards (P1-10)\n\n### Related Documentation\n\n| Document | Purpose |\n| --- | --- |\n| `DEPLOYMENT.md` | Deployment procedures |\n| `docs/reviews/2026-07-06-security-review.md` | Security findings |\n| `docs/reviews/2026-07-06-deployment-scalability-review.md` | Scalability findings |\n\n## Common Patterns to Preserve\n\n### Tenant Isolation\n- Meilisearch requires `startupId` and tier filters\n- Qdrant uses one collection per tenant\n- Conversation checkpoint vectors are per-tenant\n\n### Queue Safety\n- Jobs use deterministic IDs\n- Completed jobs are removed\n- `FOR UPDATE SKIP LOCKED` for horizontal scaling\n\n### Streaming Reliability\n- Some SSE endpoints have 25-second keepalives\n- Heartbeat patterns exist in notifications/agent-board\n\n### Upload Validation\n- Route-specific multer size limits\n- Generated filenames prevent path traversal\n\n## Related Modules\n\n| Module | Connection |\n| --- | --- |\n| `HarakatOffices_Server` | Primary API server |\n| `Harakat_AI` | AI/LLM service |\n| `StartupsPlatform` | Next.js frontend |\n| `meeting-bot-worker` | Meeting recording |\n| `meeting-transcript-worker` | Transcript processing |\n| `docker-compose.yml` | Orchestration |\n\n## Maintenance Notes\n\n- Reviews are **time-bound** - re-run audits quarterly\n- **Addendums** capture follow-up findings\n- **Fix tables** should be updated as work completes\n- **Evidence links** must remain valid (file paths)\n- **Severity classifications** should be re-evaluated after fixes\n\n---\n\n*This documentation describes the `docs/reviews/` module. For implementation details of the reviewed systems, see the referenced source files.*","docs-security":"# docs — security\n\n\n\n# docs/security — Security Audit & Conformance Documentation\n\n## Overview\n\nThe `docs/security` module is a **documentation repository** containing security audit reports, conformance test specifications, and threat model documentation for the Harakat platform. It does not contain executable security code — instead, it documents security findings, compliance obligations, and architectural security requirements.\n\nThis module serves as the **single source of truth** for security posture, audit evidence, and remediation priorities across the HarakatOffices_Server, Harakat_AI, and StartupsPlatform services.\n\n## Purpose and Scope\n\n### Primary Objectives\n\n1. **Document security vulnerabilities** with severity ratings (Critical/High/Medium/Low)\n2. **Track remediation priorities** with actionable fix descriptions\n3. **Provide conformance test specifications** for tenant isolation verification\n4. **Maintain threat model documentation** for security engineering decisions\n\n### Scope Coverage\n\n| Component | Coverage |\n|-----------|----------|\n| HarakatOffices_Server | Full audit coverage |\n| Harakat_AI | Full audit coverage |\n| StartupsPlatform | Full audit coverage |\n| Infrastructure | Docker, secrets, logging |\n\n### Document Types\n\n```mermaid\ngraph TD\n A[Security Docs Module] --> B[Audit Reports]\n A --> C[Conformance Tests]\n A --> D[Threat Models]\n \n B --> B1[AUDIT_REPORT.md]\n B --> B2[Top 10 Prioritized Fixes]\n \n C --> C1[ab-isolation-conformance.md]\n C --> C2[Self-check Scripts]\n \n D --> D1[codex_security_audit.md]\n D --> D2[Trust Boundaries]\n```\n\n## Key Components\n\n### 1. AUDIT_REPORT.md\n\nThe primary security audit document covering four main areas:\n\n#### 1.1 Sync Data Pipeline\n\nDocuments issues in the Drive → ParsedDocument → KnowledgeNode → PageIndexTree pipeline:\n\n| Issue | Severity | Location |\n|-------|----------|----------|\n| Plaintext OAuth tokens & passwords | Critical | `prisma/schema.prisma#L1050-1051` |\n| No idempotency on re-sync | High | `knowledge.service.ts#L7` |\n| Per-section HTTP round-trip | High | `document_indexer.py#L182-216` |\n| Failure handling silently drops files | High | `google-drive.service.ts#L97-116` |\n| No backpressure on indexing queue | Medium | `indexing_workflow.py#L39` |\n| OCR lacks budget guard | Medium | `core/ocr.py` |\n| Drive heartbeat visibility | Medium | `drive-heartbeat.service.ts` |\n\n#### 1.2 Graph & Tree Construction\n\nDocuments issues in knowledge graph construction:\n\n| Issue | Severity | Location |\n|-------|----------|----------|\n| Inverted tier polarity | Critical | `knowledge.service.ts#L80-112` |\n| Tier suffix not enforced | High | `tierLevel` function |\n| No soft-delete on KnowledgeNode | Medium | N/A |\n| No edges between sections | Low | `PageIndexTree.structure` |\n| Tree-build unbounded memory | Medium | `parse_workflow.py#L454-490` |\n\n#### 1.3 Export Pipeline\n\nDocuments export-related security concerns:\n\n- Imperative scripts (`_check_*.js`, `_wipe_sync_data.js`, `_reset_sync.js`) with no auth/audit\n- Outbound email exfiltration risk via `_send_email`\n- Missing GDPR data export endpoint\n\n#### 1.4 Cybersecurity\n\nDocuments 12 security vulnerabilities:\n\n| Issue | Severity | Location |\n|-------|----------|----------|\n| No 2FA on StartupUser | Critical | `prisma/schema.prisma#L184` |\n| Admin 2FA codes stored unhashed | High | `prisma/schema.prisma#L17` |\n| No CSRF nonce on Drive OAuth | High | `google-drive.service.ts#L42` |\n| AI service trusts client startup_id | Critical | `indexing_workflow.py#L47-55` |\n| JWT secret default and rotation | Medium | N/A |\n| Service account JSON path | High | `docker-compose.yml#L57` |\n| Loose secrets in .env files | High | N/A |\n| Rate limiting is global | Medium | `rate-limit.ts` |\n| Mass-assignment risk | Medium | Controllers |\n| Prompt-injection surface | High | Agent tools |\n| No content hashing | Low | `StartupDocument` |\n| Logging of file content | Low | N/A |\n\n### 2. ab-isolation-conformance.md\n\nDefines the **A/B tenant-isolation conformance test suite** with 12 obligation rows:\n\n#### Test Structure\n\n```mermaid\ngraph LR\n A[Self-check] --> B[Ordering Model]\n A --> C[Live Lane Test]\n B --> D[CI Gate]\n C --> E[Stack Lock]\n E --> F[12 Assertions]\n```\n\n#### Obligation Table\n\n| Row | Obligation | Status |\n|-----|------------|--------|\n| 0 | `row_zero_routing_key` | ❌ FAIL |\n| 1 | `reassignment_staleness` | ❌ FAIL |\n| 2-11 | Various isolation checks | ✅ PASS |\n\n#### Key Findings\n\n- **Row 0 failure**: Token claiming another tenant is served on routes without `attachAccessClaims`\n- **Row 1 failure**: Reassigned employee reads former tenant for 7-day token lifetime\n- **11 of 12 rows conformant** with 1 pending (delegation grants require WebAuthn)\n\n### 3. codex_security_audit.md\n\nProvides the **threat model and attack surface documentation**:\n\n#### Trust Boundaries\n\n```mermaid\ngraph TD\n A[Public HTTP] --> B[Express/Prisma API]\n B --> C[Postgres/Redis]\n B --> D[AI FastAPI]\n D --> E[Privileged Workers]\n \n F[Frontend] -->|sessionStorage| B\n G[AI Service] -->|JWT| B\n```\n\n#### Attack Surface Categories\n\n| Category | Examples |\n|----------|----------|\n| Auth/session | Stale JWTs, logout not revoking |\n| Authorization/tenant isolation | IDOR on startupId, officeId, userId |\n| Files/document indexing | MIME spoofing, token in URLs |\n| AI/LLM/tools | Prompt injection, tool abuse |\n| OAuth/integrations | CSRF on callback, plaintext tokens |\n| Internal/callback/worker | Docker socket mount, service account paths |\n\n## How It Works\n\n### Audit Process Flow\n\n```mermaid\nsequenceDiagram\n participant Auditor\n participant Code\n participant Schema\n participant TestSuite\n \n Auditor->>Code: Review implementation\n Code->>Schema: Check data model\n Schema-->>Auditor: Findings\n Auditor->>TestSuite: Run conformance tests\n TestSuite-->>Auditor: Pass/Fail results\n Auditor->>AUDIT_REPORT.md: Document findings\n```\n\n### Conformance Test Execution\n\n1. **Self-check**: `python3 testpipe/isolation/selfcheck.py`\n2. **Ordering model**: `bash scripts/isolation-proof.sh`\n3. **Live lane**: `bash scripts/isolation-proof.sh --live` (requires stack lock)\n\n### Integration with Codebase\n\nThe security documentation references specific code locations:\n\n```\ndocs/security/\n├── AUDIT_REPORT.md # Main audit findings\n├── ab-isolation-conformance.md # Tenant isolation tests\n└── codex_security_audit.md # Threat model\n```\n\nThese documents reference:\n- `HarakatOffices_Server/src/` - Backend implementation\n- `Harakat_AI/` - AI service implementation\n- `prisma/schema.prisma` - Data model\n- `testpipe/isolation/` - Conformance test suite\n\n## Usage Guidelines\n\n### For Developers\n\n1. **Before implementing new features**: Review `codex_security_audit.md` threat model\n2. **When fixing vulnerabilities**: Reference `AUDIT_REPORT.md` Top 10 prioritized fixes\n3. **When adding new routes**: Ensure `attachAccessClaims` middleware is mounted\n4. **When handling secrets**: Use GCP Secret Manager, not `.env` files\n\n### For Security Engineers\n\n1. **Run conformance tests**: `python3 testpipe/isolation/selfcheck.py`\n2. **Review failed rows**: Check `ab-isolation-conformance.md` for context\n3. **Track remediation**: Use `AUDIT_REPORT.md` Top 10 list as sprint backlog\n\n### For Product Managers\n\n1. **Understand risk**: Critical/High issues affect production security\n2. **Prioritize fixes**: Top 10 list indicates effort vs. risk reduction\n3. **Compliance**: GDPR data export endpoint is a Medium priority requirement\n\n## Connection to Other Modules\n\n### Security ↔ Authentication\n\n```mermaid\ngraph LR\n A[Security Docs] --> B[StartupAuth Middleware]\n A --> C[JWT Verification]\n A --> D[2FA Implementation]\n \n B --> E[verifyStartupUser]\n C --> F[attachAccessClaims]\n D --> G[TOTP Enrollment]\n```\n\n### Security ↔ AI Services\n\n```mermaid\ngraph LR\n A[Security Docs] --> B[AI Service]\n A --> C[Tool Allowlist]\n A --> D[Prompt Injection]\n \n B --> E[Startup ID Verification]\n C --> F[_send_email]\n D --> G[User Data Delimiters]\n```\n\n### Security ↔ Data Layer\n\n```mermaid\ngraph LR\n A[Security Docs] --> B[Prisma Schema]\n A --> C[Encryption]\n A --> D[Secrets Management]\n \n B --> E[StartupUser]\n C --> F[KMS Envelope]\n D --> G[GCP Secret Manager]\n```\n\n## Top 10 Prioritized Fixes\n\nFrom `AUDIT_REPORT.md`, the highest priority remediations:\n\n1. **KMS-envelope OAuth tokens, Zoho password, refresh tokens**\n2. **Fix tier polarity** (inverted RBAC)\n3. **TOTP 2FA on StartupUser signup**\n4. **AI service: assert startup_id against JWT before any tool call**\n5. **Outbound email recipient allowlist + side-effect tool approvals**\n6. **Bulk knowledge-graph upsert + transactional sync**\n7. **Hash 2FA codes; add CSRF nonce on Drive OAuth callback**\n8. **Move service-account JSON out of submodule; secrets via GCP Secret Manager**\n9. **Soft-delete knowledge nodes; reconcile on sync; content-hash dedup**\n10. **Per-user rate limits on side-effect tools; OCR budget cap per tenant**\n\n## Maintenance\n\n### Update Frequency\n\n- **AUDIT_REPORT.md**: Quarterly or after major releases\n- **ab-isolation-conformance.md**: After each tenant isolation change\n- **codex_security_audit.md**: When threat model assumptions change\n\n### Evidence Storage\n\nRaw test output is stored in:\n- `.sdd-work/vendor-extraction/T7.5-evidence/isolation-proof-live-run.txt`\n\n### Version Tracking\n\nEach document includes:\n- Date of audit\n- Server commit hash\n- Database version\n- Tenant IDs used in testing\n\n---\n\n**Note**: This documentation module is read-only. Security fixes are implemented in the referenced code locations, not in this module. The module serves as the audit trail and specification for security engineering work.","docs-specs":"# docs — specs\n\n\n\n# docs/specs — Specification Repository\n\n## Overview\n\nThe `docs/specs` module serves as the central specification repository for the HarakatOffices and StartupsPlatform codebases. It contains **implementation plans**, **completion logs**, and **technical specifications** that guide development work across both repositories.\n\nThis is not a runtime module — it's a **documentation-first** system where specifications drive implementation through a subagent-driven development workflow.\n\n## Purpose\n\nThe module provides:\n\n1. **Task tracking** — Each specification file documents a discrete development task with:\n - Clear goals and constraints\n - File-by-file implementation steps\n - Verification gates (tsc, selftests, live tests)\n - Commit instructions\n\n2. **Cross-repo coordination** — Specifications reference both `HarakatOffices_Server` and `StartupsPlatform` repositories, ensuring consistency across the codebase.\n\n3. **Audit trail** — Completion logs document what was built, what was reviewed, and what defects were caught during the review loop.\n\n## Architecture\n\n```mermaid\ngraph TD\n A[docs/specs] --> B[plans/*]\n A --> C[specs/*]\n A --> D[logs/*]\n B --> E[Implementation Tasks]\n C --> F[Technical Specs]\n D --> G[Completion Logs]\n E --> H[Subagent Execution]\n H --> I[Code Commits]\n I --> J[Working Tree Review]\n J --> K[Final Merge]\n```\n\n### File Organization\n\n| Directory | Purpose | Example Files |\n|-----------|---------|---------------|\n| `plans/` | Implementation plans with task-by-task steps | `2026-06-17-batch-embeddings.md` |\n| `specs/` | Technical design specifications | `2026-06-29-procedures-controlled-flow-parallel-design.md` |\n| `logs/` | Completion logs and review findings | `2026-06-29-procedures-controlled-flow-parallel-COMPLETE.md` |\n\n## Key Components\n\n### 1. Implementation Plans (`plans/*.md`)\n\nEach plan follows a standardized structure:\n\n```markdown\n# Plan Title\n\n**Goal:** [One-sentence objective]\n\n**Architecture:** [High-level design]\n\n**Tech Stack:** [Relevant repos and frameworks]\n\n## Global Constraints\n- [Constraint 1]\n- [Constraint 2]\n\n## Task 1: [Name]\n**Files:** [List of files to create/modify]\n**Interfaces:** [What it consumes/produces]\n\n- [ ] Step 1: Write failing tests\n- [ ] Step 2: Run to verify it fails\n- [ ] Step 3: Implement\n- [ ] Step 4: Run tests to verify pass\n- [ ] Step 5: Commit\n```\n\n### Example: Batch Embeddings Plan\n\nFrom `2026-06-17-batch-embeddings.md`:\n\n```python\n# Task 2: AI core/batch_embed.py\nBATCH_MODEL = \"gemini-embedding-2\"\nBATCH_DIM = 768\n_TERMINAL = {\"JOB_STATE_SUCCEEDED\", \"JOB_STATE_FAILED\", ...}\n\ndef extract_vectors(dest_responses: list) -> list[list[float] | None]:\n \"\"\"Pure: ordered vectors from job.dest.inlined_embed_content_responses.\"\"\"\n out: list[list[float] | None] = []\n for r in dest_responses or []:\n if getattr(r, \"error\", None) or not getattr(r, \"response\", None):\n out.append(None); continue\n emb = getattr(r.response, \"embedding\", None)\n vals = getattr(emb, \"values\", None) if emb else None\n out.append(list(vals) if vals else None)\n return out\n```\n\n### 2. Completion Logs (`logs/*.md`)\n\nCompletion logs document finished work with:\n\n- **Commit ranges** — Git commit hashes for each repository\n- **Verification results** — Test counts, build status, live render checks\n- **Defects caught** — Critical bugs found during review\n- **Root cause analysis** — Patterns and lessons learned\n\nFrom `2026-06-29-procedures-controlled-flow-parallel-COMPLETE.md`:\n\n```markdown\n## Final commit tips\n| Repo | Base | Head | Commits |\n|------|------|------|---------|\n| HarakatOffices_Server | `9e56352` | `78c7301` | 8 |\n| StartupsPlatform | `b6de51c` | `6de81e4` | 14 |\n\n## Defects caught by the review loop (all passed tests; caught by review)\n1. **B5 Critical:** `insertConcurrent` produced a cyclic fork → Fixed (post-parallel continuation).\n2. **B6a Critical:** `applyNodePatch` round-tripped through `stepsToFlow`/`flowToSteps` → Fixed (re-graft structural routing).\n```\n\n### 3. Technical Specifications (`specs/*.md`)\n\nDesign specifications define the technical contract for features:\n\n```markdown\n## Fork/join model invariant (the spine — keep it)\nA fork's `onSuccess` is the **post-parallel** continuation step, never a lane entry. \nLanes are linear chains ending at `onSuccess:null` (auto-join) or pointing at a `join` step.\n```\n\n## Execution Flow\n\n### Subagent-Driven Development\n\nThe module enables a **subagent-driven development** workflow:\n\n```mermaid\nsequenceDiagram\n participant Dev as Developer\n participant Spec as docs/specs\n participant Subagent as Subagent\n participant Repo as Git Repo\n \n Dev->>Spec: Read plan\n Spec->>Subagent: Task specification\n Subagent->>Subagent: Implement task\n Subagent->>Repo: Create commit\n Repo->>Dev: Working tree update\n Dev->>Dev: Review commit\n Dev->>Spec: Update completion log\n```\n\n### Task Lifecycle\n\n1. **Plan creation** — Developer writes implementation plan in `plans/`\n2. **Subagent execution** — Subagent implements task-by-task following the plan\n3. **Review** — Developer reviews each commit\n4. **Completion log** — Developer writes completion log in `logs/`\n5. **Merge** — Working tree merged to main (per repo convention)\n\n## Integration with Codebase\n\n### Repository References\n\nSpecifications reference actual code patterns and function names:\n\n| Specification | Code Reference |\n|---------------|----------------|\n| `batch-embeddings.md` | `Harakat_AI/core/batch_embed.py`, `HarakatOffices_Server/src/modules/knowledge/doc-chunking.ts` |\n| `enrichment-batch-state-machine.md` | `Harakat_AI/core/batch_ocr.py`, `HarakatOffices_Server/src/modules/parsing/enrich-batch.service.ts` |\n| `procedures-controlled-flow-parallel.md` | `StartupsPlatform/components/procedures/ProcedureFlowEditor.tsx`, `HarakatOffices_Server/src/modules/procedures/procedure-runs.service.ts` |\n\n### Environment Variables\n\nSpecifications document environment gating:\n\n```markdown\n## Gating\n- `EMBED_BATCH_ENABLED` default OFF → today's online ingest path byte-for-byte unchanged\n- `ENRICH_BATCH_ENABLED` (server) — Unset/`!= \"true\"` ⇒ the live inline path runs byte-for-byte unchanged\n```\n\n### Database Migrations\n\nSpecifications document database changes:\n\n```prisma\nmodel EmbedBatchItem {\n id Int @id @default(autoincrement())\n startupId Int\n nodeId Int\n contentHash String?\n status String @default(\"pending\") // pending | submitted | done | failed\n batchJobName String?\n chunkCount Int?\n error String?\n createdAt DateTime @default(now())\n submittedAt DateTime?\n completedAt DateTime?\n\n @@unique([startupId, nodeId])\n}\n```\n\n## Verification Gates\n\nEach specification defines verification requirements:\n\n### Server Verification\n\n```markdown\n## Verification (gates)\n- Server `procedures:selftest` **47/47 LIVE** on startup-19 (`:5433`)\n- StartupsPlatform: **271 procedure tests green**, `tsc --noEmit` 0 errors\n- **Live render @ 1920×1080** (real browser, logged in)\n```\n\n### Type Safety\n\n```markdown\n- `npx tsc --noEmit` is the real FE typecheck gate, not `pnpm build`/`pnpm test`\n- `python -m py_compile` for AI modules\n```\n\n### Selftest Scripts\n\nSpecifications include selftest scripts for verification:\n\n```typescript\n// src/scripts/chunking-selftest.ts\nimport { chunkPages, computeContentHash } from \"../modules/knowledge/doc-chunking\";\n\nlet ok = 0, fail = 0;\nconst t = (name: string, cond: boolean) => { cond ? ok++ : (fail++, console.error(\"FAIL\", name)); };\n\nconst chunks = chunkPages([\"a\".repeat(6000), \"b\".repeat(100)], \"Title\");\nt(\"chunk0 has title prefix\", chunks[0].startsWith(\"Title\\n\\n\"));\nt(\"splits by CHUNK_CHARS\", chunks.length >= 2);\n```\n\n## Contributing\n\n### Adding a New Specification\n\n1. **Create the plan** in `docs/specs/plans/YYYY-MM-DD-title.md`\n2. **Define the goal** and **architecture** at the top\n3. **List global constraints** (tenets, limits, gating)\n4. **Break into tasks** with file-by-file steps\n5. **Include verification gates** (tests, tsc, selftests)\n6. **Document commit instructions** (per repo convention)\n\n### Task Step Format\n\n```markdown\n- [ ] **Step 1: Write failing tests**\n - Expected: FAIL (ModuleNotFoundError)\n- [ ] **Step 2: Run to verify it fails**\n - Command: `cd Harakat_AI && python -m pytest tests/test_batch_embed.py -v`\n- [ ] **Step 3: Implement**\n - Code block with implementation\n- [ ] **Step 4: Run tests to verify pass**\n - Expected: PASS (2 tests)\n- [ ] **Step 5: Commit**\n - `git add ... && git commit -m \"feat: description\"`\n```\n\n### Writing Completion Logs\n\nAfter implementation:\n\n1. **Record commit ranges** for each repository\n2. **Document verification results** (test counts, build status)\n3. **List defects caught** during review\n4. **Note root causes** and lessons learned\n5. **Mark as COMPLETE** with date and status\n\n## Common Patterns\n\n### Gated Features\n\n```markdown\n## Gating flag: `ENRICH_BATCH_ENABLED` (server)\nUnset/`!= \"true\"` ⇒ the live inline path runs byte-for-byte unchanged.\n```\n\n### Additive Changes\n\n```markdown\n## Tenet #5 (never lose the startup-19 sync):\nevery change is additive + gated default-OFF.\nThe Prisma table is applied **dump-first** (`pg_dump` → `prisma db execute` → `migrate resolve --applied`),\n**never** `migrate dev`.\n```\n\n### State Machine Patterns\n\n```typescript\n// Common state machine pattern\nexport async function pollAndIngestEnrichBatches(): Promise<void> {\n if (!enrichBatchEnabled()) return;\n const jobs = await prisma.enrichBatchItem.findMany({\n where: { status: \"submitted\", batchJobName: { not: null } },\n distinct: [\"batchJobName\"],\n select: { batchJobName: true, batchDest: true, kind: true, startupId: true },\n });\n for (const j of jobs) {\n // poll → ingest → mark done\n }\n}\n```\n\n## Related Modules\n\n| Module | Relationship |\n|--------|--------------|\n| `docs/specs/plans/` | Contains implementation plans |\n| `docs/specs/specs/` | Contains technical specifications |\n| `docs/specs/logs/` | Contains completion logs |\n| `HarakatOffices_Server/` | Server-side implementation |\n| `StartupsPlatform/` | Frontend implementation |\n| `Harakat_AI/` | AI service implementation |\n\n## Maintenance\n\n### Keeping Specifications Current\n\n1. **Update completion logs** after each feature completion\n2. **Archive old plans** to `docs/specs/archive/` when merged\n3. **Review gating flags** — ensure environment variables are documented\n4. **Verify cross-repo consistency** — specs should reference actual code patterns\n\n### Cleanup\n\n```bash\n# Remove superseded scripts after migration\ngit -C HarakatOffices_Server rm src/scripts/batch-descriptions.ts\ngit -C HarakatOffices_Server rm src/scripts/batch-tree-summaries.ts\n```\n\n## Notes\n\n- **Working tree convention** — Per repo convention, specs are kept in working tree, NOT merged/pushed until explicitly requested\n- **Subagent-driven** — All implementers + reviewers + fixers operate on opus; one fresh subagent per task\n- **No pytest/jest** — Verification uses selftest scripts in the style of `vector:selftest` / `kms:selftest` / `ocr _flush_ocr.ts`\n- **Dump-first migrations** — Database changes require `pg_dump` → `prisma db execute` → `migrate resolve --applied`","docs":"# docs\n\n\n\n# docs\n\nThe `docs` module serves as the central knowledge repository for the Harakat platform, bridging infrastructure planning, operational execution, and developer resources. It defines the lifecycle of the `HarakatOffices_Server` environment, from hardware capacity estimation to security conformance and deployment procedures.\n\n## Infrastructure & Operations\n\nThe operational backbone of the platform is defined in the **Deployment** and **Capacity** modules.\n* **Capacity Planning**: The [Capacity](capacity.md) module establishes the single source of truth for hardware procurement, analyzing token economics to determine the machine configuration required for target user counts.\n* **Deployment Procedures**: The [Deployment](deployment.md) module tracks manual intervention points and schema evolution for the `HarakatOffices_Server` container. While Docker handles code updates, this module documents the security migrations and data integrity checks required on the deployed VM.\n* **Security**: The [Security](security.md) module acts as the audit trail for the platform, documenting vulnerability findings and compliance obligations for `HarakatOffices_Server` and `Harakat_AI`.\n\n## Data & Intelligence\n\nData handling and intelligence capabilities are governed by the **Personas**, **Retrieval**, and **AutoML** modules.\n* **Personas**: The [Personas](personas.md) module replaces legacy registries, serving as the single source of truth for test tenants. Every test persona is defined in a dedicated `persona.md` file, ensuring consistency across pipelines.\n* **Retrieval**: The [Retrieval](retrieval.md) module implements a vectorless, hierarchical pipeline using Meilisearch and AI-generated summaries. It supports optional hybrid vector capabilities for multilingual or scanned-image retrieval.\n* **AutoML**: The [AutoML](automl.md) module defines the design for Self-Building Predictive Capabilities, allowing the assistant to identify learnable patterns in tenant data without exposing ML terminology to users.\n\n## Verification & Reference\n\nQuality assurance and developer guidance are centralized in the **Formal** and **Reference** modules.\n* **Formal Verification**: The [Formal](formal.md) module provides a bounded executable model for verifying tenant-resolution ordering. It implements a state machine that enumerates interleavings of tenant resolution steps against concurrent world events to assert seven security invariants.\n* **Reference Library**: The [Reference](reference.md) module contains implementation examples (e.g., Google LiveAPI integration) and procedure specifications (e.g., Material Request Form workflows) for extending platform capabilities.\n\n## System Workflow\n\nThe following diagram illustrates how these sub-modules interact to support the platform lifecycle:\n\n```mermaid\nflowchart TD\n subgraph Planning\n Cap[Capacity Planning]\n Auto[AutoML Design]\n end\n\n subgraph Execution\n Dep[Deployment Procedures]\n Sec[Security Audit]\n end\n\n subgraph Data & Logic\n Per[Personas]\n Ret[Retrieval Pipeline]\n For[Formal Verification]\n end\n\n subgraph Reference\n Ref[Reference Examples]\n end\n\n Cap -->|Hardware Config| Dep\n Auto -->|Feature Logic| Ret\n Per -->|Test Data| Ret\n For -->|Security Invariants| Dep\n Dep -->|Operational State| Sec\n Ref -->|Integration Patterns| Auto\n```\n\n## Integration Points\n\n* **Security & Deployment**: The [Formal](formal.md) module's state machine verifies tenant resolution logic, feeding security invariants into the [Deployment](deployment.md) manual intervention checklist.\n* **Data & Retrieval**: [Personas](personas.md) provide the test data required to validate the [Retrieval](retrieval.md) pipeline, while [AutoML](automl.md) designs define the predictive patterns the retrieval system may eventually support.\n* **Reference & Development**: The [Reference](reference.md) module provides the code samples (e.g., `google-liveapi-example.py`) that developers use to implement the capabilities defined in [AutoML](automl.md) and [Retrieval](retrieval.md).","erp-harakat-backend-custom-addons":"# erp-harakat-backend — custom_addons\n\n\n\n# Custom Addons Module Documentation\n\n## Overview\n\nThe `custom_addons` directory contains a collection of Odoo 16+ modules that extend the core ERP functionality with enterprise-grade features. These modules are organized into five distinct applications:\n\n- **account_advanced** - Advanced financial reporting and dunning\n- **account_assets** - Fixed asset management with depreciation\n- **crm_advanced** - CRM enhancements with competitor tracking\n- **erp_bridge** - REST API bridge for frontend integration\n- **erp_gateway** - Per-employee access control and governance\n\n```mermaid\ngraph TB\n subgraph \"Custom Addons\"\n AA[account_advanced]\n AS[account_assets]\n CRM[crm_advanced]\n EB[erp_bridge]\n EG[erp_gateway]\n end\n \n subgraph \"Core Odoo\"\n ACC[account]\n CRM_CORE[crm]\n BASE[base]\n end\n \n AA --> ACC\n AS --> ACC\n CRM --> CRM_CORE\n EB --> BASE\n EG --> BASE\n \n subgraph \"Shared Dependencies\"\n MAIL[mail]\n WEB[web]\n end\n \n EB -.-> MAIL\n EG -.-> MAIL\n```\n\n---\n\n## 1. Account Advanced Module\n\n### Purpose\n\nExtends Odoo's Community Edition accounting with Enterprise-grade features including financial reports, dunning management, budget tracking, and cost center accounting.\n\n### Key Components\n\n#### Models\n\n**`account.analytic.cost.center`** - Cost Center Management\n\n```python\nclass AccountAnalyticCostCenter(models.Model):\n _name = 'account.analytic.cost.center'\n _description = 'Cost Center'\n```\n\n- **Computed Fields**: `actual_amount`, `remaining_amount`, `utilization_percentage`\n- **Key Method**: `_compute_actual_amount()` - Sums `account.move.line` balances filtered by `analytic_account_id.cost_center_id`\n\n**`account.budget`** - Budget Management\n\n```python\nclass AccountBudget(models.Model):\n _name = 'account.budget'\n _description = 'Budget'\n```\n\n- **Computed Fields**: `total_planned`, `total_actual`, `total_variance`, `variance_percentage`\n- **Key Method**: `_compute_totals()` - Aggregates budget line amounts\n- **State Workflow**: `draft` → `confirmed` → `done`\n\n**`account.budget.line`** - Budget Line Items\n\n```python\nclass AccountBudgetLine(models.Model):\n _name = 'account.budget.line'\n _description = 'Budget Line'\n```\n\n- **Key Method**: `compute_actual_amount()` - Queries `account.move.line` within budget period\n- **Account Type Handling**: Income/expense accounts use negative balance, assets/liabilities use positive\n\n**`account.dunning`** - Dunning Letters\n\n```python\nclass AccountDunning(models.Model):\n _name = 'account.dunning'\n _description = 'Dunning Letter'\n```\n\n- **Computed Fields**: `total_overdue`, `total_charge`, `total_due`\n- **Key Method**: `action_send()` - Marks dunning as sent (email logic TODO)\n\n**`account.dunning.line`** - Dunning Line Items\n\n```python\nclass AccountDunningLine(models.Model):\n _name = 'account.dunning.line'\n _description = 'Dunning Line'\n```\n\n- **Key Method**: `_onchange_move_id()` - Auto-computes charge based on dunning level\n- **Key Method**: `_find_appropriate_level()` - Matches days overdue to template level\n- **Charge Calculation**: `(amount_residual * level.charge / 100) + level.charge_fixed`\n\n**`account.financial.report`** - Financial Report Configuration\n\n```python\nclass AccountFinancialReport(models.Model):\n _name = 'account.financial.report'\n _description = 'Financial Report'\n```\n\n- **Report Types**: `profit_loss`, `balance_sheet`, `cash_flow`, `general_ledger`, `trial_balance`, `aging_receivable`, `aging_payable`\n- **Key Method**: `get_report_data(date_from, date_to, target_move)` - Generates report data\n- **Key Method**: `action_generate_report()` - Opens report wizard\n\n**`account.aging.report`** - Aging Report Generator\n\n```python\nclass AccountAgingReport(models.TransientModel):\n _name = 'account.aging.report'\n _description = 'Aging Report'\n```\n\n- **Key Method**: `action_generate()` - Groups invoices by partner and aging period\n- **Period Configuration**: `period_1_days`, `period_2_days`, `period_3_days`\n\n**`account.move.line` (Inherited)** - Reconciliation Helpers\n\n```python\nclass AccountMoveLine(models.Model):\n _inherit = 'account.move.line'\n```\n\n- **Key Method**: `reconcile_line_ids(ids)` - Reconcile selected move lines\n- **Key Method**: `unreconcile_line_ids(ids)` - Undo reconciliation\n\n#### Wizards\n\n**`account.financial.report.wizard`** - Report Generation Wizard\n\n```python\nclass AccountFinancialReportWizard(models.TransientModel):\n _name = 'account.financial.report.wizard'\n _description = 'Financial Report Wizard'\n```\n\n- **Fields**: `report_id`, `date_from`, `date_to`, `target_move`\n- **Key Method**: `_compute_report_data()` - Calls `report_id.get_report_data()`\n- **Key Method**: `action_export_excel()` - Placeholder for Excel export\n\n**`account.dunning.wizard`** - Dunning Generation Wizard\n\n```python\nclass AccountDunningWizard(models.TransientModel):\n _name = 'account.dunning.wizard'\n _description = 'Dunning Wizard'\n```\n\n- **Fields**: `template_id`, `partner_ids`, `min_amount`, `date`\n- **Key Method**: `action_generate_dunning()` - Creates dunning letters for overdue invoices\n- **Key Method**: `_get_dunning_level()` - Determines appropriate level based on invoice history\n\n### Data Configuration\n\n**`account_advanced_data.xml`** - Default Report Structure\n\n```xml\n<!-- Profit & Loss Report Structure -->\n<record id=\"report_profit_loss\" model=\"account.financial.report\">\n <field name=\"name\">Profit & Loss<\/field>\n <field name=\"code\">PL<\/field>\n <field name=\"report_type\">profit_loss<\/field>\n<\/record>\n\n<!-- Formula-based totals -->\n<record id=\"report_pl_gross_profit\" model=\"account.financial.report.line\">\n <field name=\"formula\">REV - COGS<\/field>\n <field name=\"line_type\">total<\/field>\n<\/record>\n```\n\n**Dunning Levels Configuration**:\n\n| Level | Delay | Charge % | Fixed Charge |\n|-------|-------|----------|--------------|\n| First Reminder | 7 days | 0% | $0 |\n| Second Reminder | 14 days | 1.5% | $10 |\n| Final Notice | 30 days | 3% | $25 |\n\n---\n\n## 2. Account Assets Module\n\n### Purpose\n\nProvides fixed asset management with straight-line depreciation schedules and automatic journal entry posting.\n\n### Key Components\n\n**`account.asset`** - Fixed Asset Model\n\n```python\nclass AccountAsset(models.Model):\n _name = 'account.asset'\n _description = 'Fixed Asset'\n```\n\n- **Fields**: `original_value`, `salvage_value`, `method_number`, `method_period`\n- **Computed Fields**: `depreciated_amount`, `book_value`\n- **Key Method**: `action_compute_depreciation()` - Generates depreciation schedule\n- **Key Method**: `action_validate()` - Sets state to 'open'\n- **Key Method**: `action_close()` - Closes asset\n\n**`account.asset.depreciation.line`** - Depreciation Schedule Lines\n\n```python\nclass AccountAssetDepreciationLine(models.Model):\n _name = 'account.asset.depreciation.line'\n _description = 'Asset Depreciation Line'\n```\n\n- **Key Method**: `action_post()` - Creates and posts journal entry\n- **Journal Entry Structure**:\n - Debit: `expense_account_id`\n - Credit: `depreciation_account_id`\n\n### Depreciation Calculation\n\n```python\ndef action_compute_depreciation(self):\n depreciable = asset.original_value - asset.salvage_value\n per = round(depreciable / asset.method_number, 2)\n \n for i in range(1, asset.method_number + 1):\n amount = per if i < asset.method_number else round(depreciable - cumulative, 2)\n cumulative += amount\n lines.append({\n 'date': asset.acquisition_date + relativedelta(months=asset.method_period * i),\n 'amount': amount,\n 'depreciated_value': cumulative,\n 'remaining_value': round(depreciable - cumulative, 2),\n })\n```\n\n---\n\n## 3. CRM Advanced Module\n\n### Purpose\n\nExtends Odoo CRM with competitor tracking, lead phase management, and priority expansion.\n\n### Key Components\n\n**`crm.competitor`** - Competitor Master Data\n\n```python\nclass CrmCompetitor(models.Model):\n _name = 'crm.competitor'\n _description = 'Competitor'\n```\n\n- **Fields**: `name`, `website`, `note`, `company_id`\n- **Tenant Scoping**: `company_id = False` indicates global template row\n\n**`crm.lead` (Inherited)** - Lead Enhancements\n\n```python\nclass CrmLead(models.Model):\n _inherit = 'crm.lead'\n```\n\n- **New Field**: `harakat_phase` - Selection: `lead` | `prospects`\n - Distinguishes pre-pipeline phases\n - Ignored when `type = opportunity`\n- **New Field**: `harakat_discovery` - Prospect discovery notes\n- **Extended Priority**: `0..5` (added `4: Critical`, `5: Top`)\n\n### Phase Workflow\n\n```\nLead (type='lead', harakat_phase='lead')\n ↓\nProspect (type='lead', harakat_phase='prospect')\n ↓\nOpportunity (type='opportunity')\n```\n\n---\n\n## 4. ERP Bridge Module\n\n### Purpose\n\nProvides REST API endpoints for frontend integration with JWT authentication and CRUD operations.\n\n### Key Components\n\n**Controllers**\n\n**`AuthController`** - Authentication Endpoints\n\n```python\n@http.route('/api/v1/auth/login', auth='none', type='http', methods=['POST'], csrf=False)\ndef login(self, **kwargs):\n data = json.loads(request.httprequest.data)\n uid = request.session.authenticate(request.db, data.get('login'), data.get('password'))\n if uid:\n return json_response({\n 'success': True,\n 'user': {\n 'id': user.id,\n 'name': user.name,\n 'email': user.email,\n 'lang': user.lang,\n },\n 'session_id': request.session.sid,\n })\n```\n\n**`CrmController`** - CRM API Endpoints\n\n```python\n@http.route('/api/v1/crm/leads', auth='user', type='http', methods=['GET'], csrf=False)\ndef get_leads(self, **kwargs):\n domain = []\n stage_id = kwargs.get('stage_id')\n if stage_id:\n domain.append(('stage_id', '=', int(stage_id)))\n \n data, error = self._get_model_data(\n 'crm.lead',\n domain=domain,\n fields=['name', 'partner_id', 'email_from', 'phone', 'stage_id', 'probability', 'expected_revenue', 'date_deadline', 'user_id', 'create_date'],\n offset=int(kwargs.get('offset', 0)),\n limit=int(kwargs.get('limit', 80)),\n )\n return json_response(data)\n```\n\n**`ErpApiController`** - Base API Controller\n\n```python\ndef _get_model_data(self, model_name, record_id=None, domain=None, fields=None, offset=0, limit=80, order=None):\n try:\n model = request.env[model_name]\n except KeyError:\n return None, f\"Model {model_name} not found\"\n \n if record_id:\n record = model.browse(record_id)\n if not record.exists():\n return None, f\"Record {record_id} not found\"\n return record.read(fields)[0] if fields else record.read()[0], None\n \n domain = domain or []\n records = model.search(domain, offset=offset, limit=limit, order=order)\n total = model.search_count(domain)\n \n data = records.read(fields) if fields else records.read()\n \n return {\n 'data': data,\n 'total': total,\n 'offset': offset,\n 'limit': limit,\n }, None\n```\n\n**`ApiLog`** - API Request Logging\n\n```python\nclass ApiLog(models.Model):\n _name = 'erp.api.log'\n _description = 'API Request Log'\n```\n\n- **Key Method**: `action_clear_logs()` - Removes logs older than 30 days\n\n### Authentication Flow\n\n```mermaid\nsequenceDiagram\n participant Client\n participant API\n participant Odoo\n \n Client->>API: POST /api/v1/auth/login\n API->>Odoo: authenticate(login, password)\n Odoo-->>API: uid (success/failure)\n alt Success\n API->>Client: JSON with user data + session_id\n else Failure\n API->>Client: 401 Invalid credentials\n end\n```\n\n---\n\n## 5. ERP Gateway Module\n\n### Purpose\n\nImplements per-employee access control with co-ownership, governance record rules, and tenant isolation.\n\n### Key Components\n\n**`post_init_generate_security()`** - Security Generator Hook\n\n```python\ndef post_init_generate_security(env):\n governed = env.ref(\"erp_gateway.group_harakat_governed\")\n \n for app, spec in ERP_CATALOG.items():\n owned = [m for m in spec.get(\"owned\", []) if m in env]\n reference = [m for m in spec.get(\"reference\", []) if m in env]\n \n # Create app groups\n user_group, mgr_group, created = _ensure_app_groups(env, app, governed)\n \n # Grant ACLs\n for model in owned:\n _upsert_acl(env, model, user_group, app, read=True, write=True, create=True, unlink=False)\n _upsert_acl(env, model, mgr_group, app, read=True, write=True, create=True, unlink=True)\n \n # Grant record rules\n for model in owned:\n if model not in _SHARED_READ_EXCEPTION:\n _upsert_owned_rules(env, model, governed)\n```\n\n**Security Domains**\n\n```python\n# Owner read: owner OR governor-of-any-owner\n_OWNED_READ_DOMAIN = (\n \"['|', ('harakat_owner_ids', 'in', [user.id]), \"\n \"('harakat_owner_ids', 'in', user.harakat_subordinate_ids.ids)]\"\n)\n\n# Owner write: owner only\n_OWNED_WRITE_DOMAIN = \"[('harakat_owner_ids', 'in', [user.id])]\"\n\n# Line (parent-scoped) read\n_LINE_READ_DOMAIN = (\n \"['|', ('{p}.harakat_owner_ids', 'in', [user.id]), \"\n \"('{p}.harakat_owner_ids', 'in', user.harakat_subordinate_ids.ids)]\"\n)\n```\n\n**Tenant Isolation**\n\n```python\ndef _company_boundary_domain(env, model, extra_parents=None):\n \"\"\"Return a GLOBAL company-boundary domain for model\"\"\"\n if model not in env:\n return None\n fields = env[model]._fields\n if \"company_id\" in fields:\n return \"[('company_id', 'in', company_ids)]\"\n for parent in (extra_parents or ()):\n f = fields.get(parent)\n comodel = getattr(f, \"comodel_name\", None) if f else None\n if comodel and comodel in env and \"company_id\" in env[comodel]._fields:\n return \"[('%s.company_id', 'in', company_ids)]\" % parent\n return None\n```\n\n**Service User Company Management**\n\n```python\ndef _ensure_service_user_companies(env):\n \"\"\"Keep the gateway service account's allowed companies COMPLETE\"\"\"\n admin = env.ref(\"base.user_admin\", raise_if_not_found=False)\n if not admin:\n return 0\n all_companies = env[\"res.company\"].sudo().search([])\n missing = all_companies - admin.company_ids\n if missing:\n admin.sudo().write({\"company_ids\": [(4, c.id) for c in missing]})\n return len(missing)\n```\n\n### Model Categories\n\n| Category | Description | ACL | Record Rules |\n|----------|-------------|-----|--------------|\n| `owned` | App-owned models | App group RWCU | Owner-only |\n| `reference` | Read-only references | Governed READ | None |\n| `shared_lines` | Writable children | App group RWCU | Parent-scoped |\n| `readonly_lines` | Human read only | Governed READ | None |\n| `audit` | Audit surfaces | Governed READ | None |\n| `sensitive` | Privacy models | Self-read only | Self-domain |\n| `company_shared` | Company work | App group RWCU | Company-boundary |\n\n### Group Hierarchy\n\n```\ngroup_harakat_governed (base)\n ├── group_harakat_owner (full company reach)\n └── group_harakat_<app> (per-app access)\n ├── user_group (RW, no unlink)\n └── manager_group (RW + unlink)\n```\n\n---\n\n## Integration Points\n\n### Module Dependencies\n\n```\naccount_advanced\n├── base\n└── account\n\naccount_assets\n└── account\n\ncrm_advanced\n└── crm\n\nerp_bridge\n├── base\n├── web\n└── rest_framework\n\nerp_gateway\n├── base\n├── mail\n├── crm\n├── sale_management\n├── purchase\n├── account\n├── stock\n├── project\n├── subscription\n└── account_advanced\n```\n\n### Data Flow: Dunning Generation\n\n```mermaid\nsequenceDiagram\n participant User\n participant Wizard\n participant Template\n participant Dunning\n participant Invoice\n\n User->>Wizard: action_generate_dunning()\n Wizard->>Template: Get template & levels\n Template-->>Wizard: level_ids\n Wizard->>Invoice: Search overdue invoices\n Invoice-->>Wizard: partner_invoices\n loop For each partner\n Wizard->>Invoice: _get_dunning_level()\n Invoice-->>Wizard: level_id\n Wizard->>Dunning: Create dunning letter\n Dunning->>Dunning: Add invoice lines\n end\n Wizard-->>User: Return dunning letters\n```\n\n### Data Flow: Financial Report Generation\n\n```mermaid\nsequenceDiagram\n participant User\n participant ReportWizard\n participant FinancialReport\n participant MoveLine\n\n User->>ReportWizard: action_generate_report()\n ReportWizard->>FinancialReport: get_report_data(date_from, date_to)\n FinancialReport->>MoveLine: Search account.move.line\n MoveLine-->>FinancialReport: debit, credit, balance\n FinancialReport->>ReportWizard: Return report_data\n ReportWizard-->>User: Display report\n```\n\n---\n\n## Security Model\n\n### Access Control Matrix\n\n| Model | Group | Read | Write | Create | Unlink |\n|-------|-------|------|-------|--------|--------|\n| `account.budget` | `account.group_account_user` | ✓ | ✓ | ✓ | ✓ |\n| `account.budget` | `account.group_account_manager` | ✓ | ✓ | ✓ | ✓ |\n| `account.dunning` | `account.group_account_user` | ✓ | ✓ | ✓ | ✓ |\n| `account.dunning` | `account.group_account_manager` | ✓ | ✓ | ✓ | ✓ |\n| `account.asset` | `account.group_account_user` | ✓ | ✓ | ✓ | ✓ |\n| `account.asset` | `account.group_account_manager` | ✓ | ✓ | ✓ | ✓ |\n| `crm.lead` | `crm.group_crm_user` | ✓ | ✓ | ✓ | ✓ |\n| `crm.lead` | `crm.group_crm_manager` | ✓ | ✓ | ✓ | ✓ |\n\n### Record Rules\n\n**Owned Models**:\n- **Read**: `('harakat_owner_ids', 'in', [user.id]) OR ('harakat_owner_ids', 'in', user.harakat_subordinate_ids.ids)`\n- **Write**: `('harakat_owner_ids', 'in', [user.id])`\n\n**Line Models** (parent-scoped):\n- **Read**: `('{parent}.harakat_owner_ids', 'in', [user.id]) OR ('{parent}.harakat_owner_ids', 'in', user.harakat_subordinate_ids.ids)`\n- **Write**: `('{parent}.harakat_owner_ids', 'in', [user.id])`\n\n**Sensitive Models** (payroll):\n- **Read**: Self-domain only\n- **Write**: Manager group only\n\n---\n\n## Development Guidelines\n\n### Adding New Models\n\n1. Define model in `models/` directory\n2. Add to `__init__.py` imports\n3. Create views in `views/`\n4. Add menu items in `views/` XML\n5. Update `__manifest__.py` data files\n\n### Adding API Endpoints\n\n```python\n@http.route('/api/v1/<model>/list', auth='user', type='http', methods=['GET'], csrf=False)\ndef list_model(self, **kwargs):\n data, error = self._get_model_data(\n '<model>',\n domain=domain,\n fields=['field1', 'field2'],\n offset=int(kwargs.get('offset', 0)),\n limit=int(kwargs.get('limit', 80)),\n )\n return json_response(data)\n```\n\n### Extending Dunning\n\n1. Create new dunning level in `account.dunning.level`\n2. Configure delay, charge %, and fixed charge\n3. Template must reference the level\n\n### Budget Variance Calculation\n\n```python\n# Income/expense accounts: negative balance\nif account_type in ['income', 'expense']:\n actual_amount = -sum(move_lines.mapped('balance'))\n# Assets/liabilities: positive balance\nelse:\n actual_amount = sum(move_lines.mapped('balance'))\n```\n\n---\n\n## Testing\n\n### Account Advanced\n\n```python\n# Test dunning level matching\ndef test_dunning_level_matching():\n dunning = self.env['account.dunning'].create({\n 'partner_id': self.partner.id,\n 'template_id': self.template.id,\n 'date': fields.Date.today(),\n })\n line = self.env['account.dunning.line'].create({\n 'dunning_id': dunning.id,\n 'move_id': self.invoice.id,\n 'amount_residual': 1000.0,\n })\n # Verify charge calculation based on days overdue\n```\n\n### ERP Gateway\n\n```python\ndef test_security_isolation():\n # Create two companies\n company1 = self.env['res.company'].create({'name': 'Company 1'})\n company2 = self.env['res.company'].create({'name': 'Company 2'})\n \n # Create user in company1\n user1 = self.env['res.users'].create({\n 'name': 'User 1',\n 'login': 'user1',\n 'company_ids': [(4, company1.id)],\n })\n \n # User1 should not see company2 records\n records = self.env['account.move'].search([('company_id', '=', company2.id)])\n self.assertEqual(records, [])\n```\n\n---\n\n## Known Limitations\n\n1. **Dunning Email**: `action_send()` marks as sent but email logic is TODO\n2. **Excel Export**: `action_export_excel()` is placeholder\n3. **Tenant Isolation**: Requires container isolation for full effect\n4. **Service User**: Must maintain company_ids for cross-tenant operations\n\n---\n\n## Version History\n\n| Version | Date | Changes |\n|---------|------|---------|\n| 1.0 | 2026-07-14 | Initial release |\n| 1.1 | 2026-07-16 | Tenant boundary refinement |\n| 1.1.0 | 2026-07-16 | ERP Gateway security generator |","erp-harakat-backend-erp-harakat-backend":"# erp-harakat-backend — erp-harakat-backend\n\n\n\n# erp-harakat-backend\n\n## Overview\n\nThe `erp-harakat-backend` module defines the runtime environment for the Odoo 19 ERP application. It is not a Python application logic module but rather a containerized service definition responsible for hosting the Odoo server, managing dependencies, and exposing the API to external clients.\n\nThis module encapsulates the Odoo 19 source code, system-level dependencies, and configuration required to run the backend service. It does not contain internal business logic execution flows; instead, it orchestrates the Odoo process (`odoo-bin`) to handle requests.\n\n## Infrastructure Stack\n\nThe service is built using a Docker container based on the `python:3.12-slim-bookworm` image. The build process installs specific system dependencies required by Odoo for PDF generation, database connectivity, and image processing.\n\n### System Dependencies\nThe following packages are installed at build time to support Odoo functionality:\n* **PDF Generation:** `wkhtmltopdf`\n* **Database Client:** `postgresql-client`\n* **Image Processing:** `libjpeg-dev`, `libfreetype6-dev`, `fonts-noto-cjk`\n* **XML/HTML Parsing:** `libxml2-dev`, `libxslt1-dev`\n* **LDAP Support:** `libldap2-dev`, `libsasl2-dev`\n* **Utilities:** `curl`, `ca-certificates`, `libmagic1`\n\n### Odoo Installation\nThe Odoo source code is vendored into the container at `/opt/erp/odoo`. The installation process performs an editable install:\n```bash\npip install --no-cache-dir -e /opt/erp/odoo\n```\nThis ensures that changes to the vendored source are reflected in the running environment without rebuilding the image.\n\n## Configuration\n\nThe service behavior is controlled by `/etc/odoo/odoo.conf`. Key configuration parameters include:\n\n| Parameter | Value | Description |\n| :--- | :--- | :--- |\n| `addons_path` | `/opt/erp/odoo/addons,/mnt/extra-addons` | Defines the search path for Odoo modules. Custom addons are mounted at `/mnt/extra-addons`. |\n| `data_dir` | `/var/lib/odoo` | Directory for storing database files and logs. |\n| `without_demo` | `all` | Prevents the installation of demo data during startup. |\n| `list_db` | `False` | Disables the database listing feature in the web interface. |\n| `proxy_mode` | `True` | Enables proxy mode for the web server. |\n\n### Database Connection\nDatabase credentials are not hardcoded. The `odoo.conf` file explicitly comments that `db_host`, `db_user`, and `db_password` are sourced from environment variables at runtime:\n* `HOST`\n* `USER`\n* `PASSWORD`\n\nDevelopers must ensure these environment variables are set when deploying the container to establish a connection to the PostgreSQL database.\n\n## Runtime & Entry Point\n\nThe container runs as the non-root user `erp`. The entrypoint is defined in the `Dockerfile` to execute the Odoo binary:\n\n```bash\nENTRYPOINT [\"python3\", \"/opt/erp/odoo/odoo-bin\"]\nCMD [\"-c\", \"/etc/odoo/odoo.conf\"]\n```\n\n* **Entry Point:** `python3 /opt/erp/odoo/odoo-bin`\n* **Command:** `-c /etc/odoo/odoo.conf`\n\nThis configuration ensures the Odoo server starts with the specified configuration file. The `odoo-bin` script handles the initialization of the WSGI server and the application logic.\n\n## Network & Security\n\n* **Exposed Port:** `8069` (HTTP)\n* **User:** The process runs under the `erp` user (`USER erp`).\n* **Security:** The `proxy_mode` setting in `odoo.conf` is enabled, which is recommended for production environments to handle reverse proxy headers correctly.\n\n## Architecture\n\nThe following diagram illustrates the relationship between the external client, the backend container, and the database.\n\n```mermaid\ngraph TD\n Client[External Client] -->|HTTP 8069| Container[erp-harakat-backend Container]\n Container -->|Process| Odoo[Odoo 19 Process (odoo-bin)]\n Odoo -->|DB Connection| Postgres[(PostgreSQL)]\n Container -.->|Mounts| Addons[/mnt/extra-addons]\n```\n\n### Component Responsibilities\n\n1. **External Client:** Initiates HTTP requests to port 8069.\n2. **erp-harakat-backend Container:** Hosts the Odoo process, manages the Python environment, and serves as the network boundary.\n3. **Odoo 19 Process:** Executes the business logic and API endpoints defined in the vendored source.\n4. **PostgreSQL:** Stores persistent data. Connection details are injected via environment variables.\n5. **Custom Addons:** Located at `/mnt/extra-addons`, these are mounted into the container to extend functionality without modifying the base image.","erp-harakat-backend-scripts":"# erp-harakat-backend — scripts\n\n\n\n# erp-harakat-backend/scripts\n\nThis module contains utility scripts for database migration, tenant isolation, and branding cleanup during the transition from Odoo to the custom ERP stack. The scripts are designed to be idempotent where possible and prioritize data integrity through transactional safety mechanisms (savepoints/rollbacks).\n\n## Overview\n\nThe scripts support the \"Startup-19\" migration workflow, specifically isolating Company 1 (LifeSpace/Harakat) from a multi-tenant Odoo instance. The workflow consists of three main components:\n\n1. **`debrand.sh`**: Cleans up the codebase branding and module names.\n2. **`tenant19_scrub.py`**: Isolates Company 1 in a database clone by removing all other tenants' data.\n3. **`tenant19_manifest.py`**: Generates a checksum manifest to verify data integrity between the source and scrubbed databases.\n\n## File Descriptions\n\n### `debrand.sh`\n\nA Bash script responsible for renaming and updating branding references within the `custom_addons` directory. It ensures the codebase reflects the new ERP identity without touching the Odoo framework namespace.\n\n**Key Operations:**\n* **Directory Renaming:** Renames `isomorphic_bridge` to `erp_bridge` if the old directory exists.\n* **Manifest Updates:** Modifies `$ADDONS/erp_bridge/__manifest__.py` to replace \"Isomorphic UI Bridge\" with \"ERP Bridge\" and updates the example URL to `https://harakat.tech`.\n* **Reference Replacement:** Recursively searches `custom_addons` for references to `isomorphic_bridge` and replaces them with `erp_bridge`.\n\n**Idempotency:**\nThe script checks for the existence of `$ADDONS/isomorphic_bridge` before attempting to move it, ensuring it can be run multiple times without error.\n\n### `tenant19_scrub.py`\n\nA Python script executed via the Odoo shell against a `tenant_19` CLONE. It performs a destructive scrub of the database to remove all data belonging to companies other than `KEEP_COMPANY` (1).\n\n**Safety Mechanisms:**\n* **Savepoints:** Uses `cr.execute(\"SAVEPOINT ...\")` and `ROLLBACK TO SAVEPOINT` for every model deletion and user removal. If a deletion fails, only the specific row is rolled back.\n* **Dry Run:** Supports a `DRY_RUN` flag to preview deletions without committing changes.\n* **Verification Gates:** Before committing, it verifies that no foreign data remains and that only Company 1 exists.\n\n**Execution Flow:**\nThe script follows a strict 9-step process to handle foreign key dependencies:\n1. **Pass-1:** Deletes company-scoped rows on every model.\n2. **Pass-2:** Deletes shared rows attributed to foreign users.\n3. **Attachments/Mail:** Removes foreign attachments and cancels pending outgoing mail.\n4. **Users:** Deactivates and deletes foreign users.\n5. **FK Break:** Updates `res_company` nullable foreign keys to NULL to prevent FK errors during company deletion.\n6. **Company Deletion:** Deletes the remaining companies.\n7. **Pass-3:** Deletes config rows (calendars, sequences) freed by company deletion.\n8. **Verification:** Checks for zero foreign data and zero active foreign users.\n9. **Commit:** Sets tenant identity parameters (`harakat.startup_id`, `database.uuid`) and commits.\n\n**Key Functions:**\n* `delete_company_rows(model)`: Handles bulk deletion of rows belonging to removed companies with savepoint protection.\n* `company_models()`: Returns a list of non-transient, non-abstract models that have a `company_id` field.\n\n### `tenant19_manifest.py`\n\nA Python script executed via the Odoo shell to generate a preservation manifest. It is run on both the source `erp` DB and the scrubbed `tenant_19` DB to compare record counts and ID checksums.\n\n**Key Operations:**\n* **Record Counting:** Counts records for a predefined list of `MODELS` (e.g., `sale.order`, `account.move`, `harakat.document.dispatch`).\n* **ID Checksums:** Generates a SHA16 digest of sorted record IDs for each model to ensure no data was lost or altered.\n* **State Analysis:** For `account.move`, it breaks down records by state and total amount.\n* **User Verification:** Counts retained users and generates a login checksum.\n\n**Output:**\nPrints a JSON object containing counts, ID digests, and state breakdowns for comparison.\n\n## Migration Workflow\n\nThe scripts are used in a specific sequence to ensure a clean migration from the Odoo environment to the isolated ERP tenant.\n\n```mermaid\nflowchart LR\n A[Codebase Prep] -->|Run debrand.sh| B[Cleaned Codebase]\n B -->|Clone DB| C[tenant_19 Clone]\n C -->|Run tenant19_scrub.py| D[Isolated Company 1]\n D -->|Run tenant19_manifest.py| E[Source DB]\n E -->|Compare Manifests| F{Integrity Check}\n F -->|Pass| G[Migration Complete]\n F -->|Fail| H[Rollback/Debug]\n```\n\n## Technical Details\n\n### Odoo Shell Usage\nAll Python scripts (`tenant19_scrub.py`, `tenant19_manifest.py`) are designed to be run via the Odoo shell:\n```bash\nodoo -d <database_name> -i <module> -u <module> --shell <script_path>\n```\nThey rely on `env` (the Odoo environment) and `env.cr` (the database cursor).\n\n### Data Integrity Guards\nIn `tenant19_scrub.py`, specific business logic guards are respected during deletion:\n* **Lifecycle Guards:** For models like `sale.order`, the script attempts to cancel the record (`action_cancel` or `button_cancel`) before deletion to bypass \"cannot delete a sent quotation\" constraints.\n* **Foreign References:** If a row is referenced by retained data, the deletion is caught by the savepoint and logged in `skipped`.\n\n### Configuration Parameters\nUpon successful scrubbing, the following parameters are set in `ir.config_parameter`:\n* `harakat.startup_id`: Set to `\"19\"`.\n* `database.uuid`: Set to a new UUID4.\n\n### Model Lists\n* **`MODELS` (Manifest):** Includes core Odoo models (`res.partner`, `sale.order`) and custom models (`harakat.document.dispatch`, `planning.slot`).\n* **`ATTRIBUTED` (Scrub):** Includes shared models that may be owned by foreign users (`res.partner`, `knowledge.article`, `crm.competitor`).\n\n## Error Handling\n* **`debrand.sh`:** Uses `set -euo pipefail` to exit immediately on error.\n* **`tenant19_scrub.py`:** If verification gates fail (foreign data remains, FK violations), the script executes `cr.rollback()` and exits without committing changes.\n* **`tenant19_manifest.py`:** Prints `absent: True` for models not found in the environment, allowing for easy diffing of schema changes.","erp-harakat-backend":"# erp-harakat-backend\n\n\n\n# erp-harakat-backend\n\n## Overview\n\nThe `erp-harakat-backend` module defines the runtime environment for the Odoo 19 ERP application. It is not a Python application logic module but rather a containerized service definition responsible for hosting the Odoo server, managing dependencies, and exposing the API to external clients.\n\nThis module encapsulates the Odoo 19 source code, system-level dependencies, and configuration required to run the backend service. It does not contain internal business logic execution flows; instead, it orchestrates the Odoo process (`odoo-bin`) to handle requests.\n\n## Infrastructure Stack\n\nThe service is built using a Docker container based on the `python:3.12-slim-bookworm` image. The build process installs specific dependencies to support the Odoo runtime and custom extensions.\n\n## Sub-Modules\n\nThe backend relies on supporting modules to manage the environment and data integrity:\n\n- **[custom_addons](custom_addons.md)**: Contains the custom business logic and extensions for the ERP stack.\n- **[scripts](scripts.md)**: Provides utility scripts for database migration, tenant isolation, and branding cleanup.\n\n## Workflow Integration\n\nThe `scripts` module supports the \"Startup-19\" migration workflow, specifically isolating Company 1 (LifeSpace/Harakat) from a multi-tenant Odoo instance. Key scripts include `debrand.sh` for codebase cleanup, `tenant19_scrub.py` for database cloning, and `tenant19_manifest.py` for configuration generation. These scripts ensure data integrity through transactional safety mechanisms (savepoints/rollbacks) before the `erp-harakat-backend` service initializes the Odoo process.\n\n## Architecture\n\n```mermaid\ngraph TD\n A[Migration Scripts] -->|Prepare Environment| B_erp-harakat-backend[\"B(erp-harakat-backend)\"]\n B -->|Hosts| C[Odoo 19 Server]\n B -->|Loads| D[custom_addons]\n C -->|API| E[External Clients]\n```","harakat-admin-app":"# Harakat_Admin — app\n\n\n\n# Harakat_Admin — App Module\n\n## Overview\n\nThe `Harakat_Admin` app is a Next.js client-side admin console that provides operational controls for tenant management, marketplace agent registration, learning content, governance, and infrastructure. It implements a unified state management pattern (`useResource` + `StateView`) and provides dynamic routing for all record-based pages.\n\n---\n\n## Architecture\n\n### Core Components\n\n```mermaid\ngraph TD\n A[PageFrame] --> B[StateView]\n B --> C[useResource]\n C --> D[API Layer]\n D --> E[startupsApi]\n D --> F[coursesApi]\n D --> G[marketplaceApi]\n D --> H[dashboardApi]\n D --> I[inferenceApi]\n B --> J[ConfirmDestructiveDialog]\n B --> K[StatTile]\n A --> L[PageFrame]\n```\n\n### Data Flow Pattern\n\nAll pages follow a consistent pattern:\n\n1. **Route Guard** → `useResource` with `isValidId` check\n2. **API Call** → `startupsApi.get()` / `coursesApi.getById()` / etc.\n3. **State Resolution** → `StateView` handles loading/error/empty\n4. **Render** → Conditional rendering based on `record.data?.kind`\n\n---\n\n## Key Modules\n\n### Companies Module\n\n**Location:** `app/admin/companies/`\n\n#### Directory Page (`page.tsx`)\n\nThe companies directory provides:\n- **Stats tiles** with state-aware counts (total, active, NGOs, pending invites)\n- **Search filtering** with debounced queries\n- **Provision dialog** for creating new tenants\n- **Action menu** per row (open detail, quick look, resend invite, change type, suspend/reactivate)\n\n**Critical Fix:** The stats tiles now track whether counts were actually observed (`statStateOf`), preventing \"0\" from rendering on API failures.\n\n#### Detail Page (`[id]/page.tsx`)\n\nThe detail page implements **four states** for the company record:\n\n| State | Meaning | Render |\n|-------|---------|--------|\n| `found` | Record exists | Full detail view |\n| `absent` | Conclusive absence | `notFound()` |\n| `inconclusive` | Truncated list | \"We can't tell you whether this company exists\" |\n| `error` | API failure | `StateView` error branch |\n\n**Key Operations:**\n- **Resend invite** → `startupsApi.resendInvite(id)`\n- **Re-apply profile** → `startupsApi.applyProfile(id, orgType)`\n- **Change type** → `startupsApi.applyProfile(id, newOrgType)` (additive only)\n- **Suspend/Re-activate** → `startupsApi.suspend(id)` / `startupsApi.reactivate(id)`\n\n**Profile Application:** Applying a profile is **additive** — it installs what the type needs and removes nothing the tenant already has.\n\n---\n\n### Marketplace Module\n\n**Location:** `app/admin/marketplace/`\n\n#### Registry Page (`page.tsx`)\n\nLists all registered MCP agents with:\n- **Status badges** (active/testing)\n- **Usage metrics** (calls, success rate, avg response time)\n- **Pricing mode** (fixed/dynamic)\n- **Refresh button** for generation-guarded reloads\n\n**Critical Fix:** The \"Refresh Registry\" button prevents out-of-order responses from corrupting the registry state.\n\n#### Agent Detail Page (`[id]/page.tsx`)\n\nImplements **dynamic routing** for agents (previously only accessible via row actions).\n\n**Features:**\n- **Cost prediction** → `predictAgentCost(id, task)` with breakdown\n- **Status management** (active/testing)\n- **Remove agent** → `deleteAgent(id)` with destructive confirmation\n\n**Usage Display:** Success rate is shown with call count because rendering a rate without its sample is misleading.\n\n---\n\n### Learning Hub Module\n\n**Location:** `app/admin/learning-hub/`\n\n#### Hub Page (`page.tsx`)\n\nTwo-tab interface:\n- **Courses tab** → List with search, add course dialog\n- **Instructors tab** → Instructor roster\n\n**Critical Fix:** Search uses `useResource` with debouncing (300ms) and cancels superseded requests, preventing stale data writes.\n\n#### Course Detail Page (`[id]/page.tsx`)\n\nImplements **record-based routing** for courses.\n\n**Features:**\n- **Status management** (draft/active/archived)\n- **Materials list** → External URLs with `rel=\"noreferrer\"` and `dir=\"ltr\"`\n- **Delete course** → `coursesApi.delete(id)` with destructive confirmation\n\n**Materials Handling:** URLs are rendered as links with `dir=\"ltr\"` to prevent RTL mirroring issues.\n\n---\n\n### Governance Module\n\n**Location:** `app/admin/governance/`\n\nThe governance console is **generated from the server's governance descriptor array** — adding a knob there makes it appear here with validation, audit trail, and rollback already working.\n\n**Authentication:** Reads session role on client (`getSession()?.role`) and passes to `GovernanceConsole`.\n\n---\n\n### Inference Module\n\n**Location:** `app/admin/inference/`\n\nManages the document pipeline GPU machine:\n\n**Operations:**\n- **Select machine** → `selectInferenceHost(instanceId)`\n- **Disconnect** → `disconnectInferenceHost()` (work queued, not failed)\n- **Health check** → OCR and LLM model availability\n\n**Critical Behavior:** Switching machines mid-run is safe — work in flight is left OWED and picked up again, never marked failed.\n\n---\n\n### Dashboard Module\n\n**Location:** `app/admin/page.tsx`\n\nProvides metrics and activity feeds:\n\n**Data Sources:**\n- **Stats** → `dashboardApi.getStats()` (degraded per-card on failure)\n- **Activity** → `dashboardApi.getActivity(5)` (degraded per-card)\n- **Requests** → `dashboardApi.getRequests()` (degraded per-card)\n- **Startups** → `dashboardApi.getStartups()` (required, failure reaches `StateView`)\n\n**Critical Fix:** Each feed degrades independently rather than blanking the whole dashboard.\n\n---\n\n## Shared Patterns\n\n### `useResource` Hook\n\n```typescript\nconst record = useResource<Loaded>(\n useCallback(async (signal: AbortSignal) => {\n const [got, profileRes] = await Promise.all([\n startupsApi.get(id, signal),\n startupsApi.tenantProfiles(signal),\n ]);\n if (!got.found) {\n return { kind: got.inconclusive ? \"inconclusive\" : \"absent\" } as const;\n }\n return { kind: \"found\", startup: got.startup, profiles: profileRes.profiles } as const;\n }, [id]),\n [id],\n { enabled: isValidId }\n);\n```\n\n**Features:**\n- Debouncing with `debounceMs` option\n- Abort signal support\n- Conditional loading (`enabled` prop)\n- Automatic reload on mutation\n\n### `StateView` Component\n\nWraps content with loading/skeleton/error/empty handling:\n\n```typescript\n<StateView\n state={companies}\n subject=\"companies\"\n isEmpty={() => false}\n skeletonRows={6}\n>\n {() => <Table>...<\/Table>}\n<\/StateView>\n```\n\n**States:**\n- `loading` → Skeleton rows\n- `error` → Error message with retry\n- `empty` → Empty state (optional)\n- `data` → Rendered content\n\n### `RecordRoute` Component\n\nProvides consistent routing for detail pages:\n\n```typescript\n<RecordRoute<Course>\n state={record}\n subject=\"this course\"\n eyebrow=\"Course\"\n fallbackTitle={`Course #${raw ?? \"\"}`}\n title={(c) => c.title}\n subtitle={(c) => `${c.category} · taught by ${c.instructor}`}\n backHref=\"/admin/learning-hub\"\n backLabel=\"Learning Hub\"\n>\n {(course) => <div>...<\/div>}\n<\/RecordRoute>\n```\n\n**Features:**\n- Dynamic title from record\n- Breadcrumb navigation\n- Back link to directory\n- Fallback title for missing records\n\n### `ConfirmDestructiveDialog`\n\nStandardized destructive action confirmation:\n\n```typescript\n<ConfirmDestructiveDialog\n open={lifecycleOpen}\n onOpenChange={setLifecycleOpen}\n title={suspended ? \"Reactivate company\" : \"Suspend company\"}\n subject={`${data.startup.name} (#${data.startup.id})`}\n irreversible={false}\n confirmLabel={suspended ? \"Reactivate\" : \"Suspend\"}\n blastRadius={{\n state: \"ready\",\n consequences: suspended\n ? [\"Everyone in this company can sign in again immediately.\"]\n : [\"Every person in this company is locked out immediately.\"],\n }}\n reversibleAlternative={suspended ? undefined : \"You can reactivate this company at any time from this page.\"}\n onConfirm={async () => {\n await run(\n suspended ? \"reactivate this company\" : \"suspend this company\",\n () =>\n suspended\n ? startupsApi.reactivate(data.startup.id)\n : startupsApi.suspend(data.startup.id),\n suspended ? \"Company reactivated.\" : \"Company suspended.\"\n );\n setLifecycleOpen(false);\n }}\n/>\n```\n\n**Features:**\n- Consequence listing\n- Reversibility indication\n- Blast radius documentation\n- Toast notification on success\n\n---\n\n## API Integration\n\n### `startupsApi`\n\n```typescript\n// Companies\nget(id: number, signal: AbortSignal)\nlist(signal: AbortSignal)\nprovision(data: ProvisionCompanyData)\nresendInvite(id: number)\nsuspend(id: number)\nreactivate(id: number)\napplyProfile(id: number, orgType: OrgType)\ntenantProfiles(signal: AbortSignal)\n```\n\n### `coursesApi`\n\n```typescript\nlist(data: { search?: string }, signal: AbortSignal)\ngetById(id: string, signal: AbortSignal)\nupdate(id: string, data: UpdateCourseData)\ndelete(id: string)\ncreate(data: CreateCourseData)\n```\n\n### `marketplaceApi`\n\n```typescript\ngetAgents(signal: AbortSignal)\ngetAgentById(id: string, signal: AbortSignal)\ndeleteAgent(id: string)\npredictAgentCost(id: string, task: string)\n```\n\n### `dashboardApi`\n\n```typescript\ngetStats(signal: AbortSignal)\ngetActivity(limit: number, signal: AbortSignal)\ngetRequests(signal: AbortSignal)\ngetStartups(signal: AbortSignal)\n```\n\n### `inferenceApi`\n\n```typescript\ngetInferenceState()\nselectInferenceHost(instanceId: number)\ndisconnectInferenceHost()\n```\n\n---\n\n## Error Handling\n\n### `MalformedPayloadError`\n\nRaised when `readList` receives a non-array response:\n\n```typescript\n// app/admin/page.tsx\nconst stats = statsOrEmpty<T>(res, key);\nfunction statsOrEmpty<T>(res: unknown, key: string): T[] {\n if (res === null || res === undefined) return [];\n return readList<T>(res, key);\n}\n```\n\n**Behavior:**\n- `null`/`undefined` → Empty array (degraded state)\n- Non-array → `MalformedPayloadError` (failure)\n\n### `describeError`\n\nStandardizes error messages for toast notifications:\n\n```typescript\ntoast({\n title: \"Couldn't provision the company\",\n description: describeError(err, \"provision this company\"),\n variant: \"destructive\",\n});\n```\n\n---\n\n## Authentication\n\n### Session Management\n\n```typescript\n// app/admin/layout.tsx\nuseEffect(() => {\n const session = getSession();\n if (!session) {\n setAuthState(\"anonymous\");\n router.replace(\"/login\");\n return;\n }\n setAdminEmail(session.email || \"Admin\");\n setAuthState(\"authenticated\");\n}, [router]);\n```\n\n**Critical Fix:** The guard now checks the token directly (not a localStorage flag), preventing authenticated shell rendering without a valid token.\n\n### Logout\n\n```typescript\nconst handleLogout = () => {\n void signOut();\n setAuthState(\"anonymous\");\n router.replace(\"/login\");\n};\n```\n\n**Behavior:** Clears browser immediately in `finally`, so operator is signed out regardless of server response.\n\n---\n\n## Navigation\n\n### `PageFrame`\n\nProvides consistent layout with:\n- **Sidebar** (desktop) / **Drawer** (mobile)\n- **Header** with global search\n- **Breadcrumbs**\n- **Action buttons**\n\n### Global Search\n\n```typescript\nconst searchResults = useMemo(() => {\n const query = globalSearch.trim().toLowerCase();\n if (!query) return [];\n return [...navigation, ...bottomNavigation]\n .filter((item) => item.name.toLowerCase().includes(query))\n .slice(0, 6);\n}, [bottomNavigation, globalSearch, navigation]);\n```\n\n**Features:**\n- Debounced blur (120ms)\n- Results dropdown\n- Direct navigation on selection\n\n---\n\n## Key Fixes & Improvements\n\n### 1. Dynamic Routes for Records\n\n**Before:** `/admin/companies/1` returned 404 (no dynamic routes)\n**After:** Full detail page with URL-based routing\n\n**Impact:** Operators can now link to specific records in tickets, bookmark them, and share URLs.\n\n### 2. State-Aware Stats\n\n**Before:** Failed API calls rendered \"0\" for all counts\n**After:** `statStateOf` tracks whether counts were observed\n\n**Impact:** Operators can distinguish between \"no companies\" and \"can't read the list\".\n\n### 3. Debounced Search\n\n**Before:** Debounced the timer, not the request (stale data writes)\n**After:** `useResource` with `debounceMs` option\n\n**Impact:** Search results are always current, no superseded writes.\n\n### 4. Per-Card Degradation\n\n**Before:** One failed request blanked the whole dashboard\n**After:** Each feed degrades independently\n\n**Impact:** Operators can still see partial data when some APIs fail.\n\n### 5. Inconclusive Absence\n\n**Before:** Truncated list returned 404\n**After:** `inconclusive` state with \"We can't tell you whether this company exists\"\n\n**Impact:** Operators understand the difference between \"not found\" and \"can't verify\".\n\n---\n\n## Contributing\n\n### Adding a New Detail Page\n\n1. Create `[id]/page.tsx` in the module directory\n2. Implement `useResource` with `lookupById`\n3. Wrap in `RecordRoute` component\n4. Add `backHref` and `backLabel` for navigation\n\n### Adding a New API Endpoint\n\n1. Add to `lib/api/[module].ts`\n2. Export type definitions\n3. Import in page and use with `useResource`\n\n### Adding Governance Controls\n\n1. Add knob to server governance descriptor\n2. Console auto-generates with validation/audit/rollback\n\n---\n\n## Testing\n\n### Critical Test Cases\n\n1. **404 vs Inconclusive** → Verify `notFound()` vs \"We can't tell\"\n2. **Stats on Failure** → Verify state-aware counts\n3. **Debounced Search** → Verify no stale writes\n4. **Per-Card Degradation** → Verify partial data display\n5. **Logout** → Verify immediate browser clear\n\n### Common Pitfalls\n\n- **`notFound()` in `useResource`** → Throws sentinel error, don't catch\n- **`?? []` on collections** → Renders \"nothing here\" for failures\n- **Shared `loading` state** → Mutations replace list with skeleton\n- **LocalStorage flags** → Token and flag must be in sync\n\n---\n\n## Related Modules\n\n- `Harakat_Admin/hooks/useResource.ts` → Data fetching pattern\n- `Harakat_Admin/components/state/StateView.tsx` → State handling\n- `Harakat_Admin/components/records/RecordRoute.tsx` → Detail page wrapper\n- `Harakat_Admin/lib/api/shape.ts` → `readList` and error handling\n- `Harakat_Admin/lib/auth/session.ts` → Authentication\n\n---\n\n## Version Notes\n\n**2026-08-04:** Dynamic routes added to companies, courses, and agents. Stats now state-aware. Search debouncing fixed. Per-card degradation implemented.","harakat-admin-components":"# Harakat_Admin — components\n\n\n\n# Harakat_Admin — components\n\n## Overview\n\nThe `Harakat_Admin/components` module provides the UI building blocks for the Harakat Admin Console. It implements a consistent design system, governance interfaces, company management tools, learning hub components, and marketplace agent registry.\n\n## Module Purpose\n\nThis module serves as the **UI layer** for the admin console, providing:\n\n- **Consistent page layouts** via `PageFrame`\n- **Governance interfaces** for command policies, shadow testing, and prompt inspection\n- **Company provisioning** with profile application tracking\n- **Learning hub** course and instructor management\n- **Marketplace agent registry** for MCP agents\n- **Shared UI patterns** (skeletons, empty states, error states)\n\n## Architecture\n\n```mermaid\ngraph TB\n subgraph \"Page Layer\"\n A[PageFrame] --> B[Content]\n end\n \n subgraph \"Governance\"\n C[GovernanceConsole] --> D[ScopeList]\n C --> E[ResourceControl]\n C --> F[SimulatorPanel]\n C --> G[ShadowPanel]\n C --> H[PromptInspector]\n E --> I[CommandPolicyEditor]\n E --> J[RevisionHistory]\n end\n \n subgraph \"Companies\"\n K[ProvisionCompanyDialog] --> L[CompanyProfileSheet]\n end\n \n subgraph \"Learning Hub\"\n M[CoursesTab] --> N[AddCourseDialog]\n O[InstructorsTab]\n end\n \n subgraph \"Marketplace\"\n P[AgentRegistryContent]\n end\n \n subgraph \"Shared UI\"\n Q[StateView] --> R[Loading Skeletons]\n Q --> S[Empty States]\n Q --> T[Error States]\n end\n \n A -.-> Q\n C -.-> Q\n K -.-> Q\n M -.-> Q\n O -.-> Q\n P -.-> Q\n```\n\n## Key Components\n\n### PageFrame\n\nThe **single source of truth** for page titles and layout.\n\n```typescript\ninterface PageFrameProps {\n title: string;\n subtitle?: ReactNode;\n eyebrow?: ReactNode;\n actions?: ReactNode;\n breadcrumb?: FrameCrumb[];\n aside?: ReactNode;\n children: ReactNode;\n}\n```\n\n**Why it exists:** Eliminates inconsistent heading styles across 14+ pages. The `check:frame` gate enforces exactly one `<h1>` per page.\n\n**Key features:**\n- Breadcrumb navigation (detail routes only)\n- Right-aligned actions slot\n- Optional `aside` for page-level counters\n- Consistent `max-w-7xl` container\n\n### Governance Console\n\nThe central interface for managing command policies and governance settings.\n\n```typescript\nfunction GovernanceConsole({ role }: { role: string | null })\n```\n\n**Execution flow:**\n```\nGovernanceConsole\n ├─ fetchScopes → ScopeList\n ├─ fetchScopeReport → ResourceControl\n ├─ SimulatorPanel (dry-run testing)\n ├─ ShadowPanel (gate shadow mode)\n └─ PromptInspector (AI prompt history)\n```\n\n**Role-based access:**\n- `SUPER_ADMIN`: All scopes including platform\n- `ADMIN`: Tenant and department scopes only\n- Read-only for non-writable scopes\n\n### ResourceControl\n\nRenders governance controls dynamically based on entry type.\n\n```typescript\nfunction ResourceControl({\n scope,\n entry,\n writable,\n onSave,\n onClear,\n onRollback,\n})\n```\n\n**Control types:**\n- `segmented`: Enum selection (e.g., allowlist/denylist)\n- `switch`: Boolean toggle\n- `policy-editor`: Command policy rules\n\n**Enforcement tracking:**\n- Shows configured vs effective values\n- Displays `effectiveFrom` authority\n- Warns when overridden by stricter scope\n\n### CommandPolicyEditor\n\nOrdered rule editor for command policies.\n\n```typescript\nfunction CommandPolicyEditor({\n policy,\n effective,\n disabled,\n onSave,\n})\n```\n\n**Key behaviors:**\n- First-match-wins ordering\n- Shows inherited rules count\n- Rule pattern matching (regex)\n- Decision types: `allow`, `deny`, `require_approval`\n\n### ProvisionCompanyDialog\n\nTwo-faced dialog: form before submission, receipt after.\n\n```typescript\nfunction ProvisionCompanyDialog({\n open,\n onOpenChange,\n form,\n onFormChange,\n result,\n busy,\n onProvision,\n onDone,\n})\n```\n\n**Two states:**\n1. **Form mode:** Company name, email, client name, org type, status\n2. **Receipt mode:** Shows invite status AND profile application status separately\n\n**Why separate:** A tenant can be created with invite failing but profile applying. Collapsing into one \"success\" hides critical failure information.\n\n### CompanyProfileSheet\n\nSide drawer showing applied profile vs registry definition.\n\n```typescript\nfunction CompanyProfileSheet({\n open,\n onOpenChange,\n startup,\n registryProfile,\n applying,\n onReapply,\n})\n```\n\n**Two panels:**\n1. **Applied profile:** Version, status, applied timestamp, re-apply button\n2. **Registry definition:** Current type definition from registry\n\n**Why separate:** They can disagree — that's why re-applying exists.\n\n### Governance Sub-components\n\n#### SimulatorPanel\n\nDry-run command testing.\n\n```typescript\nfunction SimulatorPanel({ scope, scopeLabel })\n```\n\n**What it answers:** \"Will this block X, and which rule decides?\"\n\n**Output:**\n- Decision (allow/deny/approval)\n- Matched rule index and source\n- Would enforce flag\n\n#### ShadowPanel\n\nGate shadow mode testing.\n\n```typescript\nfunction ShadowPanel({ scope, scopeLabel })\n```\n\n**What it shows:**\n- Disagreement rate (shadow vs actual)\n- Sample count with confidence floor (50)\n- Recent disagreements table\n\n**Why it exists:** Gates have only \"on/off\" — shadow mode adds \"test without enforcing\"\n\n#### PromptInspector\n\nAI prompt history viewer.\n\n```typescript\nfunction PromptInspector({ scope })\n```\n\n**What it captures:**\n- Task, history, memories, tier, grant\n- Credentials present (withheld from storage)\n- Masked count for redacted values\n\n**Two honesties:**\n1. Runtime assembles prompt after request — not captured\n2. Credential presence is reported, value is not stored\n\n### Learning Hub Components\n\n#### AddCourseDialog\n\n```typescript\nfunction AddCourseDialog({\n open,\n onOpenChange,\n onSave,\n})\n```\n\n**Fields:**\n- Title, category, instructor\n- Duration, credits, level\n- Description, materials upload\n\n#### CoursesTab / InstructorsTab\n\nTab-based course management with:\n- Search filtering\n- Stats cards (total, active, avg rating)\n- Empty states with add prompts\n\n### Marketplace Components\n\n#### AgentRegistryContent\n\n```typescript\nfunction AgentRegistryContent({ agents })\n```\n\n**Stats:**\n- Total agents\n- Dynamic pricing count\n- Total usage\n- Average success rate\n\n**Agent fields:**\n- Name, category, description\n- Endpoint URL, provider\n- Pricing mode, status\n- Usage metrics\n\n## Shared UI Patterns\n\n### StateView\n\nLoading state wrapper with skeleton support.\n\n```typescript\nfunction StateView({\n state,\n subject,\n skeletonRows,\n isEmpty,\n children,\n})\n```\n\n**Usage:** All data-fetching components wrap their content in `StateView`.\n\n### Skeleton Components\n\n```typescript\nfunction DashboardSkeleton()\nfunction CoursesSkeleton()\nfunction InstructorsSkeleton()\n```\n\n**Pattern:** Reusable skeleton layouts matching actual component structure.\n\n### Empty States\n\n```typescript\nfunction EmptyCourses({ onAddCourse })\nfunction EmptyInstructors({ onAddInstructor })\nfunction ErrorState({ message, onRetry })\n```\n\n**Consistent styling:**\n- Icon in colored circle\n- Title + description\n- Action button\n\n## Integration Points\n\n### API Layer\n\nAll components use `useResource` hook for data fetching:\n\n```typescript\nimport { useResource } from \"@/hooks/useResource\";\n```\n\n**API modules:**\n- `@/lib/api/governance` - Governance operations\n- `@/lib/api/startups` - Company provisioning\n- `@/lib/api/learning` - Learning hub\n- `@/lib/api/marketplace` - Agent registry\n\n### UI Library\n\nAll components use shadcn/ui primitives:\n\n```typescript\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog } from \"@/components/ui/dialog\";\nimport { Sheet } from \"@/components/ui/sheet\";\n```\n\n### Toast Notifications\n\n```typescript\n<ToasterProvider>\n <Toaster position=\"top-center\" toastOptions={{ duration: 3000 }} />\n<\/ToasterProvider>\n```\n\n## Governance System Architecture\n\n```mermaid\ngraph LR\n A[Platform Scope] --> B[Tenant Scopes]\n B --> C[Department Scopes]\n \n A --> D[Command Policy]\n B --> D\n C --> D\n \n D --> E[Effective Policy]\n E --> F[Agent Runtime]\n \n G[Shadow Mode] -.->|Test| F\n H[Simulator] -.->|Dry Run| F\n```\n\n**Key principles:**\n- Narrower scopes tighten, never weaken\n- Composed policy shows inherited rules\n- Shadow mode tests without enforcing\n- Simulator answers \"which rule decides\"\n\n## Design Decisions\n\n### Why PageFrame, not convention?\n\n- **14 `<h1>` elements** across pages with 5 different styles\n- **Three different containers** causing title position jumps\n- **check:frame** gate enforces consistency\n- **Breadcrumb above title** for screen reader order\n\n### Why separate invite/profile in ProvisionCompanyDialog?\n\n- Tenant can be created with invite failing\n- Profile can apply independently\n- Collapsing hides critical failure information\n\n### Why shadow mode for gates?\n\n- Gates have only \"on/off\"\n- New gate lands as coin flip between \"protects nothing\" and \"might break\"\n- Shadow mode adds \"test without enforcing\"\n\n### Why prompt inspector?\n\n- Operators need to debug \"assistant did something weird\"\n- Before: answer only, no history\n- After: task, history, memories, tier, grant all visible\n\n## Contributing Guidelines\n\n### Adding New Components\n\n1. Use `PageFrame` for page-level layout\n2. Wrap data-fetching in `StateView`\n3. Use `useResource` for API calls\n4. Include skeleton and empty states\n5. Follow shadcn/ui component patterns\n\n### Governance Components\n\n1. Control types must be registered in `controlFor()`\n2. Show configured vs effective values\n3. Display `effectiveFrom` authority\n4. Support rollback via revision history\n\n### Learning Hub\n\n1. Use `AddCourseDialog` for creation\n2. Use `CoursesTab`/`InstructorsTab` for listing\n3. Include stats cards with icons\n4. Handle empty states with add prompts\n\n## Common Patterns\n\n### Loading State\n\n```typescript\n<StateView state={data} subject=\"subject\" skeletonRows={3}>\n {(data) => <Content data={data} />}\n<\/StateView>\n```\n\n### Empty State\n\n```typescript\n<EmptyCourses onAddCourse={handleAdd} />\n```\n\n### Error State\n\n```typescript\n<ErrorState message=\"Error message\" onRetry={handleRetry} />\n```\n\n### Dialog with Receipt\n\n```typescript\n<Dialog open={open} onOpenChange={onOpenChange}>\n {result ? <Receipt result={result} /> : <Form />}\n<\/Dialog>\n```\n\n## Testing Checklist\n\n- [ ] PageFrame renders single `<h1>`\n- [ ] Governance role checks work\n- [ ] Shadow mode shows disagreement rate\n- [ ] Simulator shows matched rule\n- [ ] Provision dialog shows separate invite/profile status\n- [ ] Company profile sheet shows applied vs registry\n- [ ] Learning hub handles empty states\n- [ ] Marketplace shows agent stats\n\n## Related Modules\n\n- `Harakat_Admin/hooks/useResource.ts` - Data fetching\n- `Harakat_Admin/lib/api/*` - API operations\n- `Harakat_Admin/lib/governance/manifest-types.ts` - Governance types\n- `Harakat_Admin/components/state/StateView.tsx` - Loading states\n- `Harakat_Admin/components/ui/*` - UI primitives","harakat-admin-docs":"# Harakat_Admin — docs\n\n\n\n# Learning Hub API Integration\n\n## Overview\n\nThe Learning Hub API Integration module provides a complete backend and frontend solution for managing educational content within the Harakat Admin system. It enables administrators to create, update, and delete courses, manage course materials, and track startup enrollments and progress.\n\nThe module is built using **Next.js** (Frontend), **Express** (Backend), **Prisma** (ORM), and **PostgreSQL**. It enforces strict data integrity through database constraints and middleware-based authentication.\n\n## Architecture\n\nThe system follows a standard RESTful architecture with a clear separation between the client (Next.js Admin), the API layer (Express), and the data layer (PostgreSQL).\n\n### System Flow\n\n```mermaid\nsequenceDiagram\n participant Admin as Admin UI\n participant Client as API Client<br/>(lib/api/learning.ts)\n participant Server as Express Server<br/>(routes/learning.routes.ts)\n participant Controller as Controller<br/>(modules/learning/*.controller.ts)\n participant DB as Prisma Client<br/>(PostgreSQL)\n\n Admin->>Client: API Call (e.g., createCourse)\n Client->>Server: HTTP POST /api/learning/courses\n Server->>Controller: Route Handler\n Controller->>DB: Prisma Transaction\n DB-->>Controller: Data\n Controller-->>Server: Response\n Server-->>Client: JSON Response\n Client-->>Admin: UI Update\n```\n\n## Database Schema\n\nThe module relies on three core models defined in `prisma/schema.prisma`. All models are managed via the `harakatoffices_db` PostgreSQL instance.\n\n### Course Model\nRepresents the educational content available in the hub.\n\n| Field | Type | Description |\n| :--- | :--- | :--- |\n| `id` | `Int` | Primary Key |\n| `title` | `String` | Course title |\n| `category` | `String` | Content category |\n| `description` | `String` | Detailed description |\n| `instructor` | `String` | Instructor name |\n| `level` | `CourseLevel` | BEGINNER, INTERMEDIATE, ADVANCED |\n| `duration` | `String` | Estimated duration |\n| `credits` | `Int` | Academic credits |\n| `status` | `CourseStatus` | DRAFT, ACTIVE, ARCHIVED |\n| `rating` | `Float` | Average rating |\n\n**Relationships:**\n- One-to-Many with `CourseEnrollment`\n- One-to-Many with `CourseMaterial`\n\n### CourseEnrollment Model\nTracks which startups are enrolled in which courses.\n\n| Field | Type | Description |\n| :--- | :--- | :--- |\n| `id` | `Int` | Primary Key |\n| `courseId` | `Int` | FK to Course |\n| `startupId` | `Int` | FK to Startup |\n| `officeId` | `Int` | FK to Office |\n| `progress` | `Int` | 0-100% completion |\n| `status` | `EnrollmentStatus` | IN_PROGRESS, COMPLETED, DROPPED |\n| `enrolledAt` | `DateTime` | Enrollment timestamp |\n| `completedAt` | `DateTime` | Completion timestamp |\n\n**Constraints:**\n- **Unique:** `courseId` + `startupId` (Prevents duplicate enrollments).\n- **Auto-Complete:** Logic in `updateEnrollment` sets status to `COMPLETED` if `progress` reaches 100%.\n\n### CourseMaterial Model\nStores file references for course content.\n\n| Field | Type | Description |\n| :--- | :--- | :--- |\n| `id` | `Int` | Primary Key |\n| `courseId` | `Int` | FK to Course |\n| `fileName` | `String` | Local filename |\n| `fileUrl` | `String` | Public URL |\n| `fileSize` | `Int` | File size in bytes |\n| `uploadedAt` | `DateTime` | Upload timestamp |\n\n**Constraints:**\n- **Cascade Delete:** Materials are deleted when the associated course is deleted.\n\n## Backend Implementation\n\n### Controllers\n\nControllers are located in `src/modules/learning/`. They handle business logic and database interactions.\n\n#### `courses.controller.ts`\nManages CRUD operations for courses.\n\n- **`getCourses()`**: Fetches courses with optional filters (`status`, `category`, `instructor`, `search`).\n- **`getCourseById(id)`**: Retrieves a single course including associated materials and enrollment counts.\n- **`createCourse(data)`**: Creates a new course record.\n- **`updateCourse(id, data)`**: Updates course metadata.\n- **`deleteCourse(id)`**: Deletes a course. **Validation:** Returns 400 if the course has active enrollments.\n- **`addCourseMaterials(courseId, materials)`**: Batch adds file references to a course.\n- **`deleteCourseMaterial(courseId, materialId)`**: Removes a specific material.\n\n#### `enrollments.controller.ts`\nManages startup enrollment tracking.\n\n- **`getEnrollments()`**: Lists enrollments with filters (`courseId`, `startupId`, `officeId`, `status`).\n- **`getEnrollmentById(id)`**: Fetches a specific enrollment record.\n- **`createEnrollment(data)`**: Registers a startup in a course. **Validation:** Checks existence of Course, Startup, and Office.\n- **`updateEnrollment(id, data)`**: Updates progress or status. Triggers auto-completion logic if `progress === 100`.\n- **`deleteEnrollment(id)`**: Removes an enrollment record.\n\n### Routes\n\nRoutes are defined in `src/routes/learning.routes.ts` and registered at `/api/learning` in `src/app.ts`. All endpoints require authentication via the `requireAuth` middleware.\n\n#### Course Endpoints\n| Method | Endpoint | Description |\n| :--- | :--- | :--- |\n| `GET` | `/api/learning/courses` | List courses |\n| `GET` | `/api/learning/courses/:id` | Get single course |\n| `POST` | `/api/learning/courses` | Create course |\n| `PUT` | `/api/learning/courses/:id` | Update course |\n| `DELETE` | `/api/learning/courses/:id` | Delete course |\n| `POST` | `/api/learning/courses/:id/materials` | Add materials |\n| `DELETE` | `/api/learning/courses/:id/materials/:materialId` | Delete material |\n\n#### Enrollment Endpoints\n| Method | Endpoint | Description |\n| :--- | :--- | :--- |\n| `GET` | `/api/learning/enrollments` | List enrollments |\n| `GET` | `/api/learning/enrollments/:id` | Get single enrollment |\n| `POST` | `/api/learning/enrollments` | Create enrollment |\n| `PUT` | `/api/learning/enrollments/:id` | Update enrollment |\n| `DELETE` | `/api/learning/enrollments/:id` | Delete enrollment |\n\n## Frontend Integration\n\n### API Client\nThe API client is defined in `lib/api/learning.ts`. It provides type-safe wrappers around the HTTP requests.\n\n#### `coursesApi`\n- `list(filters?)`: Fetches courses. Supports query params for filtering.\n- `getById(id)`: Fetches a specific course.\n- `create(data)`: Submits a new course.\n- `update(id, data)`: Updates an existing course.\n- `delete(id)`: Removes a course.\n- `addMaterials(id, materials)`: Adds file references.\n- `deleteMaterial(courseId, materialId)`: Removes a file reference.\n\n#### `enrollmentsApi`\n- `list(filters?)`: Fetches enrollments.\n- `getById(id)`: Fetches a specific enrollment.\n- `create(data)`: Registers an enrollment.\n- `update(id, data)`: Updates progress/status.\n- `delete(id)`: Removes an enrollment.\n\n### Components\n\n#### `app/admin/learning-hub/page.tsx`\nThe main dashboard page for the Learning Hub.\n- **State Management:** Uses local state to manage the list of courses and enrollments.\n- **Search:** Implements debounced search (300ms delay) on the `title` field.\n- **Tabs:** Switches between \"Courses\" and \"Enrollments\" views.\n- **Error Handling:** Displays toast notifications for API failures.\n- **Auto-Reload:** Triggers a re-fetch of data after successful mutations.\n\n#### `components/learning-hub/AddCourseDialog.tsx`\nA modal component for creating new courses.\n- **Props:** Accepts `onSave: (data: CreateCourseData) => void`.\n- **Validation:** Validates required fields before submission.\n- **Reset:** Clears form state upon successful save.\n\n#### `components/learning-hub/ViewCourseDialog.tsx`\nA modal for viewing and editing course details.\n- **Props:** Accepts `onEdit`, `onDelete`, and course data.\n- **Actions:** Includes a destructive \"Delete\" button with a confirmation dialog.\n\n#### `components/learning-hub/tabs/EnrollmentsTab.tsx`\nDisplays the enrollment list.\n- **Status Styling:** Renders red badges for `dropped` status.\n- **Progress:** Displays completion percentage.\n\n## Data Integrity & Security\n\n### Authentication\nAll API routes are protected by the `requireAuth` middleware. Unauthenticated requests are rejected before reaching the controller logic.\n\n### Validation\n1. **Database Constraints:**\n - Unique constraint on `CourseEnrollment` (`courseId` + `startupId`).\n - Cascade delete on `CourseMaterial`.\n2. **Business Logic:**\n - Course deletion is blocked if `enrollments.length > 0`.\n - Enrollment progress is clamped to 0-100 range.\n - Auto-completion triggers when `progress` hits 100.\n\n### Error Handling\n- **Backend:** Controllers wrap database operations in `try-catch` blocks. Errors are returned as JSON with descriptive messages.\n- **Frontend:** API client errors are caught and mapped to user-friendly toast notifications.\n\n## Usage Examples\n\n### Create a Course\n```bash\nPOST /api/learning/courses\nContent-Type: application/json\n\n{\n \"title\": \"Advanced TypeScript\",\n \"category\": \"Programming\",\n \"instructor\": \"John Doe\",\n \"duration\": \"8h 30m\",\n \"credits\": 25,\n \"level\": \"advanced\",\n \"status\": \"active\",\n \"description\": \"Deep dive into TypeScript...\"\n}\n```\n\n### List Courses with Filters\n```bash\nGET /api/learning/courses?search=typescript&status=active\n```\n\n### Create Enrollment\n```bash\nPOST /api/learning/enrollments\nContent-Type: application/json\n\n{\n \"courseId\": 1,\n \"startupId\": 1,\n \"officeId\": 1\n}\n```\n\n### Update Enrollment Progress\n```bash\nPUT /api/learning/enrollments/1\nContent-Type: application/json\n\n{\n \"progress\": 75\n}\n```\n\n## Testing\n\n### Manual Testing Checklist\n- [ ] Create a course and verify it appears in the list.\n- [ ] Attempt to delete a course with active enrollments (expect 400 error).\n- [ ] Create an enrollment and verify the unique constraint prevents duplicates.\n- [ ] Update enrollment progress to 100% and verify status changes to `COMPLETED`.\n- [ ] Search courses with debounced input.\n- [ ] Verify toast notifications appear on API errors.\n\n## Future Enhancements\n\n### Phase 2 (Recommended)\n- **File Upload:** Integrate actual file storage (e.g., AWS S3) for `CourseMaterial`.\n- **Certificates:** Generate and issue certificates upon course completion.\n- **Analytics:** Dashboard for instructor performance and enrollment completion rates.\n\n### Phase 3 (Advanced)\n- **Prerequisites:** Define required courses before enrolling.\n- **Assessments:** Add quizzes and grading logic.\n- **Recommendations:** ML-based course suggestions based on user history.\n\n## Notes for Contributors\n1. **Type Safety:** Ensure all new API endpoints have corresponding interfaces in `lib/api/learning.ts`.\n2. **Migration:** Any schema changes require a new Prisma migration (`prisma migrate dev`).\n3. **Performance:** Database queries should utilize indexes on `courseId`, `startupId`, and `status` fields.\n4. **Security:** Do not bypass `requireAuth` middleware in new routes.","harakat-admin-e2e":"# Harakat_Admin — e2e\n\n\n\n# Harakat_Admin — e2e\n\nThis module contains the end-to-end (E2E) test suite for the `Harakat_Admin` application. It validates the Admin console's UI states, route behavior, error handling, and authentication flows using Playwright.\n\nThe suite is designed to verify that the application behaves correctly under specific conditions (found records, missing records, permission denials, server outages) and that the shell and session management function as intended.\n\n## Architecture\n\nThe test harness is built around a custom fixture defined in `console.ts`. This fixture manages the browser session, injects a valid operator token, and intercepts all API calls to return deterministic responses based on the test scenario.\n\n### Test Execution Flow\n\nWhen a test runs, the `open` fixture initializes the environment. It injects the operator session into `sessionStorage` and registers a route handler that intercepts API requests.\n\n```mermaid\nsequenceDiagram\n participant Test as Test Runner\n participant Fixture as open() Fixture\n participant Browser as Playwright Browser\n participant Mock as answer() Handler\n participant Server as API Origin\n\n Test->>Fixture: open(path, scenario)\n Fixture->>Browser: addInitScript (Session)\n Fixture->>Browser: route(API_ORIGIN/**)\n Browser->>Mock: page.route()\n Mock->>Mock: fixtureFor() / answer()\n Mock-->>Browser: JSON Response (200/403/500)\n Browser->>Test: DOM / Text\n Test->>Test: Assertions\n```\n\n### Key Components\n\n#### 1. Console Fixtures (`console.ts`)\nThe core of the test suite is the `ConsoleFixtures` interface, which extends the base Playwright test.\n\n* **`open(path, scenario?)`**: The primary entry point for tests.\n * Injects the `OPERATOR` token into `sessionStorage` via `addInitScript`.\n * Registers a catch-all route handler for `${API_ORIGIN}/**`.\n * Sets the `scenario` (default: `ok`).\n * Waits for the page to render (`[data-page-frame]`, `[data-not-found]`, or `h1`).\n* **`answer(route, scenario)`**: Determines the response based on the `scenario`.\n * **`ok`**: Returns a well-shaped JSON body from `FIXTURES` (longest prefix match).\n * **`denied`**: Returns `403` with a specific message.\n * **`outage`**: Returns `500` with a specific message.\n * **`malformed`**: Returns `200` with a JSON structure that causes a client-side crash (e.g., string instead of array).\n* **`visibleText(page)`**: A helper that normalizes the page's `innerText` into a single string for assertion. This ensures tests check *what the operator sees*, not internal state.\n* **`apiCalls`**: A fixture array that logs every API request made during the test.\n\n#### 2. Test Suites\n\nThe module is divided into three main spec files, each targeting specific invariants.\n\n##### `records.spec.ts`\nValidates detail routes for specific resources (Support Tickets, Marketplace Agents, Learning Courses, Services).\n\n* **Route Existence**: Asserts that detail routes render a frame (`[data-page-frame]`) and have a heading, distinguishing them from Next.js default 404s.\n* **Absence Handling**:\n * **`get-404`**: For resources with a `get-one` route, verifies the server returns a 404 for missing IDs.\n * **`not-in-list`**: For resources without a `get-one` route (e.g., Providers), verifies the list endpoint returns a 404 for missing IDs.\n* **Error States**:\n * **Denied (403)**: Must mention permissions, never \"not found\", and never offer a \"Try again\".\n * **Outage (500)**: Must mention server failure, offer a \"Try again\", and retain the record's ID in the heading.\n * **Non-numeric ID**: Must refuse without calling the server (e.g., `/admin/support/banana`).\n* **Linking**: Verifies that list pages link to their respective detail routes.\n\n##### `routes.spec.ts`\nValidates general admin routes and the application shell.\n\n* **Rendering**: Asserts every route renders exactly one `<h1>` with the correct page name.\n* **Error Distinction**:\n * **Denied vs. Outage**: Ensures the text content differs between 403 and 500 states.\n * **Malformed**: Ensures a 200 response with invalid JSON shape (e.g., `data: \"unexpected\"`) does not cause a white screen (Next.js crash).\n* **Figures**: Asserts that counts (e.g., \"Total Companies 0\") are not displayed confidently under 403/500 states; they should render as `—`.\n* **Shell**:\n * **Unauthenticated**: Redirects to `/login` without rendering the shell frame.\n * **Navigation**: Ensures all nav links resolve to valid routes (status < 400).\n\n##### `session.spec.ts`\nValidates the authentication and logout flow.\n\n* **Server Revocation**: Verifies that clicking \"Logout\" sends a `POST /api/auth/logout` request with the `Bearer` token.\n* **Client Clearing**:\n * Clears `sessionStorage` (`harakat_admin_session`).\n * Writes a `localStorage` entry (`harakat_admin_logout_at`) to broadcast the logout to other tabs.\n* **Failure Handling**:\n * If the logout request fails (aborted), the client must still clear the session to prevent the operator from being trapped with a live credential.\n* **Cross-Tab**: Ensures the logout signal is broadcast via `localStorage` so other tabs are notified of the session end.\n\n## Critical Invariants\n\nThe test suite enforces several strict rules derived from the application's register and previous bug fixes (e.g., `VER2`, `FIX4`).\n\n1. **403 vs. 500 Distinction**: A permission denial (403) must never offer a \"Try again\" link, as retrying a denial is futile. A server outage (500) must offer a \"Try again\" link.\n2. **Empty vs. Missing**: \"No companies yet\" (empty list) must be distinguishable from \"Could not load companies\" (outage).\n3. **Logout Revocation**: Signing out must delete the session row on the server, not just clear the browser. A failed network request during logout must still clear the local session.\n4. **Route Linking**: Every list route must link to its detail routes. A route with no caller is considered incomplete.\n5. **Malformed JSON**: The application must handle a 200 response with an unexpected JSON shape gracefully (e.g., `data: \"unexpected\"` instead of `data: []`) without crashing to a white screen.\n\n## Configuration\n\nThe tests rely on the `playwright.config` file for the `API_ORIGIN` constant. This ensures that API mocking targets the correct base URL.\n\n* **`SESSION_KEY`**: `\"harakat_admin_session\"`\n* **`OPERATOR`**:\n * `token`: `\"e2e.admin.token\"`\n * `email`: `\"operator@harakat.test\"`\n * `role`: `\"super_admin\"`\n* **`MALFORMED`**: A predefined JSON object used to trigger client-side crashes in the `malformed` scenario.\n\n## Contributing\n\nWhen adding new tests:\n1. **Use `open`**: Always use the `open` fixture to handle session injection and API mocking.\n2. **Assert Visible Text**: Use `visibleText(page)` for text assertions to ensure you are checking the rendered DOM, not internal state.\n3. **Check `apiCalls`**: Use the `apiCalls` fixture to verify that specific endpoints were called or not called (e.g., non-numeric IDs should not trigger API calls).\n4. **Scenario Selection**: Use the `scenario` argument (`\"ok\"`, `\"denied\"`, `\"outage\"`, `\"malformed\"`) to control the API response without modifying the server.","harakat-admin-harakat-admin":"# Harakat_Admin — Harakat_Admin\n\n\n\n# Harakat_Admin Module Documentation\n\n## 📋 Overview\n\n**Harakat_Admin** is a Next.js 15 admin panel application that provides platform administrators with tools to manage offices, users, documents, and system settings. It serves as the central management interface for the Harakat platform, enabling admins to configure office-specific users, manage permissions, handle document workflows, and maintain system security.\n\n---\n\n## 🏗️ Architecture\n\n```mermaid\ngraph TB\n subgraph \"Frontend (Next.js 15)\"\n A[Admin Panel] --> B[Settings Page]\n A --> C[Offices Management]\n A --> D[Documents Module]\n A --> E[User Management]\n end\n \n subgraph \"API Layer\"\n B --> F[lib/api/settings.ts]\n C --> G[lib/api/offices.ts]\n D --> H[lib/api/documents.ts]\n E --> I[lib/api/admins.ts]\n end\n \n subgraph \"Backend (Express)\"\n F --> J[Settings Controller]\n G --> K[Office Controller]\n H --> L[Documents Controller]\n I --> M[Auth Controller]\n end\n \n subgraph \"Database (Prisma)\"\n J --> N[Admin Model]\n K --> O[Office Model]\n K --> P[OfficeUser Model]\n L --> Q[StartupDocumentRequest]\n M --> R[AdminOfficeAccess]\n end\n```\n\n---\n\n## 🎯 Core Features\n\n### 1. Office User Management\n\n**Purpose:** Create and manage office-specific user accounts that will login to the office platform (separate from Harakat platform admins).\n\n**Key Components:**\n\n- **Dialog:** `components/offices/Dialog/ManageAdminsDialog.tsx`\n- **Page:** `app/admin/offices/page.tsx`\n- **API Client:** `lib/api/offices.ts`\n\n**API Endpoints:**\n\n```typescript\n// Office User Management\nGET /api/offices/:id/users - Get office users\nPOST /api/offices/:id/users - Create office user\nPUT /api/offices/:id/users/:userId - Update office user\nDELETE /api/offices/:id/users/:userId - Delete office user\n\n// Platform Admin Access (Separate)\nGET /api/offices/:id/admins - Get platform admins\nPOST /api/offices/:id/admins - Assign platform admin\nPUT /api/offices/:id/admins/:adminId - Update platform admin\nDELETE /api/offices/:id/admins/:adminId - Remove platform admin\n```\n\n**Database Models:**\n\n```prisma\nenum OfficeUserRole {\n OWNER\n ADMIN\n MANAGER\n STAFF\n}\n\nmodel OfficeUser {\n id Int @id @default(autoincrement())\n officeId Int\n name String\n email String @unique\n passwordHash String\n role OfficeUserRole @default(STAFF)\n isActive Boolean @default(true)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n office Office @relation(fields: [officeId], references: [id], onDelete: Cascade)\n sessions OfficeUserSession[]\n}\n\nmodel OfficeUserSession {\n id Int @id @default(autoincrement())\n token String @unique\n expiresAt DateTime\n officeUserId Int\n officeUser OfficeUser @relation(fields: [officeUserId], references: [id], onDelete: Cascade)\n}\n```\n\n**Usage Example:**\n\n```typescript\n// Create an office user\nconst response = await officesApi.addOfficeUser(officeId, {\n name: \"John Doe\",\n email: \"john@office.com\",\n password: \"securePassword123\",\n role: \"ADMIN\",\n});\n\n// Update office user\nawait officesApi.updateOfficeUser(officeId, userId, {\n name: \"John Smith\",\n role: \"MANAGER\",\n});\n\n// Remove office user\nawait officesApi.removeOfficeUser(officeId, userId);\n```\n\n---\n\n### 2. Access Account (Impersonation) Feature\n\n**Purpose:** Allow platform admins to login as office users for troubleshooting and support.\n\n**Implementation Status:** UI ready, backend pending office platform integration.\n\n**UI Component:**\n\n```tsx\n// Button added to Manage Office Users dialog\n<Button\n variant=\"outline\"\n className=\"w-full sm:w-auto\"\n onClick={() => handleAccessAccount(user, officeName)}\n>\n <ExternalLink className=\"w-4 h-4 mr-2\" />\n <span className=\"hidden sm:inline\">Access Account<\/span>\n <span className=\"sm:hidden\">Access<\/span>\n<\/Button>\n```\n\n**Future Backend Endpoint:**\n\n```typescript\nPOST /api/offices/:officeId/users/:userId/impersonate\n\nResponse:\n{\n \"ok\": true,\n \"impersonationToken\": \"jwt_token_here\",\n \"officeUser\": {\n \"id\": 1,\n \"email\": \"user@office.com\",\n \"name\": \"John Doe\"\n }\n}\n```\n\n**Security Considerations:**\n\n- Only SUPER_ADMIN and ADMIN roles can impersonate\n- Tokens expire after 1 hour\n- Audit logging for all impersonation attempts\n- Impersonated sessions cannot perform sensitive actions\n\n---\n\n### 3. Documents Module\n\n**Purpose:** Complete modularization of document management with AI action plans, reviews, and workspace tracking.\n\n**Component Structure:**\n\n```\ncomponents/documents/\n├── content/\n│ ├── StartupRequestsContent.tsx\n│ ├── ActionPlansContent.tsx\n│ ├── ResultsContent.tsx\n│ └── WorkspaceContent.tsx\n└── dialog/\n ├── ReviewRequestDialog.tsx\n ├── ReviewActionPlanDialog.tsx\n └── ResultDocumentDialog.tsx\n```\n\n**API Endpoints:**\n\n```typescript\n// Startup Requests\nGET /api/documents/requests\nGET /api/documents/requests/:id\nPOST /api/documents/requests\nPUT /api/documents/requests/:id\nDELETE /api/documents/requests/:id\n\n// AI Action Plans\nGET /api/documents/action-plans\nGET /api/documents/action-plans/:id\nGET /api/documents/action-plans/request/:requestId\nPUT /api/documents/action-plans/:id\nPOST /api/documents/action-plans/generate\n\n// Workspace Usage\nGET /api/documents/workspace\nGET /api/documents/workspace/:startupId\nPUT /api/documents/workspace/:startupId\n\n// Document Files\nPOST /api/documents/files/upload\nGET /api/documents/files/request/:requestId\nDELETE /api/documents/files/:fileId\n```\n\n**Database Schema:**\n\n```prisma\nmodel StartupDocumentRequest {\n id Int @id @default(autoincrement())\n startupId Int\n officeId Int\n requestedService String\n context String @db.Text\n status DocumentRequestStatus @default(PENDING)\n files Json?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n startup Startup @relation(fields: [startupId], references: [id])\n office Office @relation(\"StartupDocuments\", fields: [officeId], references: [id])\n actionPlans AIActionPlan[]\n reviews DocumentReview[]\n documentFiles DocumentFile[]\n}\n\nmodel AIActionPlan {\n id Int @id @default(autoincrement())\n requestId Int\n content String @db.Text\n confidence Float @default(0)\n recommendations Int @default(0)\n status ActionPlanStatus @default(PENDING_REVIEW)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n request StartupDocumentRequest @relation(fields: [requestId], references: [id])\n}\n\nmodel DocumentReview {\n id Int @id @default(autoincrement())\n requestId Int\n reviewerId Int\n decision ReviewDecision\n feedback String? @db.Text\n expectedOutputs String? @db.Text\n notes String? @db.Text\n expectedResult String? @db.Text\n eta String?\n createdAt DateTime @default(now())\n request StartupDocumentRequest @relation(fields: [requestId], references: [id])\n reviewer Admin @relation(fields: [reviewerId], references: [id])\n}\n```\n\n**Data Flow:**\n\n```mermaid\nsequenceDiagram\n Admin->>Frontend: Review Request Dialog\n Frontend->>Backend: POST /api/documents/requests\n Backend->>Backend: Create Request + Initialize Workspace\n Backend->>Database: Save StartupDocumentRequest\n Backend->>Database: Create WorkspaceUsage\n Admin->>Frontend: Review Action Plan Dialog\n Frontend->>Backend: POST /api/documents/action-plans/generate\n Backend->>Backend: Generate AI Plan (Mock)\n Backend->>Database: Save AIActionPlan\n Admin->>Frontend: Result Document Dialog\n Frontend->>Backend: PUT /api/documents/action-plans/:id\n Backend->>Database: Update Request Status\n Backend->>Database: Create DocumentReview\n```\n\n---\n\n### 4. Settings Management\n\n**Purpose:** Centralized configuration for admin preferences, security, and system settings.\n\n**Tabs Structure:**\n\n```\nSettings Page (app/admin/settings/page.tsx)\n├── Profile\n├── Notifications\n├── Security\n├── Platform\n├── Users (Super Admin)\n├── Permissions (Super Admin)\n├── Backup & Audit (Super Admin)\n└── [Removed] Integrations (Super Admin)\n```\n\n**Security Features:**\n\n**Password Change with Email Verification:**\n\n```mermaid\nsequenceDiagram\n User->>Frontend: Enter current + new password\n User->>Frontend: Click \"Update Password\"\n Frontend->>Backend: POST /api/auth/request-password-change\n Backend->>Backend: Validate current password\n Backend->>Backend: Generate 6-digit code\n Backend->>Database: Save code + expiry (10 min)\n Backend->>Email: Send verification email\n Backend->>Frontend: Return tempToken\n Frontend->>Frontend: Show verification UI\n User->>Email: Check inbox\n User->>Frontend: Enter 6-digit code\n Frontend->>Backend: POST /api/auth/verify-password-change\n Backend->>Backend: Validate code + expiry\n Backend->>Backend: Hash new password\n Backend->>Database: Update password\n Backend->>Database: Clear code fields\n Backend->>Frontend: Return success\n Frontend->>Frontend: Hide verification UI\n```\n\n**Database Fields:**\n\n```prisma\nmodel Admin {\n // Two-Factor Authentication\n twoFactorEnabled Boolean @default(false)\n twoFactorCode String? @db.VarChar(6)\n twoFactorExpiry DateTime?\n\n // Password Change Verification\n passwordChangeCode String? @db.VarChar(6)\n passwordChangeExpiry DateTime?\n}\n```\n\n**Permissions Management:**\n\n```typescript\n// PermissionsData interface\ninterface PermissionsData {\n super_admin: Record<string, boolean>; // Not editable in UI\n admin: Record<string, boolean>;\n}\n\n// Features managed\nconst features = [\n \"Offices\",\n \"Marketplace\",\n \"Virtual Assistant\",\n \"Settings\",\n \"Credits\",\n \"Analytics\"\n];\n```\n\n---\n\n### 5. User Management with Error Handling\n\n**Purpose:** Create and manage admin users with comprehensive error handling.\n\n**Error Handling Flow:**\n\n```typescript\n// Frontend validation\nconst emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\nif (!emailRegex.test(admin.email)) {\n toast.error(\"Invalid email format. Please enter a valid email address.\");\n return;\n}\n\n// Backend validation\ntry {\n await adminsApi.createAdmin({\n name: \"John Doe\",\n email: \"john@office.com\",\n password: \"securePassword123\",\n });\n} catch (error) {\n if (error.message.includes(\"already exists\")) {\n toast.error(\"An admin with this email address already exists. Please use a different email.\");\n }\n throw error;\n}\n```\n\n**Toast Notifications:**\n\n| Error Type | Message |\n|------------|---------|\n| Invalid email format | \"Invalid email format. Please enter a valid email address.\" |\n| Duplicate email | \"An admin with this email address already exists. Please use a different email.\" |\n| Password error | \"Password must be at least 8 characters.\" |\n| Generic error | \"Failed to create admin. Please try again.\" |\n\n---\n\n### 6. URL Validation (Portfolio Links)\n\n**Purpose:** Validate portfolio URLs for freelancers and companies.\n\n**Validation Logic:**\n\n```typescript\n// Frontend validation\nconst isValidUrl = (url: string): boolean => {\n if (!url) return true; // Optional field\n try {\n new URL(url);\n return true;\n } catch {\n return false;\n }\n};\n\n// Backend validation\nconst isValidUrl = (url: string): boolean => {\n if (!url) return true;\n const urlPattern = /^(https?:\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([\\/\\w \\.-]*)*\\/?$/;\n return urlPattern.test(url);\n};\n```\n\n**Error Response:**\n\n```json\n{\n \"ok\": false,\n \"error\": \"Invalid portfolio URL. Please provide a valid HTTP or HTTPS URL.\"\n}\n```\n\n---\n\n## 🔐 Security Architecture\n\n### Authentication & Authorization\n\n```mermaid\ngraph LR\n A[Login] --> B{Role Check}\n B -->|SUPER_ADMIN| C[Full Access]\n B -->|ADMIN| D[Permission Check]\n D --> E[Feature Toggle]\n E -->|Enabled| F[Access Granted]\n E -->|Disabled| G[Access Denied]\n```\n\n**Middleware:**\n\n```typescript\n// All protected routes\nrouter.use(\"/api/*\", verifyAdmin);\n\n// Permission checks\nconst hasPermission = (role: AdminRole, feature: string): boolean => {\n if (role === \"SUPER_ADMIN\") return true;\n return permissions.admin[feature] || false;\n};\n```\n\n### Password Security\n\n- **Hashing:** Bcrypt with 10 salt rounds\n- **Minimum Length:** 8 characters\n- **Verification:** Email-based 6-digit code (10-minute expiry)\n- **Storage:** Passwords never returned in API responses\n\n---\n\n## 📁 File Structure\n\n```\nHarakat_Admin/\n├── app/\n│ ├── admin/\n│ │ ├── settings/\n│ │ │ └── page.tsx\n│ │ ├── offices/\n│ │ │ └── page.tsx\n│ │ └── layout.tsx\n│ └── login/\n│ └── page.tsx\n├── components/\n│ ├── offices/\n│ │ └── Dialog/\n│ │ └── ManageAdminsDialog.tsx\n│ ├── settings/\n│ │ └── tabs/\n│ │ ├── SecurityTab.tsx\n│ │ ├── PermissionsTab.tsx\n│ │ └── UsersTab.tsx\n│ └── documents/\n│ ├── content/\n│ └── dialog/\n├── lib/\n│ └── api/\n│ ├── offices.ts\n│ ├── documents.ts\n│ ├── auth.ts\n│ └── admins.ts\n├── types/\n│ └── settings.ts\n└── Dockerfile\n```\n\n---\n\n## 🚀 Deployment\n\n### Docker Configuration\n\n```dockerfile\n# Multi-stage build\nFROM node:20-alpine AS base\nRUN corepack enable && corepack prepare pnpm@latest --activate\n\nFROM base AS deps\nWORKDIR /app\nCOPY package.json pnpm-lock.yaml ./\nRUN pnpm install --frozen-lockfile\n\nFROM base AS build\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY . .\nCOPY .env ./\nRUN pnpm build\n\nFROM node:20-alpine AS runner\nWORKDIR /app\nENV NODE_ENV=production\nRUN addgroup --system --gid 1001 nodejs && \\\n adduser --system --uid 1001 nextjs\n\nCOPY --from=build /app/public ./public\nCOPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./\nCOPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static\n\nUSER nextjs\nEXPOSE 3000\nENV PORT=3000\nENV HOSTNAME=\"0.0.0.0\"\n\nCMD [\"node\", \"server.js\"]\n```\n\n### Environment Variables\n\n```env\n# Database\nDATABASE_URL=\"postgresql://...\"\n\n# API\nNEXT_PUBLIC_API_URL=\"http://localhost:5000\"\n\n# Email (for password verification)\nSMTP_HOST=smtp.gmail.com\nSMTP_PORT=587\nSMTP_USER=your-email@gmail.com\nSMTP_PASS=your-app-specific-password\n\n# JWT\nJWT_SECRET=your-secret-key-here\n\n# Office Platform\nNEXT_PUBLIC_OFFICE_PLATFORM_URL=http://localhost:3001\n```\n\n---\n\n## 🧪 Testing Checklist\n\n### Office User Management\n\n- [ ] Can create office user\n- [ ] Office user appears in database\n- [ ] Email uniqueness enforced\n- [ ] Can update office user details\n- [ ] Can change office user role\n- [ ] Can delete office user\n- [ ] Office user deleted when office deleted\n\n### Documents Module\n\n- [ ] StartupRequestsContent renders with mock data\n- [ ] Search functionality filters correctly\n- [ ] Status badges display correct colors\n- [ ] Review button calls onReviewRequest callback\n- [ ] ActionPlansContent shows confidence/recommendations\n- [ ] ResultsContent filters to completed only\n- [ ] WorkspaceContent calculates stats correctly\n- [ ] All dialogs open/close properly\n\n### Security Features\n\n- [ ] Password change sends verification email\n- [ ] Email contains 6-digit code\n- [ ] Code verification succeeds with correct code\n- [ ] New password works on next login\n- [ ] Error handling works for all edge cases\n- [ ] UI state management works correctly\n\n---\n\n## 📊 Performance Considerations\n\n### Database Indexes\n\nAll critical queries indexed:\n\n```prisma\n@@index([officeId])\n@@index([email])\n@@index([status])\n@@index([createdAt])\n@@index([requestId])\n@@index([reviewerId])\n```\n\n### API Response Times\n\n| Endpoint | Expected Time |\n|----------|---------------|\n| GET /api/offices/:id/users | < 100ms |\n| POST /api/documents/requests | < 200ms |\n| POST /api/auth/request-password-change | 5-20s (email sending) |\n| POST /api/auth/verify-password-change | < 100ms |\n\n---\n\n## 🔄 Integration Points\n\n### With HarakatOffices_Server\n\n- **Office Controller:** `src/modules/offices/offices.controller.ts`\n- **Auth Controller:** `src/modules/admin/auth.controller.ts`\n- **Documents Controller:** `src/modules/documents/`\n- **Email Service:** `src/core/services/email.service.ts`\n\n### With Office Platform\n\n- **Impersonation:** `office-platform/app/auth/impersonate/page.tsx`\n- **Authentication:** `office-platform/src/middleware/authenticateOfficeUser.ts`\n\n---\n\n## 🎯 Key Design Decisions\n\n### 1. Office Users vs Platform Admins\n\n**Separation Rationale:**\n\n- Office users login to office platform (not `/admin`)\n- Platform admins manage the Harakat platform\n- Different authentication systems\n- Different permission models\n\n### 2. Email Verification for Password Changes\n\n**Security Benefits:**\n\n- Prevents unauthorized password changes\n- Adds second factor of authentication\n- Clear audit trail\n- User confirmation required\n\n### 3. Responsive Design (Mobile-First)\n\n**Breakpoints:**\n\n- Mobile (< 640px): Single column, full-width buttons\n- Tablet (640-1024px): 2-column grids\n- Desktop (> 1024px): Optimized layouts\n\n---\n\n## 📚 Related Documentation\n\n- [Documents Module Implementation](./DOCUMENTS_IMPLEMENTATION.md)\n- [Password Change Verification](./PASSWORD_CHANGE_IMPLEMENTATION_COMPLETE.md)\n- [Office Admin Management](./IMPLEMENTATION_COMPLETE.md)\n- [Responsive Design Improvements](./RESPONSIVE_DESIGN_IMPROVEMENTS.md)\n- [Access Account Feature](./ACCESS_ACCOUNT_FEATURE.md)\n\n---\n\n## 🚧 Future Enhancements\n\n1. **Session Recording:** Record actions during impersonation\n2. **Bulk Operations:** Select multiple users for bulk actions\n3. **Advanced Search:** Filter users by role, office, status\n4. **Audit Trail:** Comprehensive logging of all admin actions\n5. **Two-Factor Confirmation:** Require 2FA before impersonation\n6. **Permission Presets:** Pre-configured permission templates\n\n---\n\n## ✅ Summary\n\n**Harakat_Admin** provides a comprehensive admin panel with:\n\n- ✅ **Office User Management:** Create, update, and manage office-specific users\n- ✅ **Documents Module:** AI-powered document review workflow\n- ✅ **Security Features:** Email verification, 2FA, password policies\n- ✅ **Responsive Design:** Mobile-first approach across all components\n- ✅ **Error Handling:** Comprehensive toast notifications and validation\n- ✅ **API Integration:** Full backend integration with HarakatOffices_Server\n\nThe module is production-ready with proper security measures, error handling, and a clean, responsive UI.","harakat-admin-hooks":"# Harakat_Admin — hooks\n\n\n\n# Harakat_Admin — Hooks\n\nThis module encapsulates shared React hooks for the Harakat Admin interface. It centralizes logic for responsive design, authentication lifecycle management, robust data fetching with race condition prevention, and notification handling.\n\n## Overview\n\nThe hooks in this module address common architectural challenges in the admin dashboard:\n* **Race Conditions:** `useResource` prevents stale data overwrites during rapid state updates.\n* **Session Consistency:** `useAuthCheck` ensures cross-tab synchronization for logout events.\n* **UI State:** `use-toast` and `useIsMobile` provide standardized UI feedback and layout detection.\n\n## Data Fetching: `useResource`\n\nThe `useResource` hook is the primary mechanism for fetching data in the admin interface. It was refactored to solve race conditions where multiple rapid fetches could overwrite newer data with older responses.\n\n### Implementation Details\n\n* **Generation Counter:** Every fetch run increments a `generation` ref. Before updating state, the hook checks if the current `generation` matches the one that initiated the fetch. If not, the response is discarded.\n* **Abort Control:** Uses `AbortController` to cancel in-flight requests when dependencies change.\n* **Stale State:** When `loading` is true but `data` exists, the `stale` flag is set. This allows the UI to render existing data with a visual indicator rather than blanking the screen during a refresh.\n* **Debouncing:** Supports `debounceMs` to throttle fetches based on dependency changes.\n\n### API\n\n```typescript\nexport function useResource<T>(\n fetcher: (signal: AbortSignal) => Promise<T>,\n deps: unknown[],\n options: UseResourceOptions = {}\n): ResourceState<T>\n```\n\n**Returns:** `ResourceState<T>`\n* `data`: The fetched data.\n* `error`: Error object if the fetch failed.\n* `status`: HTTP status code if the error is an `ApiError`.\n* `loading`: Boolean indicating if a request is in flight.\n* `stale`: Boolean indicating if the current data is being refreshed.\n* `reload`: Function to trigger a new fetch.\n* `setData`: Function to update local state without a network call.\n\n### Execution Flow\n\nThe hook ensures that only the most recent request can update state.\n\n```mermaid\nsequenceDiagram\n participant Hook as useResource Hook\n participant Fetch as fetcher()\n participant State as React State\n\n Hook->>Hook: run()\n Hook->>Hook: generation++\n Hook->>Fetch: fetch(signal)\n Fetch-->>Hook: Response\n Hook->>Hook: Check generation\n alt Generation Match\n Hook->>State: setData(result)\n else Generation Mismatch\n Hook->>Hook: Ignore result\n end\n```\n\n### Integration Points\n\n`useResource` is the data layer for the following components:\n* **MarketplaceAgentPage** (`marketplace/[id]/page.tsx`)\n* **CoursePage** (`learning-hub/[id]/page.tsx`)\n* **CompaniesPage** (`admin/companies/page.tsx`)\n* **AdminDashboard** (`app/admin/page.tsx`)\n* **LearningHub** (`admin/learning-hub/page.tsx`)\n* **ShadowPanel** (`components/governance/shadow-panel.tsx`)\n* **GovernanceConsole** (`components/governance/governance-console.tsx`)\n* **PromptInspector** (`components/governance/prompt-inspector.tsx`)\n* **RevisionHistory** (`components/governance/revision-history.tsx`)\n* **UsersTab** (`settings/tabs/UsersTab.tsx`)\n* **TenantsView** (`components/packs/packs-console.tsx`)\n* **VesselTemplatesPage** (`admin/vessel-templates/page.tsx`)\n* **SupportTicketPage** (`support/[id]/page.tsx`)\n* **CompanyDetailPage** (`companies/[id]/page.tsx`)\n* **ServicesManagementPage** (`admin/services/page.tsx`)\n* **MarketplaceManagementPage** (`admin/marketplace/page.tsx`)\n* **ServiceProviderPage** (`services/[id]/page.tsx`)\n* **PacksConsole** (`components/packs/packs-console.tsx`)\n* **SupportPage** (`admin/support/page.tsx`)\n* **CreditsTab** (`settings/tabs/CreditsTab.tsx`)\n* **MarketplaceAgentPage** (`marketplace/[id]/page.tsx`)\n\n## Authentication: `useAuthCheck`\n\nThis hook manages the authentication lifecycle, specifically handling token expiration and cross-tab session synchronization.\n\n### Key Functions\n\n* **`markLoginSuccess()`**: Resets the expiration timer and flags when a user logs in. Should be called immediately after a successful login or 2FA verification.\n* **`handleTokenExpiration()`**: Triggered on HTTP 401 errors.\n * **Grace Period:** Ignores expiration triggers within 5 seconds of login (`LOGIN_GRACE_PERIOD`) to prevent false positives.\n * **Single Execution:** Uses a global flag (`isHandlingExpiration`) to ensure the logout flow runs only once per session expiry.\n * **Cross-Tab Sync:** Calls `endSession()` to broadcast a logout event via `localStorage` (`LOGOUT_BROADCAST_KEY`), ensuring all tabs sign out simultaneously.\n* **`useAuthCheck()`**: Sets up a `storage` event listener to detect logouts initiated in other tabs.\n\n### Cross-Tab Synchronization\n\nThe hook listens for `StorageEvent` changes. Because `storage` events only fire for `localStorage` changes across tabs, `endSession()` writes a specific key (`LOGOUT_BROADCAST_KEY`) to `localStorage` to trigger the listener in other tabs.\n\n### Integration Points\n\n* **AdminLayout** (`app/admin/layout.tsx`): Wraps the admin interface to ensure auth checks are active.\n* **handleLogin** (`app/login/page.tsx`): Calls `markLoginSuccess` upon successful login.\n* **handleVerify2FA** (`app/login/page.tsx`): Calls `markLoginSuccess` upon 2FA verification.\n\n## Notifications: `use-toast`\n\nA custom toast notification system inspired by `react-hot-toast`. It manages a singleton state of toasts using a reducer pattern.\n\n### Configuration\n\n* **`TOAST_LIMIT`**: Set to `1`. Only the most recent toast is displayed.\n* **`TOAST_REMOVE_DELAY`**: `1000000` ms (1000 seconds). Default timeout for auto-dismissal.\n\n### API\n\n```typescript\nexport function useToast()\nexport function toast(props: Toast)\n```\n\n**`useToast()`** returns:\n* `state`: Current toast state.\n* `toast`: Function to create a new toast.\n* `dismiss`: Function to dismiss a toast by ID.\n\n**`toast()`** returns:\n* `id`: Unique identifier for the toast.\n* `dismiss`: Function to dismiss the specific toast.\n* `update`: Function to update the toast properties.\n\n### Architecture\n\nThe hook uses a global `memoryState` and a `listeners` array. When `toast` is called, it dispatches an action to the reducer, updates `memoryState`, and notifies all listeners. This pattern allows multiple components to subscribe to the toast state without direct state coupling.\n\n### Integration Points\n\n* **useAuthCheck**: Uses `react-hot-toast` directly for specific auth errors, but `use-toast` is available for general UI feedback.\n\n## Utilities: `useIsMobile`\n\nA simple hook for detecting mobile devices based on a viewport breakpoint.\n\n### Configuration\n\n* **`MOBILE_BREAKPOINT`**: `768` px.\n\n### API\n\n```typescript\nexport function useIsMobile()\n```\n\n**Returns:** `boolean`\n* `true` if `window.innerWidth < 768`.\n* `false` otherwise.\n\n### Implementation\n\nUses `window.matchMedia` to listen for viewport changes. It initializes the state immediately and updates on `change` events.\n\n## Module Dependencies\n\n* **React**: `useState`, `useEffect`, `useCallback`, `useRef`.\n* **Next.js**: `useRouter`.\n* **External**: `react-hot-toast` (used in `useAuthCheck`), `@/lib/auth/session` (used in `useAuthCheck`), `@/lib/api/errors` (used in `useResource`).","harakat-admin-lib":"# Harakat_Admin — lib\n\n\n\n# Harakat_Admin — lib Module Documentation\n\n## Overview\n\nThe `Harakat_Admin/lib` module provides the complete API client infrastructure for the Harakat Admin application. It implements a centralized, type-safe REST client layer that abstracts HTTP communication, session management, and data validation across all admin functionality.\n\nThis module serves as the bridge between the React frontend and the backend API, ensuring consistent error handling, authentication, and data shape validation throughout the application.\n\n## Core Architecture\n\n### Centralized Request Layer\n\nAll API calls flow through a single request mechanism in `lib/api/client.ts`. This design ensures:\n\n- **Unified authentication**: All requests automatically include the access token\n- **Consistent error handling**: All failures are wrapped in `ApiError` with status codes\n- **Session renewal**: Automatic token refresh on 401 responses (except sign-in endpoints)\n- **Abort support**: All requests accept `AbortSignal` for cancellation\n\n```mermaid\ngraph TD\n A[Component] --> B[API Client]\n B --> C[Request Handler]\n C --> D{Status Check}\n D -->|401| E[Renew Token]\n D -->|200| F[Parse Response]\n D -->|Error| G[ApiError]\n E --> F\n F --> H[Return to Component]\n```\n\n### Key Design Principles\n\n1. **Fail Fast on Malformed Data**: Every read parses its response before it becomes React state. A 200 with wrong shape is treated as a failure, not an empty list.\n\n2. **Single Source of Truth for Session**: The access token is the session. Stored in `sessionStorage` with identity, cleared together on logout.\n\n3. **Exhaustive Type Coverage**: Unknown governance kinds, pack member actions, and exclusion reasons are hard errors — never silent skips.\n\n## API Client Infrastructure\n\n### `lib/api/client.ts`\n\nThe core HTTP client that all other API modules depend on.\n\n#### Request Flow\n\n```typescript\nexport async function request<T>(\n path: string,\n options: RequestOptions = {}\n): Promise<T>\n```\n\n**Key behaviors:**\n- Attaches Bearer token from `getAccessToken()` to all requests\n- Handles 401 responses with automatic token renewal (except `/auth/*` endpoints)\n- Wraps failures in `ApiError` with status code and message\n- Supports `AbortSignal` for cancellation\n\n#### `apiClient` Object\n\n```typescript\nexport const apiClient = {\n get: <T>(path: string, options: RequestOptions = {}) => request<T>(path, options),\n post: <T>(path: string, body: any, options: RequestOptions = {}) => \n request<T>(path, { ...options, method: \"POST\", body: JSON.stringify(body) }),\n put: <T>(path: string, body: any, options: RequestOptions = {}) => \n request<T>(path, { ...options, method: \"PUT\", body: JSON.stringify(body) }),\n delete: <T>(path: string, options: RequestOptions = {}) => \n request<T>(path, { ...options, method: \"DELETE\" }),\n};\n```\n\n### `lib/api/errors.ts`\n\nStandardized error handling with status-based classification.\n\n#### `ApiError` Class\n\n```typescript\nexport class ApiError extends Error {\n readonly status: number;\n readonly body: unknown;\n readonly code?: string;\n \n // Convenience getters\n get isNetwork() { return this.status === 0; }\n get isDenied() { return this.status === 403; }\n get isExpiredSession() { return this.status === 401; }\n get isNotFound() { return this.status === 404; }\n get isServerFault() { return this.status >= 500; }\n}\n```\n\n#### `describeError()`\n\nConverts `ApiError` to user-facing messages:\n\n```typescript\ndescribeError(e: unknown, subject?: string): string\n```\n\n**Examples:**\n- 401 → \"Your session ended. Sign in again to continue.\"\n- 403 → \"You don't have permission to do that.\"\n- 404 → \"That record no longer exists — someone may have removed it.\"\n- 500 → \"The server failed while trying to do that.\"\n\n## Authentication & Session Management\n\n### `lib/auth/session.ts`\n\nSingle source of truth for admin authentication.\n\n#### Session Storage\n\n```typescript\nconst SESSION_KEY = \"harakat_admin_session\";\n```\n\nSession object:\n```typescript\ninterface AdminSession {\n token: string;\n email: string;\n role: AdminSessionRole; // \"super_admin\" | \"admin\" | \"manager\"\n}\n```\n\n#### Key Functions\n\n| Function | Purpose |\n|----------|---------|\n| `getSession()` | Read current session from sessionStorage |\n| `getAccessToken()` | Get the Bearer token (only token reader) |\n| `isAuthenticated()` | Check if session exists |\n| `startSession()` | Initialize session after login |\n| `renewSession()` | Refresh token using httpOnly cookie |\n| `signOut()` | Server-side logout + client cleanup |\n| `endSession()` | Clear local session data |\n\n#### Session Renewal Logic\n\n```mermaid\nsequenceDiagram\n participant Client\n participant Server\n Client->>Server: POST /api/auth/refresh\n Server-->>Client: 200 { accessToken }\n Client->>Client: startSession(newToken)\n Client->>Client: Continue with requests\n```\n\n**Important:** Renewal sends NO Authorization header — it uses the httpOnly refresh cookie.\n\n## Data Parsing & Validation\n\n### `lib/api/shape.ts`\n\nCentralized parsing layer that prevents crashes from malformed server responses.\n\n#### The `parse()` Function\n\n```typescript\nexport function parse<T>(body: unknown, read: Reader<T>): T\n```\n\nEvery API read uses this pattern:\n```typescript\nreturn parse(await request(url), readScopeReport)\n```\n\n#### Reader Functions\n\n| Function | Type | Description |\n|----------|------|-------------|\n| `str()` | `Reader<string>` | Validates string type |\n| `num()` | `Reader<number>` | Validates finite number |\n| `bool()` | `Reader<boolean>` | Validates boolean |\n| `obj()` | `Reader<Record<string, unknown>>` | Validates non-array object |\n| `arrayOf()` | `Reader<T[]>` | Validates array and maps readers |\n| `literal()` | `Reader<T>` | Validates against closed set |\n| `nullable()` | `Reader<T \\| null>` | Allows null/undefined |\n| `withDefault()` | `Reader<T>` | Provides fallback for undefined |\n| `field()` | `Reader<T>` | Reads object field with path |\n| `readList()` | `Reader<T[]>` | Reads array from envelope |\n\n#### `MalformedPayloadError`\n\nThrown when a response cannot be parsed. Contains the exact path that failed:\n```typescript\nnew MalformedPayloadError(\"entries[2].kind\")\n```\n\nThis is rendered as an error with retry, not as an empty list.\n\n## Domain-Specific APIs\n\n### Admin Management (`lib/api/admin.ts`)\n\n```typescript\nexport const adminsApi = {\n list: async () => apiClient.get(\"/api/admin\"),\n getCoverage: async (signal?) => parse(await apiClient.get(\"/api/admin/coverage\"), readCoverage),\n createAdmin: async (data) => apiClient.post(\"/api/admin\", data),\n updateAdmin: async (adminId, data) => apiClient.put(`/api/admin/${adminId}`, data),\n deleteAdmin: async (adminId) => apiClient.delete(`/api/admin/${adminId}`),\n updateProfile: async (data) => apiClient.put(\"/api/admin/profile\", data),\n getAdminCredits: async (signal?) => apiClient.get(\"/api/admin/credits\", { signal }),\n updateAdminCreditLimit: async (adminId, creditLimit, resetTransactions?) => \n apiClient.put(`/api/admin/${adminId}/credit-limit`, { creditLimit, resetTransactions }),\n getAdminCreditUsage: async () => apiClient.get(\"/api/admin/credit-usage\"),\n};\n```\n\n### Startup/Tenant Management (`lib/api/startups.ts`)\n\n```typescript\nexport const startupsApi = {\n list: (signal?) => apiClient.get<ListStartupsResponse>(\"/api/admin/startups\", { signal }),\n get: async (id, signal?) => {\n const res = await apiClient.get<ListStartupsResponse & { page?: { hasMore?: boolean } }>(\n \"/api/admin/startups\", { signal }\n );\n const rows = Array.isArray(res.startups) ? res.startups : [];\n const startup = rows.find((s) => s.id === id);\n if (startup) return { found: true, startup };\n return { found: false, inconclusive: res.page?.hasMore === true };\n },\n provision: (data) => apiClient.post<ProvisionCompanyResponse>(\"/api/admin/startups\", data),\n applyProfile: async (id, orgType) => {\n const res = await apiClient.post<AppliedProfile | { profile: AppliedProfile }>(\n `/api/admin/startups/${id}/apply-profile`, { orgType }\n );\n return (res as { profile?: AppliedProfile }).profile ?? (res as AppliedProfile);\n },\n suspend: (id) => apiClient.post<LifecycleResponse>(`/api/admin/startups/${id}/suspend`, {}),\n reactivate: (id) => apiClient.post<LifecycleResponse>(\n `/api/admin/startups/${id}/reactivate`, {}\n ),\n tenantProfiles: (signal?) => apiClient.get<TenantProfilesResponse>(\"/api/admin/tenant-profiles\", { signal }),\n};\n```\n\n### Governance Console (`lib/api/governance.ts`)\n\n```typescript\nexport async function fetchManifest(signal?) {\n const res = await request<{ ok: boolean; resources: GovernanceManifestEntry[] }>(\n `${BASE}/resources`, { signal }\n );\n return res.resources ?? [];\n}\n\nexport async function fetchScopes(signal?) {\n return parse(await request<unknown>(`${BASE}/scopes`, { signal }), readScopes);\n}\n\nexport async function fetchScopeReport(scope, signal?) {\n return parse(\n await request<unknown>(`${BASE}/scope/${encodeURIComponent(scope)}`, { signal }),\n readScopeReport\n );\n}\n\nexport async function setResource(scope, resource, body) {\n return request<{ version: number }>(\n `${BASE}/scope/${encodeURIComponent(scope)}/${encodeURIComponent(resource)}`,\n { method: \"PUT\", body: JSON.stringify(body) }\n );\n}\n\nexport async function fetchPrompts(opts, signal?) {\n const params = new URLSearchParams();\n if (opts.startupId != null) params.set(\"startupId\", String(opts.startupId));\n if (opts.cursor) params.set(\"cursor\", opts.cursor);\n if (opts.limit) params.set(\"limit\", String(opts.limit));\n return parse(await request<unknown>(`${BASE}/prompts${params.toString()}`, { signal }), readPromptPage);\n}\n```\n\n### Pack Management (`lib/api/packs.ts`)\n\n```typescript\nexport async function fetchSources(signal?) {\n const res = await request<{ ok: boolean; sources: PackSource[] }>(BASE, { signal });\n return Array.isArray(res.sources) ? res.sources : [];\n}\n\nexport async function fetchTenantPacks(startupId, signal?) {\n return request<TenantPacksView>(`${BASE}/tenants/${startupId}`, { signal });\n}\n\nexport async function planPack(startupId, sourceId, selected, signal?) {\n return request<PlanResponse>(\n `${BASE}/tenants/${startupId}/${sourceId}/plan`,\n { method: \"POST\", body: JSON.stringify({ selected }), signal }\n );\n}\n\nexport async function installPack(startupId, sourceId, selected) {\n return request<InstallResponse>(\n `${BASE}/tenants/${startupId}/${sourceId}/install`,\n { method: \"POST\", body: JSON.stringify({ selected }) }\n );\n}\n\nexport async function fetchUninstallPreview(startupId, sourceKey, signal?) {\n const res = await request<{ ok: boolean; preview: UninstallPreview }>(\n `${BASE}/tenants/${startupId}/installed/${encodeURIComponent(sourceKey)}/uninstall-preview`,\n { signal }\n );\n return res.preview;\n}\n```\n\n### Learning Hub (`lib/api/learning.ts`)\n\n```typescript\nexport const coursesApi = {\n list: async (filters?, signal?) => {\n const params = new URLSearchParams();\n if (filters?.status) params.append(\"status\", filters.status);\n if (filters?.category) params.append(\"category\", filters.category);\n if (filters?.instructor) params.append(\"instructor\", filters.instructor);\n if (filters?.search) params.append(\"search\", filters.search);\n return apiClient.get<{ courses: Course[] }>(\n `/api/learning/courses${params.toString()}`, { signal }\n );\n },\n getById: async (id, signal?) => apiClient.get<{ course: Course }>(`/api/learning/courses/${id}`, { signal }),\n create: async (data) => apiClient.post<{ course: Course }>(\"/api/learning/courses\", data),\n update: async (id, data) => apiClient.put<{ course: Course }>(`/api/learning/courses/${id}`, data),\n delete: async (id) => apiClient.delete<{ message: string }>(`/api/learning/courses/${id}`),\n addMaterials: async (id, materials) => apiClient.post<{ message: string; count: number }>(\n `/api/learning/courses/${id}/materials`, { materials }\n ),\n deleteMaterial: async (courseId, materialId) => apiClient.delete<{ message: string }>(\n `/api/learning/courses/${courseId}/materials/${materialId}`\n ),\n};\n```\n\n### Support Tickets (`lib/api/support.ts`)\n\n```typescript\nexport const getSupportTickets = async (signal?) => {\n const response = await apiClient.get<{ tickets: SupportTicket[] }>(\n \"/api/admin/support/tickets\", { signal }\n );\n return response.tickets;\n};\n\nexport const getSupportTicketById = async (ticketId, signal?) => {\n const response = await apiClient.get<{ ticket: SupportTicket }>(\n `/api/admin/support/tickets/${ticketId}`, { signal }\n );\n return response.ticket;\n};\n\nexport const updateTicketStatus = async (ticketId, status) => {\n await apiClient.put(`/api/admin/support/tickets/${ticketId}/status`, { status });\n};\n\nexport const addAdminResponse = async (ticketId, message) => {\n await apiClient.post(`/api/admin/support/tickets/${ticketId}/responses`, { message });\n};\n\nexport const approveReopen = async (ticketId) => {\n await apiClient.post(`/api/admin/support/tickets/${ticketId}/reopen/approve`, {});\n};\n\nexport const declineReopen = async (ticketId, reason) => {\n await apiClient.post(`/api/admin/support/tickets/${ticketId}/reopen/decline`, { reason });\n};\n```\n\n## Governance Console\n\n### `lib/governance/manifest-types.ts`\n\nType definitions for governance data structures.\n\n```typescript\nexport const GOVERNANCE_KINDS = [\"boolean\", \"enum\", \"policy\"] as const;\nexport type GovernanceKind = (typeof GOVERNANCE_KINDS)[number];\n\nexport interface GovernanceManifestEntry {\n id: string;\n kind: GovernanceKind;\n label: string;\n help: string;\n target: \"platform\" | \"any\";\n authz: \"platform-admin\" | \"scope-admin\";\n enumValues?: string[];\n}\n\nexport interface GovernanceReportEntry extends GovernanceManifestEntry {\n configured: unknown | null;\n effective: unknown;\n configuredLabel: string | null;\n effectiveLabel: string;\n effectiveFrom: string | null;\n overridden: boolean;\n enforcement: EnforcementState;\n}\n```\n\n### `lib/governance/controls.ts`\n\nMaps governance kinds to UI controls.\n\n```typescript\nexport const CONTROL_FOR_KIND: Record<GovernanceKind, ControlKind> = {\n enum: \"segmented\",\n boolean: \"switch\",\n policy: \"policy-editor\",\n};\n\nexport function controlFor(kind: string, resourceId: string): ControlKind {\n if (!isKnownKind(kind)) throw new UnknownGovernanceKindError(kind, resourceId);\n return CONTROL_FOR_KIND[kind];\n}\n```\n\n### `lib/governance/parse.ts`\n\nParsers for governance data structures.\n\n```typescript\nexport const readScopeReport: Reader<ScopeReport> = (v, p) => {\n const o = obj(v, p);\n return {\n scope: field(o, \"scope\", str, p),\n scopeLabel: field(o, \"scopeLabel\", str, p),\n posture: field(o, \"posture\", withDefault(str, \"unknown\"), p),\n storeAvailable: field(o, \"storeAvailable\", bool, p),\n entries: field(o, \"entries\", arrayOf(readReportEntry), p),\n };\n};\n\nexport const readCoverage: Reader<AdminCoverage> = (v, p) => {\n const o = obj(v, p);\n const c = obj(o.coverage, `${p}coverage`);\n return {\n distinctPersons: field(c, \"distinctPersons\", num, `${p}coverage`),\n rows: field(c, \"rows\", num, `${p}coverage`),\n groups: field(c, \"groups\", withDefault(arrayOf(...), []), `${p}coverage`),\n statement: field(c, \"statement\", withDefault(str, \"\"), `${p}coverage`),\n atFloor: field(c, \"atFloor\", withDefault(bool, false), `${p}coverage`),\n };\n};\n```\n\n## Pack Management\n\n### `lib/packs/source-descriptors.ts`\n\nField descriptors for pack source forms.\n\n```typescript\nexport const SOURCE_FIELD_KINDS = [\"text\", \"url\", \"enum\", \"boolean\", \"string-list\", \"credential-ref\", \"json\"] as const;\n\nexport const PACK_SOURCE_FIELDS: SourceFieldDescriptor[] = [\n { id: \"key\", kind: \"text\", label: \"Key\", required: true, immutableAfterCreate: true },\n { id: \"label\", kind: \"text\", label: \"Name\", required: true },\n { id: \"url\", kind: \"url\", label: \"Source URL\", required: true },\n { id: \"ref\", kind: \"text\", label: \"Pinned version\" },\n { id: \"syncMode\", kind: \"enum\", label: \"How updates arrive\", options: [...] },\n { id: \"trustTier\", kind: \"enum\", label: \"Trust\", options: [...] },\n { id: \"subsetAll\", kind: \"boolean\", label: \"May contribute everything it contains\" },\n { id: \"subset\", kind: \"string-list\", label: \"What it may contribute\" },\n { id: \"authCredentialSlug\", kind: \"credential-ref\", label: \"Credential\" },\n { id: \"config\", kind: \"json\", label: \"Source options\" },\n];\n```\n\n### `lib/packs/vocabulary.ts`\n\nHuman-readable labels for pack member actions and exclusion reasons.\n\n```typescript\nexport const ACTION_COPY: Record<PackMemberAction, ActionCopy> = {\n install: { label: \"Will install\", meaning: \"New on this tenant.\", tone: \"install\" },\n update: { label: \"Will update\", meaning: \"Already here, and its content changed.\", tone: \"update\" },\n unchanged: { label: \"Unchanged\", meaning: \"Already here, byte for byte.\", tone: \"quiet\" },\n \"not-selected\": { label: \"Not selected\", meaning: \"Left out on purpose.\", tone: \"choice\" },\n excluded: { label: \"Excluded\", meaning: \"Cannot install.\", tone: \"blocked\" },\n};\n\nexport const EXCLUSION_COPY: Record<PackExcludeReason, ExclusionCopy> = {\n malformed: { label: \"Malformed\", meaning: \"The member's own payload was rejected.\", whoFixes: \"The pack author\" },\n collision: { label: \"Collides with their own\", meaning: \"This tenant already wrote something with that name.\", whoFixes: \"The tenant\" },\n scope: { label: \"Out of scope for this source\", meaning: \"The member asks for a governance class this source may not author.\", whoFixes: \"Change the source's trust tier\" },\n unsupported: { label: \"Unsupported here\", meaning: \"This platform cannot install it.\", whoFixes: \"Upgrade the platform\" },\n capability: { label: \"Needs a capability\", meaning: \"It requires a capability this tenant has not granted.\", whoFixes: \"Grant the capability\" },\n};\n```\n\n## Integration Points\n\n### With React Components\n\nComponents import specific API modules:\n\n```typescript\n// Example: Companies page\nimport { startupsApi } from \"@/lib/api/startups\";\nimport { describeError } from \"@/lib/api/errors\";\n\nconst handleProvision = async (data) => {\n try {\n const res = await startupsApi.provision(data);\n // Success handling\n } catch (e) {\n if (isApiError(e)) {\n setError(describeError(e, \"provision this company\"));\n } else {\n setError(e.message);\n }\n }\n};\n```\n\n### With Hooks\n\n```typescript\n// Example: useResource hook\nexport function useResource<T>(\n api: () => Promise<T>,\n deps: DependencyList = []\n) {\n const [data, setData] = useState<T | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n const abortController = new AbortController();\n \n api()\n .then(setData)\n .catch(setError)\n .finally(() => {\n setLoading(false);\n abortController.abort();\n });\n \n return () => abortController.abort();\n }, [...deps]);\n\n return { data, error, loading };\n}\n```\n\n### With Session Management\n\n```typescript\n// Example: Protected route\nimport { isAuthenticated, getAdminRole } from \"@/lib/auth/session\";\n\nexport default function AdminPage() {\n const session = useSession();\n \n if (!isAuthenticated()) {\n return <Redirect to=\"/login\" />;\n }\n \n if (getAdminRole() !== \"super_admin\") {\n return <Redirect to=\"/unauthorized\" />;\n }\n \n return <AdminContent />;\n}\n```\n\n## Error Handling Patterns\n\n### Try-Catch with `describeError()`\n\n```typescript\ntry {\n await apiClient.post(\"/api/admin/startups\", data);\n} catch (e) {\n if (isApiError(e)) {\n toast.error(describeError(e, \"create this company\"));\n } else {\n toast.error(e instanceof Error ? e.message : \"Something went wrong\");\n }\n}\n```\n\n### Network Error Detection\n\n```typescript\nif (isApiError(e) && e.isNetwork) {\n toast.error(\"Couldn't reach the server. Check your connection.\");\n}\n```\n\n### Session Expiration\n\n```typescript\nif (isApiError(e) && e.isExpiredSession) {\n signOut({ broadcast: true });\n router.push(\"/login\");\n}\n```\n\n## Testing Guidelines\n\n### Unit Testing API Calls\n\n```typescript\ndescribe(\"startupsApi\", () => {\n it(\"should list startups\", async () => {\n const mockResponse = { ok: true, startups: [{ id: 1, name: \"Test\" }] };\n \n // Mock apiClient\n vi.mocked(apiClient.get).mockResolvedValue(mockResponse);\n \n const result = await startupsApi.list();\n expect(result).toEqual({ ok: true, startups: [{ id: 1, name: \"Test\" }] });\n });\n});\n```\n\n### Parsing Validation\n\n```typescript\ndescribe(\"readScopeReport\", () => {\n it(\"should throw on missing entries\", () => {\n const invalidData = { scope: \"test\", storeAvailable: true };\n \n expect(() => parse(invalidData, readScopeReport)).toThrow(MalformedPayloadError);\n });\n});\n```\n\n## Common Pitfalls\n\n### 1. Don't Cast Without Parsing\n\n```typescript\n// ❌ WRONG - will crash on malformed data\nconst data = await apiClient.get<{ items: Item[] }>(\"/api/items\");\n\n// ✅ CORRECT - parse first\nconst data = parse(await apiClient.get<unknown>(\"/api/items\"), readItems);\n```\n\n### 2. Don't Default Unreadable Lists\n\n```typescript\n// ❌ WRONG - hides errors\nconst items = Array.isArray(res.items) ? res.items : [];\n\n// ✅ CORRECT - throw on failure\nconst items = field(res, \"items\", arrayOf(readItem), \"\");\n```\n\n### 3. Don't Bypass Session Management\n\n```typescript\n// ❌ WRONG - direct fetch\nconst res = await fetch(\"/api/admin\", { headers: { Authorization: `Bearer ${token}` } });\n\n// ✅ CORRECT - use apiClient\nconst res = await apiClient.get(\"/api/admin\");\n```\n\n## Environment Configuration\n\n```typescript\n// lib/api/client.ts\nconst API_URL =\n process.env.NEXT_PUBLIC_API_URL ||\n (process.env.NODE_ENV !== \"production\" ? \"http://localhost:5000\" : \"\");\n```\n\nSet `NEXT_PUBLIC_API_URL` in your environment files to override the default.\n\n## Module Dependencies\n\n```mermaid\ngraph LR\n A[client.ts] --> B[errors.ts]\n A --> C[session.ts]\n D[shape.ts] --> E[parse.ts]\n F[admin.ts] --> A\n G[governance.ts] --> A\n G --> D\n H[packs.ts] --> A\n I[learning.ts] --> A\n J[support.ts] --> A\n```\n\n## Version Compatibility\n\nThis module is designed to work with the server's `verifyAdmin` endpoints. All admin routes must be prefixed with `/api/admin/` or `/api/admin/` for tenant-specific operations.\n\nThe module assumes the server follows these conventions:\n- 200 responses with correct shape are valid\n- 401 responses trigger session renewal (except `/auth/*`)\n- 403 responses indicate permission denial\n- 404 responses indicate missing resources\n- 500+ responses indicate server errors\n\n## Contributing\n\nWhen adding new API endpoints:\n\n1. Create a new file in `lib/api/` following the naming convention\n2. Use `apiClient` for all HTTP calls\n3. Add optional `signal` parameter for abort support\n4. Parse responses using `parse()` with appropriate readers\n5. Export types for TypeScript safety\n6. Add to `lib/api/index.ts` if it's a public export\n\nWhen adding new data shapes:\n\n1. Define types in `lib/governance/manifest-types.ts` or `lib/packs/types.ts`\n2. Add parsers in `lib/governance/parse.ts` or `lib/packs/parse.ts`\n3. Ensure exhaustive coverage in `controls.ts` or `vocabulary.ts`\n4. Run `scripts/check-governance-controls.cjs` to verify","harakat-admin-scripts":"# Harakat_Admin — scripts\n\n\n\n# Harakat_Admin — scripts\n\n## Overview\n\nThe `scripts/` directory contains static analysis tools that enforce architectural and code quality rules across the Harakat_Admin codebase. These scripts run as part of CI pipelines and developer workflows to catch issues that typecheckers and linters cannot detect — specifically, patterns that render \"confident lies\" to operators, missing error boundaries, and inconsistencies between server and console.\n\nEach script validates a specific invariant and exits with a non-zero code when violations are found. All scripts include self-tests that prove they catch the violations they claim to catch.\n\n## Architecture\n\n```mermaid\ngraph TD\n A[Developer/CI] --> B[scripts/]\n B --> C[check-console-gaps]\n B --> D[check-error-boundaries]\n B --> E[check-frame]\n B --> F[check-governance-controls]\n B --> G[check-packs-console]\n B --> H[check-token-access]\n \n C --> I[lib/governance/controls.ts]\n C --> J[components/governance/*.tsx]\n D --> K[app/admin/layout.tsx]\n D --> L[app/error.tsx]\n E --> M[components/frame/PageFrame.tsx]\n F --> N[lib/governance/manifest-types.ts]\n F --> O[HarakatOffices_Server]\n G --> P[lib/packs/vocabulary.ts]\n G --> Q[HarakatOffices_Server]\n H --> R[lib/auth/session.ts]\n```\n\n## Script Documentation\n\n### check-console-gaps\n\n**Purpose:** Prevents console UI surfaces from rendering \"confident lies\" — empty states that look like success states, or missing data that appears as normal operation.\n\n**What it protects:** Six specific failure modes in the A6 console surfaces:\n1. \"No disagreements\" rendered when the measurement store is absent\n2. \"No prompts captured\" rendered when nothing is being captured\n3. A rate with no denominator (0% over 4 calls vs 0% over 4000 calls)\n4. Permission/outage state rendered as an empty list\n5. Count of ADMIN ROWS presented as a count of people\n6. Native `confirm()` calls (banned, direction-blind, untranslatable)\n\n**Key Components:**\n\n```javascript\nconst SUBJECTS = [\n {\n file: \"components/governance/shadow-panel.tsx\",\n rules: [\n {\n name: \"an absent measurement store is reported, never rendered as agreement\",\n test: (s) => /storeAvailable/.test(s) && /not recording shadow measurements/i.test(s),\n why: '\"nothing disagreed\" and \"nothing was recorded\" are opposite claims.'\n },\n // ... 4 more rules\n ],\n },\n // ... 4 more subjects\n];\n```\n\n**Execution Flow:**\n1. `check()` reads each file in `SUBJECTS`\n2. For each rule, runs `rule.test(src)` against the source\n3. Collects violations with `rule.name` and `rule.why`\n4. Also scans all subjects for native `confirm()`/`alert()` calls\n5. Returns array of problems or empty array if clean\n\n**Self-test:** The `selftest()` function creates artificial breakages (e.g., removing `storeAvailable` branch, dropping `CONFIDENCE_FLOOR`) and proves the checker catches them.\n\n---\n\n### check-error-boundaries\n\n**Purpose:** Ensures every route segment with a `layout.tsx` has a strict ancestor with an `error.tsx`, and the root has both `global-error.tsx` and `not-found.tsx`.\n\n**Why this exists:** Next.js `error.tsx` boundaries only wrap their own segment's children, not their own `layout.tsx`. A throw in `app/admin/layout.tsx` sails past `app/admin/error.tsx` and needs a strict ancestor boundary.\n\n**Key Components:**\n\n```javascript\nfunction collectSegments(appRoot, readdir) {\n // Walks app/ into flat list of segments\n // Returns [{ dir, hasLayout, hasError, hasGlobalError, hasNotFound, hasPage }]\n}\n\nfunction ancestorsOf(dir) {\n // Returns strictly-higher segments, nearest first\n // Root (\"\") is always last\n}\n\nfunction auditBoundaries(segments) {\n // Returns { violations, notes }\n // violations = white-screen paths (FAIL)\n // notes = covered only by global-error (smell)\n}\n```\n\n**Invariant:** A segment that owns a `layout.tsx` must have a strict ancestor that owns an `error.tsx`; the root must own a `global-error.tsx` and a `not-found.tsx`.\n\n**Execution Flow:**\n1. `collectSegments()` walks `app/` into segment list\n2. `auditBoundaries()` checks:\n - Root layout has `global-error.tsx`\n - Root has `not-found.tsx`\n - Each segment with layout has ancestor with `error.tsx`\n3. Returns violations (white-screen paths) and notes (global-error-only coverage)\n\n**Self-test:** Uses tree literals to test various configurations (e.g., \"shell layout, no boundary anywhere\" should produce 3 violations).\n\n---\n\n### check-frame\n\n**Purpose:** Enforces UI frame consistency: one `<h1>` per page (owned by `PageFrame`), every admin page mounts the frame, no native `<select>`, and file size ceiling of 1000 lines.\n\n**Key Components:**\n\n```javascript\nconst MAX_PAGE_LINES = 1000;\n\nconst OVERSIZE_BASELINE = {\n // Files pinned at size when gate landed\n // May only shrink, never grow\n};\n\nconst UNFRAMED = [\n /^app\\/error\\.tsx$/,\n /^app\\/global-error\\.tsx$/,\n /^app\\/not-found\\.tsx$/,\n /^app\\/login\\/page\\.tsx$/,\n];\n\nconst FRAME_HOSTS = {\n \"@/components/records/RecordRoute\": \"components/records/RecordRoute.tsx\",\n};\n```\n\n**Four Rules:**\n1. ONE `<h1>`, owned by `PageFrame`\n2. EVERY `app/admin/**/page.tsx` mounts `PageFrame`\n3. NO native `<select>` (use design-system Select)\n4. FILE SIZE ceiling of 1000 lines\n\n**Execution Flow:**\n1. `walk()` traverses `app/`, `components/`, `hooks/`, `lib/`\n2. `stripBlockComments()` removes `/* */` spans (keeps line count)\n3. For each file:\n - Checks for `<h1>` outside `PageFrame`\n - Checks for native `<select>`\n - Checks if admin page mounts `PageFrame`\n - Checks file size against ceiling/baseline\n4. Returns violations array\n\n**Self-test:** Creates temporary trees with violations (e.g., `<h1>` in admin page, page over 1000 lines) and proves the checker catches them.\n\n---\n\n### check-governance-controls\n\n**Purpose:** Validates that every `GovernanceKind` the server declares has a control in `lib/governance/controls.ts`, and the console's kind list matches the server's in both directions.\n\n**Key Components:**\n\n```javascript\nfunction serverKinds(serverDir) {\n // Reads `export type GovernanceKind = ...` from server\n}\n\nfunction consoleKinds() {\n // Reads `export const GOVERNANCE_KINDS = [...]` from console\n}\n\nfunction consoleControls() {\n // Reads `CONTROL_FOR_KIND = {...}` from controls.ts\n}\n```\n\n**Four Rules:**\n1. Every server `GovernanceKind` has a control in `controls.ts`\n2. Console kind list matches server list (both directions)\n3. Every descriptor id has a `kind` that resolves\n4. `controls.ts` still throws on unknown kind\n\n**Execution Flow:**\n1. `run()` reads console kinds, controls, and (if `--cross-repo`) server kinds\n2. Checks each kind has a control\n3. Checks kind lists match both directions\n4. Checks `UnknownGovernanceKindError` throw exists\n5. Returns problems array\n\n**Self-test:** Simulates various mismatches (missing control, extra kind, lost throw) and proves the checker catches them.\n\n---\n\n### check-packs-console\n\n**Purpose:** Ensures the pack catalog's vocabularies (exclusion reasons, member actions, source field kinds) match between console and server.\n\n**Key Components:**\n\n```javascript\nfunction consoleList(src, name) {\n // Reads `export const EXCLUDE_REASONS = [...]`\n}\n\nfunction recordKeys(src, name) {\n // Reads `EXCLUSION_COPY = {...}` object keys\n}\n\nfunction consoleDescriptors(src) {\n // Reads `PACK_SOURCE_FIELDS` array\n}\n\nfunction renderedKinds(src) {\n // Reads `kind === \"...\"` branches in source-field.tsx\n}\n```\n\n**Four Rules:**\n1. Every `PackExcludeReason` and `PackMemberAction` has copy in `vocabulary.ts`\n2. `vocabulary.ts` throws on unknown values\n3. Every source-field kind has a control in `source-field.tsx`\n4. (Cross-repo) Console vocabularies match server's\n\n**Execution Flow:**\n1. `readEverything()` reads all console files\n2. If `--cross-repo`, reads server files from `HarakatOffices_Server`\n3. `evaluate()` checks:\n - Vocabularies match both directions\n - Unknown-value throw exists\n - Field kinds have controls\n - Descriptors have valid kinds\n4. Returns problems array\n\n**Self-test:** Uses `CLEAN` fixture and patches to simulate violations (missing copy, extra dead code, lost throw).\n\n---\n\n### check-token-access\n\n**Purpose:** Enforces single source of truth for session tokens. The token must be named in exactly one module (`lib/auth/session.ts`), and legacy split-brain flags (`admin_authenticated`, etc.) are banned.\n\n**Key Components:**\n\n```javascript\nconst OWNER = \"lib/auth/session.ts\";\n\nconst SESSION_KEYS = [\"harakat_admin_session\", \"accessToken\"];\n\nconst BANNED_LEGACY_KEYS = [\"admin_authenticated\", \"admin_email\", \"admin_role\"];\n\nfunction findViolations(root = ROOT) {\n // Walks source dirs, finds storage access patterns\n}\n```\n\n**Two Rules:**\n1. Session key named in exactly ONE module (`lib/auth/session.ts`)\n2. Legacy split-brain flags may not be written anywhere\n\n**Execution Flow:**\n1. `walk()` traverses `app/`, `components/`, `hooks/`, `lib/`, `types/`\n2. For each file:\n - Skips test files, e2e, journeys\n - Checks for `sessionStorage`/`localStorage` access to session keys\n - Checks for legacy flag writes\n3. Returns violations with `rel`, `line`, `text`, `kind`\n\n**Self-test:** Creates temporary trees with violations (raw token read, split-brain flag returning) and proves the checker catches them.\n\n---\n\n## Usage\n\n### Running Scripts\n\n```bash\n# Check console gaps\npnpm check:console-gaps\n\n# Check error boundaries\npnpm check:boundaries\n\n# Check frame consistency\npnpm check:frame\n\n# Check governance controls\npnpm check:governance\n\n# Check packs catalog\npnpm check:packs\n\n# Check token access\npnpm check:token\n```\n\n### Cross-repo Checks\n\nSome scripts need the server tree to compare against:\n\n```bash\n# From meta checkout\npnpm check:governance:cross\npnpm check:packs:cross\n```\n\n### Self-tests\n\n```bash\npnpm check:console-gaps --selftest\npnpm check:boundaries --selftest\npnpm check:frame --selftest\npnpm check:governance --selftest\npnpm check:packs --selftest\npnpm check:token --selftest\n```\n\n### JSON Output\n\n```bash\npnpm check:boundaries --json\n```\n\n## Integration with CI\n\nAll scripts are wired into CI pipelines. They exit with:\n- `0` = clean (no violations)\n- `1` = violations found\n- `2` = cross-repo check couldn't run (server tree missing)\n\nThe CI pipeline runs all scripts in sequence. A single failure blocks the build.\n\n## Connection to Codebase\n\n### lib/governance/\n- `controls.ts` - Maps `GovernanceKind` to UI controls\n- `manifest-types.ts` - Declares `GOVERNANCE_KINDS`\n\n### lib/packs/\n- `types.ts` - Declares `EXCLUDE_REASONS`, `MEMBER_ACTIONS`\n- `vocabulary.ts` - Copy for UI rendering\n- `source-descriptors.ts` - `PACK_SOURCE_FIELDS` array\n\n### lib/auth/\n- `session.ts` - Single source of truth for session tokens\n\n### components/frame/\n- `PageFrame.tsx` - Renders the one `<h1>` per page\n\n### app/\n- `admin/layout.tsx` - Root layout (needs `global-error.tsx` above)\n- `error.tsx` - Segment error boundary\n- `not-found.tsx` - 404 page\n\n## Common Violations\n\n| Script | Violation | Fix |\n|--------|-----------|-----|\n| check-console-gaps | Empty state rendered as success | Add `storeAvailable` branch |\n| check-error-boundaries | Layout without ancestor error | Add `error.tsx` to ancestor |\n| check-frame | `<h1>` outside `PageFrame` | Move to `PageFrame` |\n| check-frame | File over 1000 lines | Split into parts |\n| check-governance | Kind without control | Add to `controls.ts` |\n| check-packs | Vocab without copy | Add to `vocabulary.ts` |\n| check-token | Token read outside owner | Import from `session.ts` |\n\n## Notes\n\n- All scripts include self-tests that must pass before trusting them\n- Cross-repo checks exit 2 when server tree is missing (never pass)\n- Test files are excluded from `check-token-access` (they seed storage)\n- Block comments are stripped for JSX rule checks (not tidiness)\n- Baseline file sizes may only shrink, never grow","harakat-admin-styles":"# Harakat_Admin — styles\n\n\n\n# Harakat_Admin — styles\n\n## Overview\n\nThe `globals.css` module serves as the central configuration file for the application's visual design system. It leverages **Tailwind CSS v4** to define global color tokens, typography settings, and base layer resets. This file does not contain runtime logic; instead, it acts as a static stylesheet that initializes the theme context for the entire application.\n\n## Theme System & Color Tokens\n\nThe module implements a comprehensive color system using the **OKLCH** color space, which provides better perceptual uniformity compared to traditional RGB or HSL.\n\n### Light Mode\nDefault color values are defined in the `:root` selector. These variables are used for the standard application interface.\n\n```css\n:root {\n --background: oklch(1 0 0);\n --foreground: oklch(0.145 0 0);\n --primary: oklch(0.205 0 0);\n /* ... additional tokens ... */\n}\n```\n\n### Dark Mode\nDark mode values are defined within the `.dark` selector. This ensures that when the application's dark mode class is active, the CSS variables automatically update to high-contrast dark values.\n\n```css\n.dark {\n --background: oklch(0.145 0 0);\n --foreground: oklch(0.985 0 0);\n --primary: oklch(0.985 0 0);\n /* ... additional tokens ... */\n}\n```\n\n### Custom Variants\nThe `@custom-variant dark` directive enables the use of the `dark:` modifier in Tailwind utility classes (e.g., `dark:bg-background`).\n\n## Tailwind v4 Integration\n\nThis module utilizes the native Tailwind v4 CSS-first configuration approach.\n\n### `@theme inline`\nThe `@theme inline` directive maps the CSS custom properties defined in `:root` and `.dark` directly to Tailwind's utility class system. This allows developers to use semantic class names (e.g., `bg-background`, `text-foreground`) instead of hardcoded hex codes.\n\n**Example Usage:**\n```html\n<div class=\"bg-background text-foreground\">\n <!-- Renders using --background and --foreground variables -->\n<\/div>\n```\n\n### Font Configuration\nTypography is configured via CSS custom properties referencing external font assets:\n- `--font-sans`: Mapped to `var(--font-geist-sans)`\n- `--font-mono`: Mapped to `var(--font-geist-mono)`\n\nDevelopers must ensure these font assets are loaded in the application entry point (e.g., `index.html` or `layout.tsx`) for these variables to resolve correctly.\n\n## Global Resets & Base Layer\n\nThe `@layer base` directive applies global styles that cascade over the entire application.\n\n### Border & Outline\nAll elements inherit a border color defined by `--border` and an outline style defined by `--ring`.\n```css\n* {\n @apply border-border outline-ring/50;\n}\n```\n\n### Body Styling\nThe `body` element is explicitly styled to use the background and foreground colors defined in the theme.\n```css\nbody {\n @apply bg-background text-foreground;\n}\n```\n\n## Architecture & Execution Flow\n\nThis module is a **static configuration asset**. It does not execute logic, nor does it make outgoing or incoming calls during runtime. Its purpose is to be processed by the build toolchain (e.g., Vite, Next.js) and injected into the final HTML/CSS bundle.\n\n### Theme Resolution Flow\nThe following diagram illustrates how the CSS variables defined in this module are resolved by Tailwind utilities.\n\n```mermaid\nflowchart LR\n subgraph CSS Variables\n A[:root] -->|Light Mode| B[--background]\n C[.dark] -->|Dark Mode| D[--background]\n end\n\n subgraph Tailwind Utilities\n E[bg-background] -->|Resolves to| B\n F[text-foreground] -->|Resolves to| G[--foreground]\n end\n\n B --> H[Rendered Color]\n D --> H\n G --> H\n```\n\n## Usage Guidelines\n\n1. **Color Changes**: To modify the application's color palette, update the `oklch` values in the `:root` or `.dark` sections.\n2. **Dark Mode**: Ensure the application's root component or layout toggles the `dark` class on the `<html>` or `<body>` tag to activate dark mode variables.\n3. **Utility Classes**: Prefer semantic Tailwind classes (e.g., `border-border`) over raw CSS values to maintain consistency with the theme tokens.\n4. **Font Loading**: Verify that `geist-sans` and `geist-mono` are available in the project's asset pipeline before relying on the `@theme inline` font mappings.","harakat-admin-types":"# Harakat_Admin — types\n\n\n\n# Harakat_Admin — types\n\nThis module defines the TypeScript interfaces and type aliases used throughout the Harakat Admin backend. It standardizes data structures for authentication, platform configuration, service management, and API communication.\n\n## File Organization\n\nThe module is split into two files to separate API response contracts from internal state definitions:\n\n* **`index.ts`**: Primary export file. Contains core entity types (`Admin`, `Agent`, `Service`), API response wrappers (`LoginResponse`, `SettingsResponse`), and utility types (`ApiResult`).\n* **`settings.ts`**: Contains internal state types, permission matrices, and notification preferences.\n\n## Core Type Definitions\n\n### Authentication & Admin\nDefines the structure for administrative users and their roles.\n\n* **`Admin`** (`index.ts`): Represents a full administrative entity retrieved from the database.\n * `id`: Unique numeric identifier.\n * `name`: Optional display name.\n * `email`: Unique login identifier.\n * `role`: Defines access level (see **Critical Implementation Notes**).\n * `createdAt`: ISO timestamp of account creation.\n* **`AdminUser`** (`settings.ts`): A simplified internal representation of an admin, used for state management.\n * `name`: Required (unlike `Admin` where it is nullable).\n* **`AdminRole`**:\n * **`index.ts`**: `\"SUPER_ADMIN\" | \"ADMIN\" | \"MANAGER\"`\n * **`settings.ts`**: `\"super_admin\" | \"admin\"`\n * *Note: See **Critical Implementation Notes** below regarding casing and value consistency.*\n\n### Platform Configuration\nDefines the global settings for the Harakat platform.\n\n* **`PlatformSettings`** (`index.ts`): API response version. Includes metadata like `id` and `updatedAt`.\n* **`PlatformSettings`** (`settings.ts`): Internal state version. Excludes database metadata fields.\n* **`PermissionsMatrix`** (`settings.ts`): Defines granular permission flags for different admin roles.\n* **`NotificationPreferences`** (`settings.ts`): User-specific notification toggles (email, pending, system).\n\n### Services & Agents\nTypes for the marketplace or service catalog.\n\n* **`Agent`** (`index.ts`): Represents an AI agent or service provider.\n * `price`: Optional cost associated with the agent.\n* **`Service`** (`index.ts`): Represents a general service offering.\n * `price`: Optional cost associated with the service.\n\n### API Response Patterns\nStandardized shapes for API interactions to ensure consistent error handling and data parsing.\n\n* **`LoginResponse`** (`index.ts`):\n * `ok`: Boolean flag for success.\n * `accessToken`: JWT or session token.\n * `admin`: Nested object containing admin details.\n * `requires2FA`: Optional boolean for 2FA enforcement.\n * `tempToken`: Optional temporary session token.\n* **`SettingsResponse`** (`index.ts`):\n * `ok`: Boolean flag for success.\n * `settings`: The `PlatformSettings` object.\n* **`ErrorResponse`** (`index.ts`):\n * `error`: Error code or identifier.\n * `message`: Human-readable error string.\n * `details`: Optional array of specific validation errors.\n* **`ApiResult<T>`** (`index.ts`):\n * Utility type: `Promise<T | ErrorResponse>`.\n * Used to type API functions that return either a success payload or an error object.\n\n## Critical Implementation Notes\n\n### 1. `AdminRole` Inconsistency\nThere is a discrepancy in the `AdminRole` type definition between `index.ts` and `settings.ts`.\n\n* **`index.ts`** uses uppercase values: `\"SUPER_ADMIN\" | \"ADMIN\" | \"MANAGER\"`.\n* **`settings.ts`** uses lowercase values: `\"super_admin\" | \"admin\"`.\n\nDevelopers must ensure they are using the correct file for their context:\n* Use **`index.ts`** types when handling API responses or database records.\n* Use **`settings.ts`** types when managing internal application state or permissions.\n* *Recommendation*: Align these definitions to a single source of truth to prevent runtime casting errors.\n\n### 2. `PlatformSettings` Metadata\nThe `PlatformSettings` interface differs between files regarding metadata:\n* **`index.ts`** includes `id` and `updatedAt`.\n* **`settings.ts`** omits these fields.\n\nThis suggests `index.ts` is intended for API payloads (where database IDs are required), while `settings.ts` is intended for internal state (where IDs may be irrelevant).\n\n## Type Hierarchy\n\nThe following diagram illustrates the relationship between the primary API response types and the core entity types they contain.\n\n```mermaid\nclassDiagram\n class ApiResult~T~ {\n +Promise<T | ErrorResponse>\n }\n class LoginResponse {\n +ok: boolean\n +accessToken: string\n +admin: Admin\n }\n class SettingsResponse {\n +ok: boolean\n +settings: PlatformSettings\n }\n class Admin {\n +id: number\n +role: AdminRole\n +email: string\n }\n class PlatformSettings {\n +platformName: string\n +maintenanceMode: boolean\n }\n class ErrorResponse {\n +error: string\n +message: string\n }\n\n ApiResult~T~ --> ErrorResponse\n LoginResponse --> Admin\n SettingsResponse --> PlatformSettings\n```\n\n## Usage Examples\n\n### Handling API Results\nWhen calling an API endpoint, use `ApiResult` to type the return value:\n\n```typescript\nimport { ApiResult, LoginResponse } from './types';\n\nasync function login(email: string, password: string): Promise<ApiResult<LoginResponse>> {\n // Implementation details\n return { ok: true, accessToken: \"...\" };\n}\n```\n\n### Accessing Admin Data\nWhen parsing a `LoginResponse`, destructure the nested `admin` object:\n\n```typescript\nimport { LoginResponse } from './types';\n\nconst response: LoginResponse = {\n ok: true,\n accessToken: \"token\",\n admin: { id: 1, email: \"admin@example.com\", role: \"ADMIN\", name: null }\n};\n\nif (response.ok) {\n const { id, role } = response.admin;\n // role is typed as AdminRole\n}\n```","harakat-admin":"# Harakat_Admin\n\n\n\n# Harakat_Admin\n\nThe **Harakat_Admin** module serves as the central administrative interface for the Harakat platform, enabling management of educational content, startup enrollments, and system governance. Built on a **Next.js** frontend and **Express** backend, it enforces strict data integrity via **Prisma** and **PostgreSQL**.\n\n## Architecture & Stack\n\nThe system follows a standard RESTful architecture with a clear separation between the client (`app`), the API layer (`lib`), and the data layer (`docs`). Visual consistency is managed through `styles`, while shared logic is encapsulated in `hooks`.\n\n## Core Sub-modules\n\n* **`hooks`**: Centralizes critical React logic including `useResource` for race-condition-free data fetching and `useAuthCheck` for cross-tab session synchronization. These hooks power key views such as `AdminDashboard`, `CoursePage`, and `CompaniesPage`.\n* **`docs`**: Defines the Learning Hub API Integration, outlining the RESTful architecture connecting the client to the Express API layer.\n* **`e2e`**: Validates the application's UI states and authentication flows using Playwright, ensuring consistency across the `app` and `components` layers.\n* **`styles`**: Manages the visual design system using Tailwind CSS v4 and OKLCH color tokens in `globals.css`.\n* **`types`**: Standardizes TypeScript interfaces for entities like `Admin`, `Agent`, and `Service`, ensuring type safety across API responses and internal state.\n\n## Integration Workflows\n\nData flows from the `app` pages through `hooks` (e.g., `useResource`) to the `lib` API layer, adhering to contracts defined in `types`. The `e2e` suite verifies these interactions, while `styles` ensures consistent rendering.\n\n```mermaid\ngraph TD\n subgraph Frontend\n App[app]\n Hooks[hooks]\n Styles[styles]\n Components[components]\n end\n\n subgraph Backend\n Lib[lib]\n Docs[docs]\n end\n\n subgraph Testing\n E2E[e2e]\n end\n\n subgraph Data\n DB[(PostgreSQL)]\n end\n\n App --> Hooks\n Hooks --> Lib\n Lib --> Docs\n Docs --> DB\n E2E -.-> App\n Styles -.-> App\n Components -.-> Hooks\n```\n\n## Key Data Flows\n\n* **Authentication**: `useAuthCheck` manages session consistency across tabs, triggering `endSession()` via `localStorage` when `AdminLayout` detects token expiration.\n* **Data Fetching**: `useResource` prevents stale data overwrites during rapid state updates, utilized by `MarketplaceAgentPage` and `LearningHub`.\n* **Error Handling**: `types` defines `MalformedPayloadError` contracts, which are caught by `readList` in `AdminDashboard` and `CompaniesPage` to ensure graceful degradation.","harakat-ai-agents":"# Harakat_AI — agents\n\n\n\n# Harakat_AI — agents/hermes_native\n\n## Overview\n\nThe `hermes_native` package is the integration layer that adapts Harakat's governed backend tools into upstream Hermes' tool registry. It enables Hermes-native Harakat runs while ensuring tenant data flows only through Harakat backend endpoints.\n\nThis module provides:\n- **Tool bridging** — Converts Harakat tools into Hermes-compatible `BridgedTool` objects\n- **Session management** — Builds Hermes-compatible session contexts from Harakat request bodies\n- **Agent execution** — Runs Hermes `AIAgent` with Harakat-native guards and policy validation\n- **Child delegation** — Runs specialized subagents (search, document, ERP) as isolated child agents\n- **Audit & telemetry** — Emits audit logs and usage events for compliance and analytics\n\n## Architecture\n\n```mermaid\ngraph TD\n A[Harakat Request] --> B[build_native_session]\n B --> C[SessionContext]\n C --> D[stream_native_aia_agent_events]\n D --> E[prepare_harakat_toolsets]\n E --> F[AIAgent.run_conversation]\n F --> G[HermesSSECallbackBridge]\n G --> H[Child Delegation]\n H --> I[doc_subagent/erp_subagent]\n I --> J[run_child_blocking]\n J --> K[run_child_with_busy_retry]\n F --> L[HarakatMemoryProvider]\n F --> M[RunBudget]\n F --> N[Audit.emit]\n```\n\n## Core Components\n\n### Session Building\n\nThe entry point for all Hermes-native runs is `stream_native_aia_agent_events` in `aia_runner.py`. It orchestrates the entire agent lifecycle:\n\n```python\nasync def stream_native_aia_agent_events(body: Any) -> AsyncIterator[dict[str, Any]]\n```\n\n**Key responsibilities:**\n1. **Session construction** — Calls `build_native_session` to create `SessionContext`\n2. **System prompt augmentation** — Appends creation prompts, surface directives, and page context\n3. **Toolset preparation** — Calls `prepare_harakat_toolsets` to register tools and set up the runtime environment\n4. **Child delegation arming** — Enables search/doc/ERP subagents when configured\n5. **Agent execution** — Spawns a thread to run `AIAgent` synchronously\n6. **Event streaming** — Yields Hermes-shape events to the SSE bridge\n\n**Session types:**\n- **Shell runs** — `build_shell_agent_context` for general-purpose queries\n- **Assistant runs** — `build_assistant_agent_context` for tenant-scoped assistant operations\n\n### Tool Bridging\n\nThe `tool_bridge` module converts Harakat tools into Hermes-compatible objects:\n\n```python\nclass BridgedTool\n```\n\n**Key functions:**\n- `as_hermes_tools` — Converts Harakat tool registry to Hermes format\n- `build_tool_registry` — Builds the complete tool registry with role/tier filtering\n- `SessionContext` — Carries session-scoped tool context\n\n**Tool visibility:**\n```python\ndef _role_visible(tool: dict, session: SessionContext) -> bool\n```\nTools are filtered by:\n- User tier (`session.user_tier`)\n- Role (`session.role`)\n- Grant (`session.grant`)\n\n### Memory Management\n\nThe `HarakatMemoryProvider` manages memory for Hermes-native runs:\n\n```python\nclass HarakatMemoryProvider\n```\n\n**Key methods:**\n- `system_prompt_block()` — Returns memory prompt for the agent\n- `on_pre_compress()` — Called before memory compression\n- `on_delegation()` — Called when delegation occurs\n- `is_available()` — Checks if memory is available\n\n**Memory isolation:**\n- Honcho user memory is **withheld during creation runs** to prevent poisoning the rule being authored\n- Memory is tenant-scoped and never crosses user/tenant boundaries\n\n### Child Delegation\n\nSpecialized subagents run as isolated child agents:\n\n#### Search Subagent\n```python\ndef run_search_subagent(parent_ctx: ToolRunContext, query: str) -> dict[str, Any]\n```\n- Runs on a separate model (configurable via `SEARCH_SUBAGENT_MODEL`)\n- Grounded in the tenant's document corpus\n- Returns search results with quality metrics\n\n#### Document Subagent\n```python\nasync def run_document_subagent(parent_ctx: ToolRunContext, args: dict[str, Any]) -> dict[str, Any]\n```\n**Two modes:**\n1. **Drafting mode** — `create_document` delegation with grounding\n2. **Evidence mode** — `gather_document_evidence` for retrieval-only\n\n**Grounding loop:**\n1. Find source material in the workspace\n2. Read and extract actual facts (line items, prices, TRNs)\n3. Look at reference documents for layout\n4. Stage the document with real facts (bracketing unknowns)\n\n#### ERP Subagent\n```python\nasync def run_erp_subagent(parent_ctx: ToolRunContext, app: str, task: str) -> dict[str, Any]\n```\n- Runs per-app MCP bundles (contacts, projects, CRM)\n- Child agent loaded with app-specific skills\n- Main model never learns ERP conventions\n\n### Budget Management\n\nThe `RunBudget` class tracks and enforces spend limits:\n\n```python\nclass RunBudget\n```\n\n**Key methods:**\n- `note_retrieval_result()` — Records search costs\n- `note_external_cost()` — Charges child agent costs\n- `current_cost_usd()` — Returns current spend\n- `attach_child()` / `detach_child()` — Manages child agent billing\n\n**Budget enforcement:**\n- Soft cap: Warning at threshold\n- Hard cap: Blocks further tool calls\n- Child costs billed in real-time, not just at completion\n\n### Runtime Policy\n\nThe `runtime_policy` module validates and enforces runtime constraints:\n\n```python\ndef validate_native_runtime_policy(\n session: SessionContext,\n provider: str,\n model: str,\n base_runtime_home: Path,\n runtime_home: Path,\n enabled_toolsets: list[str]\n) -> None\n```\n\n**Validations:**\n- Tenant boundary enforcement\n- LLM selection restrictions\n- Toolset availability\n- Runtime home path validation\n\n## Execution Flow\n\n### Main Agent Run\n\n```\n1. stream_native_aia_agent_events(body)\n └─> build_native_session(body)\n └─> build_assistant_agent_context / build_shell_agent_context\n └─> SessionContext, vessel, system_prompt\n └─> _maybe_append_creation_prompt(body, system_prompt)\n └─> _maybe_append_surface_prompt(body, system_prompt)\n └─> _maybe_append_page_context(body, system_prompt)\n └─> prepare_harakat_toolsets(session, task_id, ...)\n └─> HermesSSECallbackBridge(...)\n └─> Thread: _run_agent_sync(...)\n └─> AIAgent.run_conversation(task_text, ...)\n └─> final_response\n └─> _deliver(final_text)\n └─> _capture_native_usage(...)\n```\n\n### Child Agent Run\n\n```\n1. run_child_with_busy_retry(...)\n └─> run_child_blocking(...)\n └─> set_hermes_home_for_context(child_home)\n └─> ensure_harakat_hermes_config(...)\n └─> registration_lock()\n └─> reassert_run_registration(child_ctx)\n └─> AIAgent(...)\n └─> run_conversation(user_message=goal, ...)\n └─> result\n └─> bill_child_to_run(parent_ctx, outcome, ...)\n```\n\n## Key Configuration\n\n### Environment Variables\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `HERMES_RUNTIME_HOME` | Base runtime directory | `.runtime/hermes-agent` |\n| `HERMES_NATIVE_PERSISTENCE` | Enable session persistence | `on` |\n| `HERMES_FINALIZE_TIMEOUT_S` | Finalize response timeout | `120` |\n| `SEARCH_BUDGET` | Enable search budget | `on` |\n| `HONCHO_ENABLED` | Enable Honcho user memory | `off` |\n| `DOC_SUBAGENT` | Enable document subagent | `off` |\n| `ERP_SUBAGENT` | Enable ERP subagent | `off` |\n| `OPENROUTER_REASONING` | Reasoning effort level | `medium` |\n\n### LLM Selection\n\n```python\nfrom core.config import resolve_llm_selection\n\nselection = resolve_llm_selection()\nprovider = selection.provider\nmodel = selection.model\nbase_url = selection.base_url\napi_key = selection.api_key\n```\n\n## Integration Points\n\n### With Harakat Backend\n\n- **Tool Gateway** — All tool calls go through Harakat's tool gateway\n- **Audit Logging** — Tool calls emit to Harakat audit endpoint\n- **Memory Provider** — Harakat memory provider manages memory\n- **Session Context** — Harakat session context carries tenant data\n\n### With Hermes Upstream\n\n- **AIAgent** — Uses Hermes' `AIAgent` class for conversation\n- **SSE Bridge** — `HermesSSECallbackBridge` streams events to Hermes\n- **Tool Registry** — Tools registered in Hermes-compatible format\n- **State DB** — Optional session persistence via `SessionDB`\n\n### With Core Modules\n\n```python\nfrom core.config import (\n HERMES_MODEL,\n HERMES_RUNTIME_HOME,\n openrouter_providers,\n resolve_llm_selection,\n)\nfrom core.llm_pricing import estimate_cost_usd\nfrom core.grant import narrate_for_model\nfrom core.posthog_tracker import capture_ai_generation\n```\n\n## Usage Examples\n\n### Basic Run\n\n```python\nfrom agents.hermes_native import stream_native_aia_agent_events\n\nasync def run_agent(task: str):\n body = {\n \"task\": task,\n \"surface\": \"dashboard\",\n \"creation_kind\": None,\n }\n async for event in stream_native_aia_agent_events(body):\n if event.get(\"type\") == \"user_message\":\n yield event\n elif event.get(\"type\") == \"action_preview\":\n yield event\n```\n\n### Assistant Run\n\n```python\nfrom agents.hermes_native import build_assistant_agent_context\n\nasync def run_assistant(task: str, assistant_id: int):\n body = {\n \"task\": task,\n \"assistant_id\": assistant_id,\n \"tenant\": {\"name\": \"Acme Corp\"},\n }\n session, vessel, system_prompt = await build_assistant_agent_context(body)\n # Use session for tenant-scoped operations\n```\n\n### Document Creation\n\n```python\nfrom agents.hermes_native.category_agents import doc_subagent\n\nasync def create_document(args: dict[str, Any]):\n result = await doc_subagent.run_document_subagent(\n parent_ctx=parent_ctx,\n args=args,\n )\n if result is None:\n # Fallback to inline tool\n return await inline_create_document(args)\n return result\n```\n\n## Testing\n\n### Key Test Files\n\n- `tests/test_hermes_native_phase2.py` — Tool bridging and session context\n- `tests/test_hermes_native_phase3.py` — Memory provider and system prompts\n- `tests/test_run_budget.py` — Budget enforcement and child billing\n- `tests/test_search_failsoft.py` — Search subagent failover\n\n### Test Patterns\n\n```python\n# Test tool visibility\ndef test_grant_visible():\n from agents.hermes_native.tool_bridge import _role_visible\n assert _role_visible(tool, session) == expected\n\n# Test budget enforcement\ndef test_budget_sees_candidates():\n budget = RunBudget(agent=agent, model=model)\n budget.note_retrieval_result(...)\n assert budget.current_cost_usd > 0\n\n# Test child delegation\ndef test_external_cost_from_child():\n outcome = await run_child_blocking(...)\n bill_child_to_run(parent_ctx, outcome, label=\"test\")\n assert parent_ctx.session.budget.current_cost_usd > 0\n```\n\n## Security & Compliance\n\n### Tenant Isolation\n\n- Runtime home paths are tenant-scoped\n- Session DB lives above conversation home (FTS recall within assistant)\n- Memory provider enforces tenant boundaries\n- Audit logs include tenant context\n\n### Policy Enforcement\n\n```python\ndef validate_category_child_policy(\n label: str,\n session: SessionContext,\n selection: LLMSelection,\n base_runtime_home: Path,\n runtime_home: Path,\n enabled_toolsets: list[str],\n tool_names: list[str],\n allowed_tools: frozenset[str]\n) -> None\n```\n\nValidates:\n- LLM selection against allowed list\n- Toolset availability\n- Runtime home path\n- Tool allowlist compliance\n\n### Audit Logging\n\n```python\nasync def emit(\n *,\n startup_id: int | str,\n assistant_id: int | str | None,\n tool: str,\n args: dict[str, Any],\n result_status: str,\n tier_at_call: str,\n jwt: str,\n ...\n) -> bool\n```\n\nEmits to Harakat audit endpoint with:\n- Tool call arguments (hashed)\n- Tier at call time\n- Risk level\n- Mutation/outbound flags\n\n## Troubleshooting\n\n### Common Issues\n\n1. **Empty final response**\n - Check for thinking model returning reasoning-only\n - Verify `HERMES_FINALIZE_TIMEOUT_S` is sufficient\n - Check for unfulfilled claim guard firing\n\n2. **Child agent not converging**\n - Check `rounds` vs `iteration_limit`\n - Verify `reasoning_config` for load-bearing models\n - Check `child_cost_usd` for pricing issues\n\n3. **Budget not seeing child costs**\n - Verify `RunBudget` is attached to session\n - Check `bill_child_to_run` is called\n - Verify model is in `core/llm_pricing.py`\n\n4. **Honcho memory poisoning creation**\n - Verify `creation_run` flag is set\n - Check `HONCHO_ENABLED` is `on`\n - Review logs for \"Honcho user-memory block withheld\"\n\n## Contributing\n\n### Adding a New Subagent\n\n1. Create `category_agents/<name>_subagent.py`\n2. Define `TOOLSET` frozenset of allowed tools\n3. Implement `run_<name>_subagent()` function\n4. Add to `category_agents/__init__.py` exports\n5. Update `validate_category_child_policy` if needed\n\n### Adding a New Tool\n\n1. Define `BridgedTool` in `tool_bridge.py`\n2. Add to tool registry via `as_hermes_tools()`\n3. Set appropriate `min_tier` and `require_role`\n4. Add audit logging in tool executor\n\n### Adding a New Policy\n\n1. Extend `validate_native_runtime_policy()`\n2. Add to `runtime_policy.py`\n3. Document in policy comments\n4. Add test coverage\n\n## Related Modules\n\n- `agents/hermes_native/harakat_toolset.py` — Tool registration and context\n- `agents/hermes_native/memory_setup.py` — Memory manager setup\n- `agents/hermes_native/skills_sync.py` — Skills/policies/procedures sync\n- `agents/hermes_native/honcho_user_memory.py` — Honcho user memory\n- `agents/hermes_native/usage_events.py` — Usage event tracking\n- `agents/hermes_native/system_prompt.py` — System prompt building\n- `agents/hermes_native/tenant_vocabulary.py` — Tenant vocabulary section","harakat-ai-bench":"# Harakat_AI — bench\n\n\n\n# Harakat_AI — bench/headroom\n\n## Overview\n\nThe `bench/headroom` module is an **offline, read-only benchmark suite** for evaluating the Headroom AI context compression system. It is **never imported by the live AI service** and operates in an isolated virtual environment.\n\n**Purpose**: Measure how well Headroom compresses assistant context while preserving answer quality, across real startup-19 traces and synthetic fixtures.\n\n**Output**:\n- `scratchpad/p5bench/results.json` — per-payload compression metrics\n- `scratchpad/p5bench/results_matrix.json` — per-surface × per-model quality scores\n- `docs/architecture/p5_headroom_offline_benchmark.md` — verdict report\n- `docs/architecture/p5_headroom_matrix.md` — plain-English results\n- `docs/architecture/p5_headroom_token_economics.md` — token cost analysis\n\n---\n\n## Architecture\n\n```mermaid\nflowchart TB\n subgraph \"Input\"\n FIX[fixtures/*.json]\n end\n \n subgraph \"Compression\"\n NOOP[NoOpCompressor]\n HERMES[HermesBaselineCompressor]\n HEADROOM[HeadroomCompressor]\n end\n \n subgraph \"Judging\"\n JUDGE[judge.py]\n end\n \n subgraph \"Reports\"\n REPORT_MATRIX[report_matrix.py]\n REPORT_TOKENS[report_tokens.py]\n REPORT[report.py]\n end\n \n FIX --> NOOP\n FIX --> HERMES\n FIX --> HEADROOM\n \n NOOP --> JUDGE\n HERMES --> JUDGE\n HEADROOM --> JUDGE\n \n JUDGE --> REPORT_MATRIX\n JUDGE --> REPORT_TOKENS\n JUDGE --> REPORT\n```\n\n---\n\n## Core Components\n\n### 1. Compression Strategies (`compressors.py`)\n\nThree strategies are compared:\n\n| Strategy | Description |\n|----------|-------------|\n| `noop` | No compression (baseline) |\n| `hermes_baseline` | Hermes's existing head+tail truncation (2000 chars head + 800 chars tail) |\n| `headroom` | Headroom AI's SmartCrusher (JSON) + Kompress pipeline (prose/code) |\n\n**HeadroomCompressor** routes content by type:\n- **JSON** → `SmartCrusher.crush()` (lossless, reversible)\n- **Prose/Code** → Kompress pipeline (frames as tool message, uses `protect_recent=0`, `target_ratio=0.3`)\n\n```python\n# Example compression result\nCompressionResult(\n strategy=\"headroom_smartcrusher:lossless:table\",\n original=\"...\",\n compressed=\"...\",\n original_tokens=1500,\n compressed_tokens=600,\n latency_ms=45,\n content_type=\"json_array\"\n)\n```\n\n### 2. Eligibility Rules (`eligibility.py`)\n\nContent is classified and filtered before compression:\n\n```python\nNEVER_COMPRESS_SOURCES = {\n \"user_message\", \"approved_args\", \"outbound_email\",\n \"policy\", \"permission_decision\"\n}\n\ndef is_eligible(source_type, text, min_tokens=200) -> bool:\n if source_type in NEVER_COMPRESS_SOURCES:\n return False\n if count_tokens(text) < min_tokens:\n return False\n return True\n```\n\n**Content classification**:\n- JSON arrays/objects (detected by `[` or `{` prefix)\n- Code (regex for `def `, `class `, `=>`, `import `, etc.)\n- Text (default)\n\n### 3. Harvesting Real Traces\n\nTwo harvest scripts populate the fixture directory:\n\n**`harvest.py`** — Live tool execution (read-only):\n- Phase 1: `search_documents` queries (JSON payloads)\n- Phase 2: `get_document_content` for document prose (text payloads)\n\n**`harvest_matrix.py`** — Inner-content artifacts:\n- `rag_page` — Decrypted document content (what flows to LLM)\n- `rag_candidates` — Retrieved candidate list (JSON)\n- `static_tools` — Tool catalog (near-always-loaded)\n- `static_policy` — PLANNER_ROUTING_RULE\n\n```python\n# Fixture structure\n{\n \"surface\": \"rag_page\",\n \"id\": \"rag_page_0\",\n \"content_type\": \"text\",\n \"text\": \"The actual document content...\",\n \"question\": \"Summarize the key facts, names, and figures\",\n \"query\": \"budget\"\n}\n```\n\n### 4. Judging (`judge.py`)\n\nQuality assessment using DeepInfra Qwen:\n\n```python\ndef judge_equivalence(question, ctx_original, ctx_compressed, model) -> dict:\n a_orig, u_orig = answer_with_usage(question, ctx_original, model)\n a_comp, u_comp = answer_with_usage(question, ctx_compressed, model)\n score = judge_score_only(question, a_orig, a_comp, model)\n return {\n \"score\": score,\n \"answer_full\": a_orig,\n \"answer_compressed\": a_comp,\n \"usage_full\": u_orig,\n \"usage_compressed\": u_comp\n }\n```\n\n**Reasoning modes**:\n- `P5_THINK=off` (default) — fast, direct answers\n- `P5_THINK=on` — guided reasoning for compressed/reformatted context\n\n### 5. Reporting\n\nThree report generators:\n\n| Report | Purpose |\n|--------|---------|\n| `report.py` | Verdict + per-payload table |\n| `report_matrix.py` | Plain-English per-surface analysis |\n| `report_tokens.py` | Token economics (input saved vs output added) |\n\n---\n\n## Execution Flow\n\n### Full Benchmark Run\n\n```bash\n# 1. Create isolated venv\ncd Harakat_AI\npython -m venv bench/headroom/.venv\nbench/headroom/.venv/Scripts/python -m pip install -U pip\nbench/headroom/.venv/Scripts/python -m pip install \"headroom-ai[all]\" openai pytest\nbench/headroom/.venv/Scripts/python -m pip install -e vendor/hermes-agent\n\n# 2. Harvest fixtures (needs owner JWT)\nexport P5BENCH_DIR=\".../scratchpad/p5bench\"\nexport P5_JWT=\"<owner login JWT>\"\nexport P5_STARTUP_ID=19\nexport P5_ASSISTANT_ID=1\npython -m bench.headroom.harvest\n\n# 3. Run compression matrix\nbench/headroom/.venv/Scripts/python -m bench.headroom.run_matrix\n\n# 4. Run full benchmark with judge\nexport P5_RUN_JUDGE=1\nbench/headroom/.venv/Scripts/python -m bench.headroom.run_benchmark\n```\n\n### Honcho Embedding Matrix\n\nFor testing different embedding models with Honcho chat memory:\n\n```bash\n# Generate .env for one cell\npython bench/headroom/honcho/gen_env.py gemini2 off\n\n# Run eval\npython bench/headroom/honcho/honcho_eval.py --embed gemini2 --think off\n\n# Drive full matrix\nbash bench/headroom/honcho/run_matrix.sh gemini2:off gemini2:low qwen8b:off ...\n```\n\n---\n\n## Key Metrics\n\n### Compression Metrics\n- **reduction_pct** = `(original_tokens - compressed_tokens) / original_tokens * 100`\n- **latency_ms** — compression time\n- **strategy** — which compressor was used\n\n### Quality Metrics\n- **judge_score** — 0-100 equivalence score (100 = no factual loss)\n- **noop_judge_score** — anchor (should be 100 for no-op)\n- **quality_regression** = `noop_judge_score - judge_score`\n\n### Token Economics\n- **input_saved** = `usage_full.input - usage_compressed.input`\n- **output_added** = `usage_compressed.output - usage_full.output`\n- **net_cost** = `-input_saved + (output_added * out_in_ratio)`\n\n---\n\n## Integration Points\n\n### External Dependencies\n| Module | Purpose |\n|--------|---------|\n| `core.tools` | Tool execution (harvest) |\n| `core.config` | LLM selection (judge) |\n| `vendor/hermes-agent` | Hermes context compressor |\n| `headroom-ai` | Headroom compression library |\n\n### Environment Variables\n```bash\nP5BENCH_DIR # Output directory\nP5_JWT # Owner JWT for harvest\nP5_STARTUP_ID # Startup ID (default: 19)\nP5_ASSISTANT_ID # Assistant ID (default: 1)\nP5_RUN_JUDGE # Enable judge (1/0)\nP5_MODELS # Judge models (comma-separated)\nP5_JUDGE_MODEL # Override judge model\nP5_THINK # Enable reasoning (on/off)\nP5_OUT_IN_RATIO # Output token price multiplier (default: 3)\n```\n\n---\n\n## Test Coverage\n\n### Unit Tests\n```bash\ncd Harakat_AI && python -m pytest bench/headroom/tests/ -v\n```\n\n### Synthetic Fixtures\n`synthetic.py` generates:\n- **ERP tool catalog** (~110 tools) — tests large static context\n- **Long conversation** (~56 turns) — tests chat memory compression\n\n---\n\n## Decision Thresholds\n\nThe verdict passes if all thresholds are met:\n\n| Metric | Threshold |\n|--------|-----------|\n| Median token reduction | ≥ 40% |\n| Median quality regression | ≤ 3 points |\n| Worst class judge score | ≥ 70 (no-op ≥ 90) |\n| p95 latency | ≤ 750ms |\n\n---\n\n## Contributing\n\n### Adding a New Compressor\n\n1. Inherit from `CompressionResult` dataclass\n2. Implement `compress(text, content_type, query)` → `CompressionResult`\n3. Register in `run_benchmark.py` alongside existing strategies\n\n### Adding a New Report\n\n1. Read `results.json` or `results_matrix.json`\n2. Use `render_markdown()` or `render_plain()` patterns\n3. Write to `docs/architecture/`\n\n### Adding a New Surface\n\n1. Update `SURFACE_INFO` in `report_matrix.py`\n2. Ensure harvest scripts capture the surface\n3. Add to `NEVER_COMPRESS_SOURCES` if applicable\n\n---\n\n## Notes\n\n- **Never modifies tenant data** — all operations are read-only\n- **Deterministic compressor** — reduction is model-independent\n- **Judge is LLM-based** — quality scores vary by model\n- **Isolated venv** — does not pollute AI service environment\n- **Content-free reports** — no tenant text in output files","harakat-ai-capabilities":"# Harakat_AI — capabilities\n\n\n\n# Harakat_AI — Capabilities Module\n\nThis module manages the lifecycle of learned capabilities within the Harakat platform. It provides three primary interfaces:\n1. **Client Helpers**: Direct wrappers for the server-side API to manage capability creation, specification, and training.\n2. **Definition Chat**: Utilities for validating and summarizing capability definitions during conversational interactions.\n3. **Augmentation Framework**: A pluggable system for generating synthetic data variants to improve model training.\n\n## Module Architecture\n\nThe module is split into three logical sub-modules, each handling a distinct aspect of capability management.\n\n```mermaid\ngraph TD\n Tools[core/tools.py]\n Capabilities[Harakat_AI/capabilities]\n \n subgraph Capabilities\n Client[client.py<br/>Lifecycle API]\n Def[definition_chat.py<br/>Validation & Drafts]\n Aug[augmentation.py<br/>Synthetic Data]\n end\n \n Tools -->|Propose/Train/Upsert| Client\n Tools -->|Validate/Summarize| Def\n Tools -->|Generate Variants| Aug\n \n Client -->|HTTP| Server[Offices Server]\n Aug -->|HTTP| Server\n Def -->|Dataclass| Client\n```\n\n## 1. Capability Lifecycle (`client.py`)\n\nThis file contains the HTTP client wrappers for the server-side capability API. These functions are the entry point for agents to interact with the capability registry.\n\n### Lifecycle Operations\n* **`propose_capability`**: Creates a new capability in `IN_DEFINITION` status.\n * **Required**: `name`, `description`, `category`, `permitted_action_types`.\n * **Optional**: `assistant_id`, `access_tier`, `branch_scope`.\n * **Action Types**: Must be a subset of `ALLOWED_ACTION_TYPES` (e.g., `score`, `flag`, `draft_email`).\n* **`upsert_capability_spec`**: Updates the technical specification of an existing capability.\n * **Required**: `capability_id`, `prediction_target`, `signal_columns`.\n * **Optional**: `decision_threshold`, `audit_cadence`, `notes`.\n* **`start_capability_training`**: Triggers the server-side learning sandbox.\n * **Parameters**: `capability_id`, `force` (bool).\n * **Timeout**: 45.0s (longer than standard ops due to training init).\n* **`list_capabilities` / `list_opportunities`**: Query endpoints for discovery.\n * `list_capabilities`: Filters by `status`, `assistant_id`, `limit`.\n * `list_opportunities`: Filters by `dismissed`, `accepted`, `limit`.\n\n### Error Handling\nAll client functions use `httpx.AsyncClient` with `resp.raise_for_status()`. They return the JSON response body directly. If an HTTP error occurs, an exception is raised.\n\n## 2. Definition & Validation (`definition_chat.py`)\n\nThis module bridges conversational inputs with the strict data structures required by the server. It ensures that chat-derived definitions are valid before being persisted.\n\n### `CapabilityDefinitionDraft`\nA dataclass representing a capability in progress. It normalizes incoming data (handling `permittedActionTypes` vs `permitted_action_types` casing differences).\n\n* **`from_mapping`**: Converts a raw dictionary (from chat or API) into a `CapabilityDefinitionDraft`.\n* **`missing_definition_fields`**: Returns a list of required fields that are empty or invalid.\n * Checks for `name`, `description`, `prediction_target`, `signal_columns`, `permitted_action_types`.\n * Validates `permitted_action_types` against `ALLOWED_ACTION_TYPES`.\n* **`summarize_definition_draft`**: Generates a human-readable string for tool output or user display.\n * Formats signals and actions as comma-separated lists.\n * Defaults to \"not set yet\" for missing values.\n\n### Constants\n* **`ALLOWED_ACTION_TYPES`**: `{\"score\", \"flag\", \"draft_email\", \"send_email\", \"modify_record\"}`. Used to validate permitted actions during definition.\n\n## 3. Augmentation Framework (`augmentation.py`)\n\nThis module implements a pluggable framework for generating synthetic data variants for active capabilities. It is designed to be \"smoke-friendly\": if no generator is registered for a category, the framework skips gracefully without crashing.\n\n### Core Concepts\n* **`GeneratorFn`**: An async callable `Callable[[GeneratorContext], Awaitable[list[AugmentedSample]]]`.\n* **`GeneratorContext`**: Passed to the generator. Contains `auth_token`, `capability`, `spec`, `recent_real_samples`, and `target_variant_count`.\n* **`AugmentedSample`**: The output variant. Contains `sample` (dict) and optional `prompt_text`.\n* **`GenerationOutcome`**: A dataclass returned by the framework to report success/failure metrics without raising exceptions.\n\n### Registration\nGenerators are registered via `register_augmentation_generator(category, version, generator)`.\n* **Scoping**: Generators are scoped by `(category, version)`.\n* **Selection**: `_pick_generator` selects the latest version registered for a category.\n* **Safety**: Duplicate registration raises `ValueError`.\n\n### Execution Flow\nThe main entry point is `generate_augmentation_samples_for_capability`. It follows this logic:\n1. **Fetch Capability**: Retrieves capability details. If failed, returns `GenerationOutcome` with `skipped_reason`.\n2. **Check Category**: If no category exists, skips.\n3. **Pick Generator**: Looks up the generator for the category. If none found, skips.\n4. **Fetch Samples**: Retrieves recent real samples from the server.\n5. **Generate**: Calls the registered `GeneratorFn`.\n6. **Enqueue**: Iterates through generated variants and posts them to the server's enqueue endpoint.\n7. **Report**: Returns `GenerationOutcome` with counts of requested, produced, and enqueued variants, plus any errors.\n\n### Batch Processing\n`generate_augmentation_samples_for_capabilities` iterates over a list of `capability_ids` serially. This ensures predictable DB and LLM load.\n\n### Diagram: Augmentation Flow\n```mermaid\nsequenceDiagram\n participant Agent\n participant Framework as generate_augmentation_samples_for_capability\n participant Server as Harakat Server\n participant Gen as Registered Generator\n\n Agent->>Framework: generate_augmentation_samples_for_capability(cap_id)\n Framework->>Server: GET /capabilities/{cap_id}\n Server-->>Framework: Capability Data\n Framework->>Framework: _pick_generator(category)\n alt Generator Found\n Framework->>Server: GET /augmentation/recent-real-samples\n Server-->>Framework: Real Samples\n Framework->>Gen: Generate Variants(samples)\n Gen-->>Framework: AugmentedSamples\n loop For each variant\n Framework->>Server: POST /augmentation/enqueue\n Server-->>Framework: Success\n end\n Framework-->>Agent: GenerationOutcome(success=true)\n else No Generator\n Framework-->>Agent: GenerationOutcome(skipped=true)\n end\n```\n\n## 4. Integration with Core Tools\n\nThe `capabilities` module is primarily consumed by `core/tools.py`. The call graph indicates the following integration points:\n\n* **Definition Phase**:\n * `_propose_predictive_capability` calls `missing_definition_fields` and `summarize_definition_draft` to validate chat inputs before calling `propose_capability`.\n * `_update_predictive_capability_spec` calls `from_mapping` to parse updates and `upsert_capability_spec` to persist them.\n* **Discovery Phase**:\n * `_list_predictive_capabilities` calls `list_capabilities`.\n * `_list_capability_opportunities` calls `list_opportunities`.\n* **Training Phase**:\n * `_start_predictive_capability_learning` calls `start_capability_training`.\n* **Augmentation Phase**:\n * While not explicitly shown in the provided call graph, the augmentation framework is designed to be invoked by a scheduler or tool that manages active capability maintenance.\n\n## 5. Extensibility Guidelines\n\n### Adding a New Generator\nTo add a new augmentation strategy:\n1. Implement a function matching the `GeneratorFn` signature.\n2. Register it using `register_augmentation_generator(category=\"your_category\", version=\"1.0\", generator=your_func)`.\n3. Ensure the function returns a list of `AugmentedSample` objects.\n4. Handle transient errors internally; the framework expects the generator to be pure with respect to inputs.\n\n### Adding a New Client Endpoint\nIf the server API changes:\n1. Update the corresponding function in `client.py`.\n2. Ensure `_headers` is used for authentication.\n3. Update `__init__.py` exports if the function is public.\n\n### Adding a New Definition Field\n1. Add the field to `CapabilityDefinitionDraft`.\n2. Update `from_mapping` to handle the new key.\n3. Add validation logic to `missing_definition_fields`.\n4. Update `summarize_definition_draft` to display the new field.","harakat-ai-core":"# Harakat_AI — core\n\n\n\n# Harakat_AI — core Module Documentation\n\n## Overview\n\nThe `Harakat_AI/core` module provides the foundational infrastructure for Harakat's AI-powered document processing, batch inference, browser automation, and multi-provider LLM orchestration. It serves as the central hub for:\n\n- **Batch AI inference** (OCR, embeddings, entity extraction, document summarization)\n- **Browser automation** (Google Meet joining, audio bridging)\n- **Configuration management** (multi-provider LLM routing, environment validation)\n- **Context governance** (vocabulary learning, fact candidate flagging)\n- **Document planning** (structured document rendering schemas)\n\nThe module is designed for **stateless server-side processing** where the server owns accumulation and ingestion, while endpoints trigger batch jobs that run asynchronously.\n\n---\n\n## Architecture\n\n```mermaid\ngraph TB\n subgraph \"AI Endpoints\"\n A[Parse/OCR] --> B[batch_ocr.py]\n C[Parse/Desc] --> D[batch_desc.py]\n E[Parse/Entities] --> F[batch_entities.py]\n G[Parse/Embed] --> H[batch_embed.py]\n I[Parse/Rollup] --> J[batch_rollup.py]\n K[Parse/TOC] --> L[batch_toc.py]\n end\n \n subgraph \"GCS Staging\"\n B --> M[GCS Upload]\n D --> M\n F --> M\n H --> M\n J --> M\n L --> M\n end\n \n subgraph \"Vertex Batch Jobs\"\n M --> N[submit_batch]\n N --> O[Vertex AI]\n O --> P[Poll Results]\n P --> Q[parse_results]\n end\n \n subgraph \"Browser Agent\"\n R[FastAPI Endpoint] --> S[BrowserAgentManager]\n S --> T[BrowserSession]\n T --> U[CDP Page]\n T --> V[Audio Bridge]\n V --> W[Gemini Live API]\n end\n \n subgraph \"Configuration\"\n X[config.py] --> Y[LLM Selection]\n Y --> Z[Vertex/Gemini/DeepInfra/Groq]\n end\n```\n\n---\n\n## Batch Processing Modules\n\n### Core Pattern: GCS + Vertex Batch Plumbing\n\nAll batch modules (`batch_ocr`, `batch_desc`, `batch_entities`, `batch_rollup`, `batch_toc`, `batch_embed`) share a common architecture:\n\n1. **Stage** → Upload manifest to GCS\n2. **Submit** → Create Vertex batch job\n3. **Poll** → Wait for terminal state\n4. **Read** → Parse output JSONL\n\nThe `batch_ocr` module provides the shared plumbing (`core.batch_ocr as B`) that other modules reuse.\n\n### `batch_ocr.py` — Batch OCR Pipeline\n\n**Purpose:** Process scanned PDFs and images via Vertex AI's Gemini 3 Flash. Returns verbatim page text + document description.\n\n**Key Functions:**\n\n| Function | Description |\n|----------|-------------|\n| `submit_items(items, display_name)` | Stages manifest and submits batch job. Returns `(job_name, dest_uri)` |\n| `fetch_results(job_name, dest)` | Polls job once. Returns `(state, results)` |\n| `build_manifest(items)` | Creates JSONL manifest from `BatchItem` objects |\n| `parse_results(jsonl_text)` | Parses output JSONL into `BatchResult` objects |\n\n**Data Structures:**\n\n```python\n@dataclass\nclass BatchItem:\n key: str # Stable ID (e.g., \"node-{nodeId}\")\n gcs_uri: str # gs://bucket/.../<file>.pdf\n mime_type: str = \"application/pdf\"\n\n@dataclass\nclass BatchResult:\n key: str\n text: str # Pages joined with \"\\n\\n---\\n\\n\"\n description: str\n page_count: int\n error: str | None = None\n```\n\n**Schema:**\n```json\n{\n \"pages\": [\n {\"page\": 1, \"text\": \"...\"},\n {\"page\": 2, \"text\": \"...\"}\n ],\n \"description\": \"Document type and purpose\"\n}\n```\n\n**Usage:**\n```python\nfrom core.batch_ocr import BatchItem, submit_items, fetch_results\n\nitems = [\n BatchItem(key=\"node-123\", gcs_uri=\"gs://bucket/doc.pdf\"),\n BatchItem(key=\"node-124\", gcs_uri=\"gs://bucket/doc2.pdf\"),\n]\n\njob_name, dest = submit_items(items, display_name=\"ocr-job-1\")\nstate, results = fetch_results(job_name)\n```\n\n---\n\n### `batch_desc.py` — Batch Description + Abstract\n\n**Purpose:** Generate document descriptions, abstracts, and topic tags via Vertex (Gemini 3 Flash). Text-only, temperature 1.0.\n\n**Key Functions:**\n\n| Function | Description |\n|----------|-------------|\n| `submit_items(items, display_name)` | Stages manifest and submits batch job |\n| `fetch_results(job_name, dest)` | Polls job once |\n| `build_request(item)` | Builds single Vertex request |\n| `parse_result_line(raw_line)` | Parses one JSONL line into `DescResult` |\n\n**Data Structures:**\n\n```python\n@dataclass\nclass DescItem:\n key: str # Stable ID\n text: str # Cleaned document text\n file_name: str = \"\"\n\n@dataclass\nclass DescResult:\n key: str\n description: str\n abstract: str\n topic_tags: list[str]\n error: str | None = None\n```\n\n**System Prompt:**\n```\nYou are a precise document analyst. Given a document's filename and its \nextracted text, produce strict JSON with three fields: \n(1) 'description' — 2-3 sentences stating what the document IS \n(2) 'abstract' — one paragraph capturing the key content \n(3) 'topicTags' — 3 to 8 short topic keywords.\n```\n\n---\n\n### `batch_entities.py` — Batch Entity + Relation Extraction\n\n**Purpose:** Extract structured entities and relations from documents via Vertex (Gemini Flash). Temperature 0.2 for determinism.\n\n**Key Functions:**\n\n| Function | Description |\n|----------|-------------|\n| `submit_items(items, display_name)` | Stages manifest and submits batch job |\n| `fetch_results(job_name, dest)` | Polls job once |\n| `parse_result_line(raw_line)` | Parses one JSONL line into `EntityResult` |\n\n**Data Structures:**\n\n```python\n@dataclass\nclass EntityItem:\n key: str\n text: str\n mm_description: str = \"\" # Optional multimodal description\n file_name: str = \"\"\n\n@dataclass\nclass EntityResult:\n key: str\n entities: list[dict[str, Any]]\n relations: list[dict[str, Any]]\n error: str | None = None\n```\n\n**Schema:**\n```json\n{\n \"entities\": [\n {\n \"name\": \"Entity Name\",\n \"type\": \"person|org|project|date|money|other\",\n \"mentions\": [{\"page\": 1}],\n \"confidence\": 0.95\n }\n ],\n \"relations\": [\n {\n \"subject\": \"Entity A\",\n \"predicate\": \"REFERENCES\",\n \"object\": \"Entity B\"\n }\n ]\n}\n```\n\n**Predicate Values:** `REFERENCES`, `AUTHORED_BY`, `BUDGET_FOR`, `DISCUSSED_IN`, `DEPENDS_ON`, `VERSION_OF`, `RELATED_TO`\n\n---\n\n### `batch_embed.py` — Batch Embeddings\n\n**Purpose:** Generate document embeddings via Google GenAI SDK's native batch API. Uses `gemini-embedding-2` with 768 dimensions.\n\n**Key Functions:**\n\n| Function | Description |\n|----------|-------------|\n| `submit_texts(texts, display_name)` | Creates embeddings batch job |\n| `fetch_results(job_name)` | Polls job once |\n| `extract_vectors(dest_responses)` | Extracts vectors from job responses |\n\n**Usage:**\n```python\nfrom core.batch_embed import submit_texts, fetch_results\n\ntexts = [\"Document 1 text...\", \"Document 2 text...\"]\njob_name = submit_texts(texts, display_name=\"embed-job-1\")\nstate, vectors = fetch_results(job_name)\n# vectors = [[0.1, 0.2, ...], [0.3, 0.4, ...], None] # None = error\n```\n\n**Note:** Results map to inputs by index — the server owns the key<->order mapping.\n\n---\n\n### `batch_rollup.py` — Batch Folder Rollup\n\n**Purpose:** Generate one-paragraph summaries of folders for retrieval indexing. Text-only, temperature 1.0.\n\n**Data Structures:**\n\n```python\n@dataclass\nclass RollupItem:\n key: str\n folder_name: str\n child_summaries: list[dict[str, Any]] # {title, abstract, topicTags?}\n\n@dataclass\nclass RollupResult:\n key: str\n rollup: str\n error: str | None = None\n```\n\n**System Prompt:**\n```\nYou are writing a one-paragraph summary of a folder for a retrieval index. \nGiven the folder name and its child item summaries, return ONLY strict JSON \n{rollup: a 3-6 sentence paragraph describing what this folder covers, what kinds \nof documents live in it, and any recurring themes}.\n```\n\n---\n\n### `batch_toc.py` — Batch Tenant Table of Contents\n\n**Purpose:** Generate tenant-wide TOC digests for AI planners. Temperature 0.4 for focus.\n\n**Data Structures:**\n\n```python\n@dataclass\nclass TocItem:\n key: str\n startup_id: int\n folder_rollups: list[dict[str, Any]] # {nodeId, path, rollup, childCount, accessTier?}\n\n@dataclass\nclass TocResult:\n key: str\n digest: str\n structure: dict[str, Any] # folder path -> one-sentence label\n error: str | None = None\n```\n\n**System Prompt:**\n```\nYou are writing a tenant-wide table-of-contents digest for a retrieval index, \nshown to an AI planner BEFORE it decides where to search. Return ONLY strict JSON \n{digest: a 4-8 sentence paragraph naming top-level themes and main folders \n(refer to folders by path, never specific document titles — it is redacted per-user later); \nstructure: object mapping folder path -> a one-sentence label}.\n```\n\n---\n\n## Browser Agent Module\n\n### `browser_agent.py` — Google Meet Join + Audio Bridging\n\n**Purpose:** Automate Google Meet joining via browser-use Agent, with bidirectional audio bridging to Gemini Live API.\n\n**Architecture:**\n\n```mermaid\nsequenceDiagram\n participant FE as Frontend\n participant API as FastAPI\n participant Manager as BrowserAgentManager\n participant Agent as browser-use Agent\n participant CDP as CDP Page\n participant Live as Gemini Live API\n \n FE->>API: POST /join-meeting\n API->>Manager: start_meeting_session()\n Manager->>Agent: Create Agent with task\n Agent->>CDP: Navigate to meeting URL\n CDP->>Agent: Screenshot + URL\n Agent->>CDP: Click \"Join\"\n CDP->>Agent: Screenshot\n Agent->>Live: Send audio (16kHz PCM)\n Live->>Agent: Respond (24kHz PCM)\n Agent->>CDP: Inject audio (virtual mic)\n CDP->>Agent: Screenshot\n Agent->>API: Stream step events (SSE)\n API->>FE: Stream SSE events\n```\n\n**Key Functions:**\n\n| Function | Description |\n|----------|-------------|\n| `get_browser_agent_manager()` | Singleton manager for sessions |\n| `start_meeting_session(...)` | Launches browser agent to join meeting |\n| `stop_session(session_id)` | Stops session and cleans up |\n| `_run_browser_agent(session, task)` | Runs browser-use agent loop |\n| `_run_audio_bridge(session, cdp_page)` | Bidirectional audio bridge |\n\n**Data Structures:**\n\n```python\n@dataclass\nclass BrowserStepEvent:\n type: str = \"browser_step\"\n session_id: str\n step: int\n url: str\n title: str\n screenshot_b64: str # Base64 PNG\n action_text: str\n thinking: str\n status: str # running | waiting_for_user | done | error\n error: str | None\n prompt: str | None # For user intervention (e.g., sign-in required)\n\n@dataclass\nclass BrowserSession:\n session_id: str\n meeting_url: str\n title: str\n auth_token: str\n queue: asyncio.Queue\n status: str\n _agent: Any\n _cdp_page: CDPPage\n _chrome_process: subprocess.Popen\n _ext_dir: str # Audio extension directory\n```\n\n**Critical Rules:**\n- **NEVER** click \"Continue without microphone and camera\"\n- Camera must be OFF, microphone must stay ON\n- Pre-grant permissions via Chrome profile + CDP `Browser.setPermission`\n- Use CDP WebSocket directly (no Playwright subprocess on Windows)\n\n**Audio Bridge:**\n- Browser→Gemini: 16kHz PCM from `__hrkDrainPcm()` → Live API input\n- Gemini→Browser: Live API 24kHz PCM → `__hrkInjectAudio()` → virtual mic\n\n---\n\n## Configuration Module\n\n### `config.py` — Multi-Provider LLM Orchestration\n\n**Purpose:** Central configuration for all LLM providers, with runtime resolution and credential management.\n\n**Key Functions:**\n\n| Function | Description |\n|----------|-------------|\n| `resolve_llm_selection(chosen)` | Resolves (provider, model, api_key, base_url) |\n| `mint_vertex_access_token()` | OAuth2 token for Vertex AI OpenAI endpoint |\n| `allowed_llm_selections()` | Permitted provider→model pairs |\n| `search_subagent_enabled()` | Check if search subagent is active |\n| `doc_subagent_enabled()` | Check if document subagent is active |\n\n**Supported Providers:**\n\n| Provider | Model | Base URL |\n|----------|-------|----------|\n| gemini | HERMES_MODEL | GEMINI_BASE_URL |\n| deepinfra | DEEPINFRA_MODEL | DEEPINFRA_BASE_URL |\n| groq | GROQ_MODEL | GROQ_BASE_URL |\n| parasail | PARASAIL_MODEL | PARASAIL_BASE_URL |\n| openrouter | OPENROUTER_MODEL | OPENROUTER_BASE_URL |\n| nebius | NEBIUS_MODEL | NEBIUS_BASE_URL |\n\n**Vertex OAuth Token Minting:**\n```python\ndef mint_vertex_access_token() -> str:\n \"\"\"Mint OAuth2 token from service account for Vertex AI OpenAI endpoint.\"\"\"\n # Cached module-wide, auto-refreshed on expiry\n return _vertex_creds.token\n```\n\n**Environment Variables:**\n\n```python\n# LLM Selection\nLLM_CHOSEN: str = \"gemini\" # or \"deepinfra\", \"groq\", etc.\n\n# Search Subagent\nSEARCH_SUBAGENT: str = \"off\"\nSEARCH_SUBAGENT_MODEL: str = \"Qwen/Qwen3-Next-80B-A3B-Instruct\"\n\n# Document Subagent\nDOC_SUBAGENT: str = \"off\"\nDOC_SUBAGENT_MODEL: str = \"Qwen/Qwen3.5-397B-A17B\"\n\n# Vertex AI\nVERTEX_SERVICE_ACCOUNT_JSON: str = \"./service-account.json\"\nVERTEX_PROJECT_ID: str = \"\"\nVERTEX_REGION: str = \"global\"\nVERTEX_MODEL: str = \"gemini-3-flash-preview\"\n```\n\n---\n\n## Context Gate Module\n\n### `context_gate.py` — Vocabulary Learning Gate\n\n**Purpose:** Decide whether a retrieval event teaches something durable about a folder/document. Only adds `vocabulary` blocks; never writes facts.\n\n**Key Function:**\n\n| Function | Description |\n|----------|-------------|\n| `judge_proposal(event, node_name, node_path, search_card, current_context)` | Runs one proposal through the gate |\n\n**Rubric:**\n```\nADD a vocabulary block only when the event shows how people genuinely refer to\nthis material and the phrasing is NOT already in the context or the search\ncard — e.g. the query used a nickname, an abbreviation, a project alias, or a\nplain-words phrasing that found this document only indirectly.\n\nREJECT when any of these hold:\n- the query already matches the search card or context\n- the phrasing is about the user's momentary task\n- one-off phrasing you would not expect a second person to type\n- the event is a plain use of a known document under its known name\n- the wording embeds an assumption the material does not support\n\nFLAG fact_candidate when the event suggests the stored facts may be wrong or\nstale (e.g. the user corrected a party name or amount).\n```\n\n**Hard Rules (enforced outside the model):**\n- Gate can only add `vocabulary` blocks\n- Cannot write facts\n- Accepted blocks are bounded per node (top-K by recency+frequency)\n- Inherit node's access tier\n\n**Response Format:**\n```json\n{\n \"verdict\": \"add\" | \"reject\" | \"fact_candidate\",\n \"block\": {\n \"kind\": \"vocabulary\",\n \"text\": \"<the phrase(s) worth remembering>\"\n } | null,\n \"reason\": \"<one short sentence>\"\n}\n```\n\n---\n\n## Document Plan Module\n\n### `document_plan.py` — Structured Document Rendering Schema\n\n**Purpose:** Define the vocabulary of blocks that business documents are made of. CLOSED vocabulary — models cannot invent new types.\n\n**Block Types:**\n```python\nBLOCK_TYPES = (\n \"letterhead\", # sender info\n \"title_block\", # document type + reference + date\n \"parties\", # from / to side by side\n \"fields\", # label:value metadata\n \"heading\",\n \"paragraph\",\n \"bullets\",\n \"table\",\n \"totals\", # money band\n \"notes\", # terms, bank details\n \"signatures\", # name + role + signature line\n \"image\", # picture from workspace\n)\n```\n\n**Data Structures:**\n\n```python\n@dataclass\nclass DocumentPlan:\n blocks: list[dict[str, Any]]\n meta: dict[str, Any] # title, reference, direction\n unknown_types: list[str] # Types not in vocabulary (reported, not dropped)\n\n@dataclass\nclass LegacyDowngrade:\n sections: list[dict[str, Any]] # Flattened for old renderers\n lost: list[str] # What flattening couldn't carry\n```\n\n**Backward Compatibility:**\n- `normalize_plan()` accepts EITHER new blocks OR old sections\n- Always returns blocks\n- `to_legacy_sections()` flattens for old renderers (lossy)\n\n---\n\n## Integration Points\n\n### With `agents/meeting/agent.py`\n```python\n# Browser agent integration\nfrom core.browser_agent import get_browser_agent_manager\n\nmanager = get_browser_agent_manager()\nsession = manager.start_meeting_session(\n session_id=\"meet-123\",\n meeting_url=\"https://meet.google.com/abc-def-ghi\",\n title=\"Executive Review\",\n auth_token=\"...\",\n)\n```\n\n### With `agents/hermes_native/*`\n```python\n# LLM selection for subagents\nfrom core.config import resolve_search_subagent_selection, resolve_doc_subagent_selection\n\nsearch_selection = resolve_search_subagent_selection()\ndoc_selection = resolve_doc_subagent_selection()\n```\n\n### With `workflows/browser_workflow.py`\n```python\n# Stop session from workflow\nfrom core.browser_agent import get_browser_agent_manager\n\nmanager = get_browser_agent_manager()\nawait manager.stop_session(session_id)\n```\n\n### With `app.py`\n```python\n# LLM selection at app startup\nfrom core.config import resolve_llm_selection\n\nselection = resolve_llm_selection()\n```\n\n---\n\n## Error Handling\n\n### Batch Job States\n```python\n_TERMINAL = {\n \"JOB_STATE_SUCCEEDED\",\n \"JOB_STATE_FAILED\",\n \"JOB_STATE_CANCELLED\",\n \"JOB_STATE_EXPIRED\"\n}\n\ndef is_terminal(job) -> bool:\n return batch_state(job) in _TERMINAL\n\ndef succeeded(job) -> bool:\n return batch_state(job) == \"JOB_STATE_SUCCEEDED\"\n```\n\n### Browser Agent States\n```python\nstatus: str = \"starting\" # starting | running | waiting_for_user | done | error\n```\n\n### Configuration Validation\n```python\n# Local profile coherence guard\nfrom core.local_guard import assert_local_profile_coherent\nassert_local_profile_coherent(os.environ)\n```\n\n---\n\n## Testing Considerations\n\n### Unit Testable Components\n- `build_manifest()` — Pure JSONL building\n- `parse_result_line()` — Pure JSON parsing\n- `build_request()` — Pure request building\n- `extract_vectors()` — Pure vector extraction\n\n### Integration Test Requirements\n- GCS bucket access (`GCS_OCR_BUCKET`, `GCS_OCR_PREFIX`)\n- Vertex service account (`VERTEX_SERVICE_ACCOUNT_JSON`)\n- Chrome/Chromium executable path\n- Service account credentials for audio bridge\n\n### Mocking Strategy\n```python\n# Mock GCS upload\nfrom unittest.mock import patch, MagicMock\n\nwith patch('core.batch_ocr._gcs') as mock_gcs:\n mock_gcs.return_value.bucket.return_value.blob.return_value.upload_from_string = MagicMock()\n # Test build_manifest and submit_items\n```\n\n---\n\n## Performance Notes\n\n### Batch Job Turnaround\n- OCR: ≤24h (scanned PDFs)\n- Desc/Entities/Rollup/TOC: ~50% cheaper than online, offline quota\n- Embeddings: Native batch API, order-based mapping\n\n### Browser Agent\n- Chrome launch: ~5-10s\n- CDP connection: ~2-5s\n- Audio bridge: 16kHz → 24kHz conversion, 0.4s polling interval\n\n### Token Limits\n- OCR: `GEMINI_OCR_MAX_OUTPUT_TOKENS` (default 65535)\n- Thinking budget: `GEMINI_OCR_THINKING_BUDGET` (default 0)\n- Live API: `MEDIA_RESOLUTION_MEDIUM`, sliding window 52428 tokens\n\n---\n\n## Security Considerations\n\n### Credential Management\n- Vertex OAuth tokens minted per-process, auto-refreshed\n- Service account JSON loaded from `VERTEX_SERVICE_ACCOUNT_JSON`\n- API keys read at call time (not import time)\n\n### Permission Pre-granting\n- Chrome profile preferences written before agent runs\n- CDP `Browser.setPermission` for per-origin grants\n- `--auto-grant-permissions` flag for Chrome\n\n### Local Profile Guard\n```python\nfrom core.local_guard import assert_local_profile_coherent\nassert_local_profile_coherent(os.environ)\n```\n\n---\n\n## Module Dependencies\n\n| Module | Imports |\n|--------|---------|\n| `batch_ocr.py` | `google.cloud.storage`, `google.genai`, `google.oauth2.service_account` |\n| `batch_desc.py` | `core.batch_ocr` |\n| `batch_embed.py` | `google.genai` |\n| `browser_agent.py` | `browser_use`, `websockets`, `httpx`, `playwright` |\n| `config.py` | `google.auth`, `dotenv`, `local_guard` |\n| `context_gate.py` | `core.selfhosted` |\n| `document_plan.py` | Standard library only |\n\n---\n\n## Version History\n\n- **2026-07-13:** Groq provider added for speed trials\n- **2026-07-14:** Parasail/OpenRouter model trials\n- **2026-07-16:** Qwen3-Next-80B-A3B as default search subagent\n- **2026-07-28:** `DOC_CHILD_RETRIEVAL_ONLY` flag for attribution safety\n- **2026-07-30:** Context gate design finalized\n- **2026-08-13:** Document plan schema with closed vocabulary","harakat-ai-docs":"# Harakat_AI — docs\n\n\n\n# Harakat_AI — docs Module\n\n## Overview\n\nThe `docs` module contains the developer documentation for the Harakat AI Runtime. This module provides the reference material for backend engineers, AI engineers, and maintainers who need to understand, extend, or contribute to the AI system.\n\n**Location:** `Harakat_AI/docs/`\n\n**Purpose:** To provide a living blueprint for the AI runtime architecture, agent development workflow, and system integration patterns.\n\n---\n\n## Documentation Files\n\n| File | Purpose | Target Audience |\n|------|---------|-----------------|\n| `AGENT_DEVELOPMENT_GUIDE.md` | Complete workflow for adding agents and capabilities | Backend/AI engineers |\n| `AI_SETUP_GUIDE.md` | Local and production setup instructions | DevOps, new hires |\n| `AI_USAGE_GUIDE.md` | Runtime API and integration patterns | API consumers |\n\n---\n\n## Module Structure\n\n```text\nHarakat_AI/\n └─ docs/\n ├─ AGENT_DEVELOPMENT_GUIDE.md ← Primary developer reference\n ├─ AI_SETUP_GUIDE.md ← Infrastructure setup\n └─ AI_USAGE_GUIDE.md ← Runtime API reference\n```\n\n---\n\n## Core Documentation: Agent Development Guide\n\nThe `AGENT_DEVELOPMENT_GUIDE.md` is the primary reference for building AI agents. It documents the 9-step lifecycle:\n\n```mermaid\nflowchart LR\n A[Define Agent] --> B[Create Folder]\n B --> C[Write Manifest]\n C --> D[Implement Handler]\n D --> E[Register with UV]\n E --> F[Sync to Platform]\n F --> G[Test End-to-End]\n G --> H[Enable for Offices]\n H --> I[Monitor Usage]\n```\n\n### Key Components Documented\n\n#### 1. Agent Manifest (`agent.json`)\n\nDefines agent metadata and capabilities:\n\n```json\n{\n \"name\": \"MarketingAgent\",\n \"key\": \"marketing\",\n \"category\": \"business\",\n \"version\": \"1.0.0\",\n \"workspace\": \"expert\",\n \"capabilities\": [\n {\n \"key\": \"marketing.research\",\n \"displayName\": \"Market Research\",\n \"status\": \"ACTIVE\"\n }\n ]\n}\n```\n\n#### 2. Agent Handler (`handler.py`)\n\nEntry point for task execution:\n\n```python\ndef handle(task: Dict[str, Any]) -> Dict[str, Any]:\n capability = task.get(\"capability\")\n if capability == \"marketing.research\":\n return run_market_research(task)\n elif capability == \"marketing.social_media\":\n return run_social_media(task)\n return {\"status\": \"failed\", \"error\": f\"Unknown capability: {capability}\"}\n```\n\n#### 3. Workspace Manager (`src/runtime/uv/workspace_manager.py`)\n\nDynamic agent loading:\n\n```python\nclass WorkspaceManager:\n def _load_agents(self):\n registry = {}\n for folder in os.listdir(AGENTS_DIR):\n manifest_path = os.path.join(AGENTS_DIR, folder, \"agent.json\")\n if os.path.isfile(manifest_path):\n with open(manifest_path) as f:\n manifest = json.load(f)\n registry[manifest[\"key\"]] = {\n \"manifest\": manifest,\n \"path\": os.path.join(AGENTS_DIR, folder, \"handler.py\")\n }\n return registry\n```\n\n#### 4. UV Controller (`src/runtime/uv/controller.py`)\n\nTask execution orchestration:\n\n```python\nclass UVController:\n def execute_task(self, task):\n space, agent = self.manager.resolve_workspace(task.agent)\n handler_path = agent[\"path\"]\n spec = importlib.util.spec_from_file_location(\"agent_handler\", handler_path)\n module = importlib.util.module_from_spec(spec)\n spec.loader.exec_module(module)\n return module.handle(task.dict())\n```\n\n---\n\n## Platform Integration\n\nThe documentation specifies the database schema integration with HarakatOffices_Server:\n\n### Database Tables Referenced\n\n| Table | Purpose |\n|-------|---------|\n| `Agent` | Agent metadata |\n| `AgentCapability` | Capability definitions |\n| `OfficeCapabilityAccess` | Office-level access control |\n| `AiTask` | Task execution records |\n| `AiTaskEvent` | Task event logging |\n| `AIOutput` | Task output storage |\n| `UsageRecord` | Usage tracking |\n\n### Sync Endpoint\n\n```typescript\nPOST /internal/ai/agents/sync\n{\n \"name\": \"MarketingAgent\",\n \"key\": \"marketing\",\n \"capabilities\": [...],\n \"version\": \"1.0.0\"\n}\n```\n\n---\n\n## Runtime Execution Flow\n\n```mermaid\nsequenceDiagram\n participant OfficeProvider\n participant HarakatOffices_Server\n participant AI_Runtime\n participant Agent_Handler\n\n OfficeProvider->>HarakatOffices_Server: POST /ai/tasks\n HarakatOffices_Server->>HarakatOffices_Server: Create AiTask (ACCEPTED)\n HarakatOffices_Server->>AI_Runtime: POST /run\n AI_Runtime->>AI_Runtime: Resolve workspace\n AI_Runtime->>Agent_Handler: Execute handler\n Agent_Handler-->>AI_Runtime: Return output\n AI_Runtime->>HarakatOffices_Server: PATCH /ai/tasks/:requestId\n HarakatOffices_Server->>HarakatOffices_Server: Update AiTask (SUCCEEDED)\n HarakatOffices_Server->>HarakatOffices_Server: Create AIOutput, AiTaskEvent\n```\n\n---\n\n## Usage Patterns\n\n### Task Request Format\n\n```json\n{\n \"requestId\": \"...\",\n \"officeId\": 12,\n \"agent\": \"marketing\",\n \"taskType\": \"GENERATE\",\n \"payload\": {...}\n}\n```\n\n### Response Format\n\n```json\n{\n \"status\": \"succeeded\",\n \"data\": {\n \"summary\": \"...\",\n \"sources\": []\n },\n \"credits\": 10\n}\n```\n\n---\n\n## Security & Tenancy\n\nAll tasks must include:\n\n- `officeId` (required)\n- `startupId` (optional, for multi-tenant isolation)\n\nThe runtime rejects tasks without `officeId` at the entry point (`src/runtime/main.py`).\n\n---\n\n## Contributing to Documentation\n\nWhen adding new documentation:\n\n1. **Update `AGENT_DEVELOPMENT_GUIDE.md`** for agent-related changes\n2. **Update `AI_SETUP_GUIDE.md`** for infrastructure changes\n3. **Update `AI_USAGE_GUIDE.md`** for API changes\n4. **Add code examples** that match actual implementation\n5. **Reference actual function names** and class names\n\n---\n\n## Related Modules\n\n| Module | Relationship |\n|--------|--------------|\n| `HarakatOffices_Server` | Receives task requests, syncs agents |\n| `OfficeProvider` | Triggers AI tasks from UI |\n| `src/runtime/uv/` | Core runtime logic |\n| `agents/` | Agent implementations |\n\n---\n\n## Quick Reference\n\n| Task | Reference |\n|------|-----------|\n| Add new agent | `AGENT_DEVELOPMENT_GUIDE.md` → Section 3 |\n| Setup locally | `AI_SETUP_GUIDE.md` → Section 2 |\n| API endpoints | `AI_USAGE_GUIDE.md` → Section 2 |\n| Database schema | `AGENT_DEVELOPMENT_GUIDE.md` → Section 7 |\n| Security rules | `AGENT_DEVELOPMENT_GUIDE.md` → Section 11 |","harakat-ai-envelopes":"# Harakat_AI — envelopes\n\n\n\n# Harakat_AI/envelopes\n\nThe `envelopes` module defines the standardized data contract used to transport results from specialist agents to the composer and planner components. It ensures that disparate output shapes from various specialists can be stitched together mechanically without requiring the consumer to guess at incompatible structures.\n\n## Overview\n\nThis module provides a shared result envelope system. Every specialist returns `ResultEnvelope` instances, allowing the composer/planner to process context uniformly. The module is self-contained regarding execution logic (no internal or outgoing calls), serving primarily as a schema definition and utility layer.\n\n### Key Responsibilities\n1. **Standardization:** Enforces a consistent structure (`ResultEnvelope`) across all specialist outputs.\n2. **Metadata Tracking:** Captures source references (`SourceRef`), confidence scores, and tiering.\n3. **Context Rendering:** Provides a deterministic string representation for planner context via `envelope_to_context_line`.\n\n## Core Concepts\n\n### Envelope Kinds\nThe `EnvelopeKind` literal type categorizes the nature of the data being returned. This allows the consumer to route or prioritize data based on its semantic type.\n\n| Kind | Description |\n| :--- | :--- |\n| `context` | General context information. |\n| `action` | Actionable items or commands. |\n| `procedure` | Step-by-step procedural data. |\n| `doc_ref` | References to documentation. |\n| `capability_invocation` | Invocations of system capabilities. |\n| `retrieval_trace` | Trace breadcrumbs emitted by `docs_orchestrator` (TOC → FTS → rerank → drill) with latency and candidate counts. |\n\n### Data Models\n\n#### `SourceRef`\nA Pydantic model representing a reference to a specific item within a store.\n\n```python\nclass SourceRef(BaseModel):\n storeId: str\n itemId: str\n```\n\n#### `ResultEnvelope`\nThe primary data structure returned by specialists. It wraps the payload data with metadata required for processing.\n\n```python\nclass ResultEnvelope(BaseModel):\n kind: EnvelopeKind\n data: dict[str, Any] = Field(default_factory=dict)\n source: SourceRef\n tier: str\n confidence: float = Field(ge=0.0, le=1.0)\n```\n\n**Field Details:**\n* `kind`: The type of envelope (see `EnvelopeKind`).\n* `data`: A dictionary containing the actual payload. Common keys include `title`, `name`, `summary`, `content`, or `description`.\n* `source`: The `SourceRef` identifying where the data originated.\n* `tier`: A string categorization of the result quality or priority.\n* `confidence`: A float between 0.0 and 1.0 indicating the reliability of the result.\n\n## Utility Functions\n\n### `envelope_to_context_line`\nConverts a `ResultEnvelope` into a compact, deterministic string for planner context. This is useful for logging or displaying a summary of the result in a UI.\n\n**Logic:**\n1. **Title:** Extracts `title` from `data`, falling back to `name`, then `source.itemId`.\n2. **Body:** Extracts `summary` from `data`, falling back to `content`, then `description`.\n3. **Prefix:** Constructs `[kind:storeId/itemId]`.\n4. **Output:**\n * If `body` exists: `{prefix} {title}: {body}`\n * Otherwise: `{prefix} {title}`\n\n## Architecture\n\nThe module acts as the interface layer between the **Specialist** (producer) and the **Composer/Planner** (consumer).\n\n```mermaid\nsequenceDiagram\n participant Specialist\n participant Envelope\n participant Composer\n\n Specialist->>Envelope: Create ResultEnvelope\n Note over Specialist,Envelope: Enforces Pydantic Schema\n Specialist->>Composer: Return ResultEnvelope\n Composer->>Envelope: Read kind, confidence, data\n Composer->>Composer: Stitch context mechanically\n```\n\n## Integration\n\n### For Specialists\nWhen returning results, ensure the output conforms to `ResultEnvelope`:\n1. Instantiate `SourceRef` with valid `storeId` and `itemId`.\n2. Set `kind` to the appropriate `EnvelopeKind`.\n3. Populate `data` with relevant payload (e.g., `title`, `summary`).\n4. Assign a `tier` and `confidence` score.\n\n### For Composers/Planners\nWhen consuming results:\n1. Iterate over the list of `ResultEnvelope` objects.\n2. Use `envelope_to_context_line` for quick context inspection.\n3. Filter or prioritize based on `confidence` and `tier`.\n4. Access specific content via `envelope.data`.\n\n## Dependencies\n* `pydantic`: Used for `BaseModel` definitions and field validation (e.g., `confidence` range).\n* `typing`: Used for type hints (`Literal`, `Any`, `dict`).\n\n## Notes\n* **No Execution Logic:** This module contains no business logic execution flows. It is purely a data contract and utility layer.\n* **Trace Breadcrumbs:** The `retrieval_trace` kind is specifically designed for the `docs_orchestrator` to emit loop traces (TOC → FTS → rerank → drill) for expandable \"How I found this\" footers.","harakat-ai-erp-mcp":"# Harakat_AI — erp-mcp\n\n\n\n# erp-mcp\n\nThe `erp-mcp` module is the Model Context Protocol (MCP) server implementation for the Harakat ERP (a forked headless Odoo). It exposes the ERP backend as a set of agent tools, implementing the contract defined by the [`erp-skills`](../erp-skills) library.\n\nThis module provides a generic ORM surface for data operations and a Phase‑4 customization surface for UI/UX configuration. It is designed to allow agents to operate and customize every app within the ERP, including adding data, changing data, removing data, running lifecycle methods, rendering reports, and reshaping the UI per tenant.\n\n## Architecture & Data Flow\n\nThe module acts as a bridge between the agent (via Hermes) and the Odoo backend. It does not contain business logic but rather exposes the Odoo ORM via RPC.\n\n### Component Interaction\n\n```mermaid\nsequenceDiagram\n participant Agent\n participant Hermes\n participant MCP_Server as erp-mcp (src/index.ts)\n participant Odoo_Client as Odoo RPC (src/odoo.ts)\n participant Odoo_DB as Odoo Backend\n\n Agent->>Hermes: Tool Call (e.g., erp_create)\n Hermes->>MCP_Server: _call_backend\n MCP_Server->>Odoo_Client: execKw / jsonrpc\n Odoo_Client->>Odoo_DB: execute_kw\n Odoo_DB-->>Odoo_Client: Result\n Odoo_Client-->>MCP_Server: Response\n MCP_Server-->>Hermes: Tool Result\n Hermes-->>Agent: Final Response\n```\n\n### Key Modules\n\n1. **`src/index.ts`**: The main entry point. It initializes the MCP server, registers tools, and routes incoming requests to the appropriate handler (ORM, Customization, or Report).\n2. **`src/odoo.ts`**: Handles the Odoo RPC connection. It manages sessions (`webSession`), executes queries (`execKw`), and renders PDF reports (`renderReportPdf`).\n3. **`src/customize.ts`**: Manages tenant-specific UI customizations (`AppOverride`). It handles reading and writing overrides (`getOverride`, `setOverride`, `load`).\n4. **`scripts/parity/`**: A suite of scripts used to validate the \"skill\" surface against the Odoo manifest and maintain the `parity-ledger.json`.\n\n## Tool Surface\n\nThe module exposes two categories of tools to the agent.\n\n### Generic ORM Surface\n\nThese tools map directly to Odoo ORM methods. They allow the agent to interact with any model in the system.\n\n| Tool | Odoo Method | Description |\n|---|---|---|\n| `erp_search` | `search_read` | List and filter records. |\n| `erp_count` | `search_count` | Count records matching a domain. |\n| `erp_read` | `read` | Fetch fields of records by ID. |\n| `erp_read_group` | `read_group` | Generate KPIs, pivots, or analytics. |\n| `erp_create` | `create` | Create a new record. |\n| `erp_write` | `write` | Update existing records. |\n| `erp_unlink` | `unlink` | Delete records (irreversible). |\n| `erp_call` | `execute_kw` | Call a method **on records** (e.g., `action_post`, `action_confirm`). |\n| `erp_call_model` | `execute_kw` | Call a **model** method or wizard (e.g., `create_payments`). |\n| `erp_metadata` | `fields_get` | Discover fields, types, and relations. |\n| `erp_name_search` | `name_search` | Resolve a relation name to an ID. |\n| `erp_list_models` | `ir.model` search | Discover available models. |\n| `erp_report` | `/report/pdf` | Render a QWeb PDF to base64. |\n| `erp_session` | — | Retrieve current `uid`, `login`, or `company`. |\n\n### Customization Surface (Phase‑4)\n\nThese tools allow the agent to modify the presentation layer for a specific tenant. They operate on `AppOverride` records.\n\n| Tool | Description |\n|---|---|\n| `erp_customize_get` | Read a tenant's `AppOverride` (e.g., `app=\"*\"` for all). |\n| `erp_customize_set` | Write a tenant's `AppOverride` (tabs, columns, filters, KPIs, actions, fields, app enable/rename). |\n\n## Odoo Integration Layer\n\nThe Odoo integration is centralized in `src/odoo.ts`. It abstracts the RPC calls to ensure consistent session management and error handling.\n\n### Session Management\n* **`webSession`**: Initializes and maintains the Odoo session context.\n* **`fetchOnce`**: Caches session data to avoid redundant RPC calls.\n\n### Execution\n* **`execKw`**: The primary method for executing Odoo actions. It wraps `jsonrpc` calls, handling the `execute_kw` payload.\n* **`renderReportPdf`**: Specifically handles the `/report/pdf` endpoint, returning the result as base64.\n\n### Call Flow\nWhen a tool like `erp_create` is invoked:\n1. `src/index.ts` calls `execKw` (via `src/odoo.ts`).\n2. `execKw` constructs the `execute_kw` payload.\n3. `jsonrpc` sends the request to the Odoo backend.\n4. The result is returned to the caller.\n\n## Customization Layer\n\nCustomization logic resides in `src/customize.ts`. It manages the `AppOverride` data structure, which defines how the UI is rendered for specific tenants.\n\n### Override Management\n* **`load`**: Loads the current state of overrides.\n* **`getOverride`**: Retrieves a specific override for a given app.\n* **`setOverride`**: Writes a new override configuration.\n* **`getAllOverrides`**: Retrieves all overrides for a tenant.\n\n### Resolution\nThe system resolves the final UI configuration using the formula:\n`manifest ⊕ override → resolved`\n\nThis means the base Odoo manifest is merged with the tenant-specific overrides to produce the final UI state.\n\n## Parity & Ledger Management\n\nThe `parity-ledger.json` file tracks discrepancies between the \"skill\" surface (what the agent knows) and the Odoo manifest (what exists in the database). This is critical for maintaining the contract between the agent and the ERP.\n\n### Ledger Categories\nEntries in `parity-ledger.json` are categorized by `kind`:\n* `skill_field_not_in_manifest`: A field reachable via MCP but not in the manifest (e.g., lifecycle methods like `action_post`).\n* `skill_model_no_manifest_form`: A model reachable via MCP but without a bespoke form (e.g., `account.asset`).\n* `manifest_field_no_skill`: A manifest field not enumerated in the skill prose (e.g., `name`, `date`).\n* `parser noise`: Tokens caught by the field regex that are not actual fields (e.g., source filenames like `manifest.ts`).\n\n### Maintenance Scripts\n* **`_seed_finance.py`**: A Python script used to populate the ledger with finance-specific entries. It defines rules for fields like `action_post` (lifecycle) or `original_value` (asset editor).\n* **`scripts/parity/ledger.ts`**: Applies the ledger entries to the system.\n* **`scripts/parity/diff.ts`**: Compares the current state against the ledger (`diffApp`).\n* **`scripts/parity/skill-parity.ts`**: The main orchestration script that runs parity checks.\n\n### Execution Flow\n1. `skill-parity.ts` calls `applyLedger`.\n2. `applyLedger` processes entries from `parity-ledger.json`.\n3. Tests (`ledger.test.ts`, `diff.test.ts`) verify the integrity of the ledger against the actual Odoo state.\n\n## Security & Access Control\n\n### God-Mode by Design\nThe `erp-mcp` server runs as the configured Odoo user (typically admin/bridge). This means the top-level agent has **full access** to all data and actions within the ERP.\n\n### Hermes Integration\nPer-employee access control is **not** implemented within `erp-mcp` itself. Instead, it is handled by the **Hermes** wrapper:\n1. Hermes registers each tool as an `McpCatalogTool` (`category: \"erp\"`).\n2. Hermes calls this server via `_call_backend`.\n3. Hermes applies the **access-card gate**, **MutationProposal approval**, and **AuditEntry** wrap on its side.\n4. This package remains the ERP-facing implementation, while Hermes enforces the tenant-specific security policies.\n\n## Development & Operations\n\n### Prerequisites\n* Node.js environment.\n* Odoo backend running (`docker compose -f odoo-infra/docker-compose.yml up -d`).\n* Database `erp` available.\n\n### Setup\n```bash\ncd erp-mcp\nnpm install\ncp .env.example .env # Point at your Odoo (defaults match erp-ui/.env.local)\n```\n\n### Commands\n* **`npm run smoke`**: Verifies the god-mode RPC path against live Odoo.\n* **`npm run dev`**: Starts the MCP server over stdio (using `tsx`).\n* **`npm run build`**: Compiles TypeScript to JavaScript.\n* **`npm start`**: Runs the built server.\n* **`npm run parity:test`**: Runs the full parity test suite (parse, load, diff, ledger).\n\n### Environment\nThe `.env` file must point to the Odoo backend. Defaults match `erp-ui/.env.local`.\n\n## Contributing\n\nWhen adding new tools or modifying the surface:\n1. Ensure the tool maps to a valid Odoo method (`execute_kw` or ORM method).\n2. Update `parity-ledger.json` if the new tool introduces new fields or models not in the manifest.\n3. Run `npm run parity:test` to ensure no regressions in the skill/manifest contract.\n4. Verify that the tool respects the \"God-mode\" constraint (it should not enforce user-level access control; that is Hermes's responsibility).","harakat-ai-erp-skills":"# Harakat_AI — erp-skills\n\n\n\n# erp-skills — Agent Operating Skills for Harakat ERP\n\n## Overview\n\nThe `erp-skills` module is the **Phase-5 agent integration library** that provides procedural knowledge for Harakat per-employee agents to operate the ERP system. These skills teach agents how to create invoices, advance opportunities, run payroll batches, reconcile bank lines, and perform all other business operations — **through the `erp-mcp` server** — faster and more accurately than a human clicking the UI.\n\nThese are **not** end-user click-paths or developer build guides. They encode the **models, methods, field semantics, sequences, and guardrails** behind each part of each app, expressed against the `erp-mcp` tool set (`erp_search`, `erp_create`, `erp_call`, etc.).\n\n```\nerp-skills/\n├── README.md\n├── _shared/\n│ ├── erp-mcp-foundation/ ← Core MCP tool contract\n│ ├── erp-chatter-activities/← Logging & activities\n│ ├── erp-customization/ ← UI customization\n│ ├── erp-dispatch/ ← Document delivery\n│ └── erp-ownership/ ← Record ownership (kernel)\n└── <module>/<app>/<skill>/SKILL.md\n```\n\n## Architecture\n\n### Module Structure\n\nThe library mirrors `erp-ui/lib/apps.ts` (`APP_GROUPS`) with **8 modules** and **32 apps**:\n\n```mermaid\ngraph TD\n A[erp-skills] --> B[_shared]\n A --> C[sales-crm]\n A --> D[finance]\n A --> E[supply-chain]\n A --> F[projects]\n A --> G[people]\n A --> H[services]\n A --> I[marketing]\n A --> J[productivity]\n\n B --> B1[erp-mcp-foundation]\n B --> B2[erp-chatter-activities]\n B --> B3[erp-customization]\n B --> B4[erp-dispatch]\n B --> B5[erp-ownership]\n\n C --> C1[contacts]\n C --> C2[crm]\n C --> C3[pos]\n C --> C4[sales]\n C --> C5[subscription]\n\n D --> D1[advancedaccounting]\n D --> D2[expenses]\n D --> D3[finance]\n D --> D4[payroll]\n\n E --> E1[inventory]\n E --> E2[maintenance]\n E --> E3[manufacturing]\n E --> E4[purchase]\n E --> E5[repairs]\n\n F --> F1[planning]\n F --> F2[project]\n F --> F3[timesheets]\n\n G --> G1[attendance]\n G --> G2[employees]\n G --> G3[recruitment]\n G --> G4[skills]\n G --> G5[timeoff]\n\n H --> H1[helpdesk]\n H --> H2[livechat]\n\n I --> I1[emailmarketing]\n I --> I2[marketing-automation]\n I --> I3[marketingcard]\n I --> I4[website]\n\n J --> J1[datacycle]\n J --> J2[knowledge]\n J --> J3[sign]\n J --> J4[todos]\n```\n\n### Skill Organization\n\nEach **app** contains:\n- **Granular operating skills** — one per logical part (workflow, operation cluster, form, reports surface, configuration)\n- **One coordinator skill** (`<app>-coordinator`) — teaches a coordinator agent how to add/modify/delete that app's skills as the product and per-tenant customizations change\n\n**Total: 158 skills** = 5 shared foundations + 153 app skills.\n\n## Core Foundation Skills\n\n### erp-mcp-foundation\n\nThe foundational contract for operating the ERP through the `erp-mcp` server. Every ERP operation goes through this generic tool surface, which executes each call against Odoo's ORM (or the Rust kernel).\n\n**Tools:**\n\n| Tool | Odoo method | Use for |\n|---|---|---|\n| `erp_search` | `search_read` | List/filter records |\n| `erp_count` | `search_count` | Count matches |\n| `erp_read` | `read` | Fields of known records |\n| `erp_read_group` | `read_group` | KPIs, pivots, analytics |\n| `erp_create` | `create` | New record |\n| `erp_write` | `write` | Change records |\n| `erp_unlink` | `unlink` | Permanently delete |\n| `erp_call` | any method | Lifecycle actions |\n| `erp_call_model` | model-level method | `default_get`, wizards |\n| `erp_metadata` | `fields_get` | Discover fields/types |\n| `erp_name_search` | `name_search` | Resolve labels to ids |\n| `erp_list_models` | `ir.model` search | Discover models |\n| `erp_report` | `/report/pdf` | PDF generation |\n| `erp_session` | — | Current user context |\n\n**Critical Conventions:**\n\n- **Domains** are nested arrays of `[field, op, value]` (Polish-prefix: `[\"|\", [\"a\",\"=\",1], [\"b\",\"=\",2]]`)\n- **many2one** returns `[id, \"Display Name\"]` (resolved), `false` (empty), or bare id (unresolved reference)\n- **many2many** uses command tuples: `[[6, 0, [ids]]]` (replace), `[[4, id]]` (add), `[[3, id]]` (remove)\n- **Dates** are strings: `\"YYYY-MM-DD\"` / `\"YYYY-MM-DD HH:MM:SS\"`\n- **Booleans**: clearing any field = write `false`\n- **Soft-archive**: `active=false` with context `{ \"active_test\": false }`\n\n**Kernel-backed models** (Rust kernel) have different conventions:\n- Record ids are **UUID strings** (`\"019fd6…\"`)\n- Domains are **flat** `[field, op, value]` only (no `\"|\"` / `\"&\"` / `\"!\"`)\n- x2many values are **flat id arrays** (no command tuples)\n- `state` is **action-owned** (never write directly)\n- `tax_ids` is a **flat uuid array** (no command tuples)\n\n### erp-chatter-activities\n\nHow to log notes/messages, manage followers, and schedule follow-up activities on ANY ERP record.\n\n**Two paths:**\n\n1. **Odoo-SoT hosts** → `mail.*` models (`message_post`, `mail.activity`)\n2. **Kernel-routed hosts** → kernel chatter capabilities (`harakat.activity.type`)\n\n**Odoo-SoT:**\n```\ncall <model>/<id> message_post\n kwargs: { body: \"<html>\", message_type: \"comment\", subtype_xmlid: \"mail.mt_note\" }\n```\n\n**Kernel-routed:**\n```\ncall <model>/<uuid> message_post kwargs: { body: \"<text>\", note?: <bool> }\n```\n\n### erp-customization\n\nHow to change the ERP itself — add a field, rename a stage, hide a column, add a dropdown choice, define a new record type.\n\n**Two tools:**\n- `erp_customize_compile` — dry run, changes nothing\n- `erp_customize` — apply one change\n\n**Three possible outcomes:**\n- `applied` — change is live\n- `proposed` — filed for admin approval (success, not failure)\n- `denied` — request refused, read reason and fix\n\n**60 operations** across categories: fields, views, dropdowns, stages, automation, structural, wording, connections, record types, funds & distribution.\n\n### erp-dispatch\n\nOutbound document delivery on kernel-routed tenants — the `harakat.document.dispatch` state machine that sends a platform document by email and tracks delivery idempotently.\n\n**State machine:** `draft → queued → delivered → accepted`, with `queued|delivered → bounced` and `queued → failed`.\n\n**Agent actions:**\n- `queue` — enqueue the send (approval-carded)\n- `retry` — re-enqueue a failed/bounced dispatch (approval-carded)\n\n**System-only callbacks:** `mark_delivered`, `mark_accepted`, `mark_bounced`, `mark_failed` — never call these.\n\n### erp-ownership\n\nRecord ownership on KERNEL-ROUTED ERP hosts — the `Owned` mixin's claim/release/assign/unassign actions over a record's `owner_ids` set (platform users), which is **orthogonal** to an app's assignee/salesperson (`user_id`).\n\n**Four actions:**\n- `claim` — take ownership yourself\n- `release` — drop your own ownership\n- `assign` — add a specific user as an owner (requires being an owner or governor)\n- `unassign` — remove a specific user from the owners\n\n**Guardrail:** `assign` only lets you add owners on records you already own or govern.\n\n## Module-Specific Skills\n\n### Finance (Highest Stakes)\n\nThe finance module contains the most critical skills because they create accounting facts.\n\n| Skill | Model | Purpose |\n|---|---|---|\n| `finance-invoice-form` | `account.move` | Create/edit invoices/bills |\n| `finance-invoice-lifecycle` | `account.move` | Post/cancel/draft |\n| `finance-payments` | `account.payment` | Record/review payments |\n| `finance-credit-notes` | `account.move` | Refunds (hand-authored) |\n| `finance-bank-reconcile` | `account.move.line` | Review open items |\n| `finance-bank-statements` | — | **NOT AVAILABLE** on kernel |\n| `finance-assets` | — | **NOT AVAILABLE** on kernel |\n| `finance-reports` | `account.move.line` | P&L / balance / cash / aged |\n| `finance-configuration` | `account.*` | Chart of accounts, taxes, journals |\n\n**Kernel-backed accounting models** have these critical differences:\n- `account.move` actions: `post`, `button_draft`, `button_cancel` (not `action_post`)\n- `account.move.line` has no lifecycle actions\n- `account.payment` actions: `post`, `cancel`, `reset`\n- **No reconciliation engine** — `reconciled` is a plain boolean flag\n- **No bank statements** — use journal entries instead\n- **No fixed assets** — depreciation as manual journal entries\n- **No reversal wizard** — credit notes are hand-authored\n\n**Critical guardrails:**\n- `account.move.name` and `.date` are server-owned (readonly)\n- `move_type` distinguishes invoice types: `out_invoice`, `in_invoice`, `out_refund`, `in_refund`, `out_receipt`, `in_receipt`, `entry`\n- `display_type:\"product\"` is load-bearing on lines — only these are priced into GL legs\n- `tax_ids` is a flat uuid array (no command tuples)\n- **Posting books accounting facts** — verify partner, lines, taxes, and total before `post`\n\n### Sales-CRM\n\n| Skill | Model | Purpose |\n|---|---|---|\n| `contacts-*` | `res.partner` | Contact management |\n| `crm-*` | `crm.lead` | Opportunity pipeline |\n| `pos-*` | `pos.order` | Point of sale |\n| `sales-*` | `sale.order` | Sales orders |\n| `subscription-*` | `subscription.*` | Billing subscriptions |\n\n### Supply Chain\n\n| Skill | Model | Purpose |\n|---|---|---|\n| `inventory-*` | `stock.*` | Stock management |\n| `maintenance-*` | `maintenance.*` | Equipment maintenance |\n| `manufacturing-*` | `mrp.*` | Production |\n| `purchase-*` | `purchase.*` | Procurement |\n| `repairs-*` | `repair.*` | Repair orders |\n\n### Projects & People\n\n| Skill | Model | Purpose |\n|---|---|---|\n| `project-*` | `project.*` | Project management |\n| `timesheets-*` | `hr.timesheet` | Time tracking |\n| `employees-*` | `hr.employee` | Employee records |\n| `attendance-*` | `hr.attendance` | Attendance |\n| `recruitment-*` | `hr.recruitment` | Hiring |\n| `skills-*` | `hr.skill` | Skills catalog |\n| `timeoff-*` | `hr.leave` | Leave management |\n\n### Services & Marketing\n\n| Skill | Model | Purpose |\n|---|---|---|\n| `helpdesk-*` | `helpdesk.ticket` | Support tickets |\n| `livechat-*` | `chat.*` | Live chat |\n| `emailmarketing-*` | `mail.*` | Email campaigns |\n| `marketing-automation-*` | `mail.*` | Journeys |\n| `website-*` | `website.*` | Website management |\n\n### Productivity\n\n| Skill | Model | Purpose |\n|---|---|---|\n| `datacycle-*` | `datacycle.*` | Data processing |\n| `knowledge-*` | `knowledge.*` | Knowledge base |\n| `sign-*` | `sign.*` | Document signing |\n| `todos-*` | `todo.*` | Task management |\n\n## Key Patterns & Conventions\n\n### Read Order for an Agent\n\n1. `_shared/erp-mcp-foundation` — how to talk to the ERP at all\n2. `_shared/erp-chatter-activities` — logging notes, scheduling activities\n3. The specific `<module>/<app>/<skill>` for the task at hand\n4. `_shared/erp-customization` — only when reshaping the UI\n\n### Scope Narrowing\n\nA narrower answer worn as a general one is the same defect as a wrong answer.\n\n**When your answer covers less than the question did:**\n1. Say the scope you actually used — \"across the 2 confirmed orders (there are also 1 awaiting approval and 2 drafts)\"\n2. Prefer the WIDER read when the wording is general\n3. Name the alternatives when you disambiguate\n4. A clean \"nothing found\" is a claim about the SCOPE YOU SEARCHED\n\n### Blank Fields\n\nIf a field came out empty because you did not look the value up, **say that** — never explain it as an absence in the tenant's data.\n\n| What happened | What you may say |\n|---|---|\n| You called the lookup and it matched nothing | \"there is no X in the catalog\" |\n| You did not call the lookup | \"I left X blank — I did not look it up. Want me to?\" |\n\n### Unresolved References\n\nAn unresolved reference is **not** an empty field. The field holds a value; we could not look up what it is called.\n\n**Never report:** \"Unassigned\", \"empty\", \"missing\", \"not set\" for an unresolved reference.\n\n**Say:** \"The reference could not be resolved\" and offer to look it up.\n\n## Guardrails & Safety\n\n### Destructive Operations\n\n- **Prefer soft-archive** (`active=false`) over `unlink`\n- **High-stakes writes** (posting, payments, config edits) follow the propose→approve path\n- **Mass-delete** is never silent — always surface to the user\n\n### Idempotency\n\n- Lifecycle methods usually reject double-calls (confirming a confirmed order errors)\n- Read state first; treat \"already in state\" as success\n- Document dispatch uses `idempotency_key` for \"send at most once\"\n\n### Money & Posting\n\n- Posting an invoice (`action_post`), confirming an order, validating payroll — these create accounting facts\n- Double-check the record's totals/lines before calling them\n- **Never** report an unpaid invoice as paid (no payment status on `account.move`)\n\n### Customization vs Data\n\n| The user wants… | What to do |\n|---|---|\n| A new field, renamed label, hidden column, new dropdown choice, new record type | `erp_customize` |\n| A new stage / tag / team / category — master data the app already reads from a table | Ordinary ERP write tools on that config model |\n| A new lead, invoice, order, employee | Ordinary ERP write tools on the business model |\n\n## How to Contribute\n\n### Adding a New Skill\n\n1. **Identify the app and part** — which app, which logical operation cluster?\n2. **Check for existing skills** — don't duplicate\n3. **Write the SKILL.md** with:\n - Frontmatter with unique `name` (app-prefixed) and \"pushy\" `description`\n - Model names, method names, field names from `erp-ui/lib/<app>.ts` and live DB\n - CRUD operations with actual method names\n - Lifecycle actions (if any)\n - Guardrails and edge cases\n - Tips for common pitfalls\n4. **Reference shared skills** — don't repeat `erp-mcp-foundation` or `erp-chatter-activities`\n5. **Update the coordinator** — add the skill to the app's coordinator skill's inventory\n\n### Updating an Existing Skill\n\n1. **Verify against the server** — method/field names may change on kernel bumps\n2. **Check the manifest** — tabs/columns/actions may have been restructured\n3. **Update the skill** with new method names, removed capabilities, or changed semantics\n4. **Update the coordinator** to reflect changes\n\n### Coordinators\n\nCoordinator skills maintain the app's skill set. They are read by the **coordinator/customization agent**, not normally by the operating agent.\n\n**Rules:**\n1. Follow the seven rules from `crm-coordinator`\n2. Map each skill to models, manifest keys, and lib functions\n3. Flag high-stakes customizations (disabling app, hiding actions, changing config)\n4. Keep skills in sync with product changes and tenant customizations\n\n## Host Differences\n\n### Odoo-SoT vs Kernel-Routed\n\n| Feature | Odoo-SoT | Kernel-Routed |\n|---|---|---|\n| Record ids | Integers | UUID strings |\n| Domains | Nested with `\"|\"` / `\"&\"` | Flat only |\n| x2many | Command tuples | Flat id arrays |\n| State | Action-driven | Action-driven |\n| Chatter | `mail.*` models | `harakat.activity.type` |\n| Ownership | `user_id` (assignee) | `owner_ids` (platform users) |\n| Bank statements | `account.bank.statement` | **NOT AVAILABLE** |\n| Fixed assets | `account.asset` | **NOT AVAILABLE** |\n| Reconciliation | Engine exists | Plain `reconciled` flag |\n\n**Skills must specify which path applies** and use the correct wire calls.\n\n## Execution Flow Example\n\nCreate an opportunity for \"Azure Interior\" worth 50k and move it to Proposition:\n\n```\n1. erp_name_search { model: \"res.partner\", name: \"Azure Interior\" }\n → [[6, \"Azure Interior\"]]\n\n2. erp_name_search { model: \"crm.stage\", name: \"Proposition\" }\n → [[3, \"Proposition\"]]\n\n3. erp_create { model: \"crm.lead\", values: {\n name: \"Azure expansion\",\n type: \"opportunity\",\n partner_id: 6,\n expected_revenue: 50000,\n stage_id: 3\n } }\n → { id: 12 }\n\n4. erp_read { model: \"crm.lead\", ids: [12], fields: [\"name\",\"stage_id\",\"expected_revenue\"] }\n → confirm\n```\n\n## Summary\n\nThe `erp-skills` module is the procedural knowledge base that enables Harakat agents to operate the ERP system. It provides:\n\n- **158 skills** across 8 modules and 32 apps\n- **5 shared foundations** for cross-cutting concerns\n- **Coordinator skills** for maintaining skill sets\n- **Host-specific paths** for Odoo-SoT and kernel-routed tenants\n- **Strong guardrails** for high-stakes operations (finance, destructive writes)\n- **Clear conventions** for scope, disambiguation, and unresolved references\n\nAgents should read the foundation skills first, then the specific app skill for their task, and only consult customization skills when reshaping the UI.","harakat-ai-harakat-ai":"# Harakat_AI — Harakat_AI\n\n\n\n# Harakat_AI Module Documentation\n\n## Overview\n\nHarakat_AI is a FastAPI-based AI service that orchestrates agent workflows for the Harakat platform. It serves as the central inference and orchestration layer, integrating the Hermes agent runtime with multiple LLM providers, document processing capabilities, and MCP (Model Context Protocol) tool execution.\n\nThe service exposes REST APIs for assistant interactions, document parsing, multimodal processing, and knowledge graph queries. It runs as a multi-worker uvicorn application with configurable graceful shutdown handling.\n\n---\n\n## Architecture\n\n```mermaid\ngraph TB\n subgraph \"External Clients\"\n UI[Frontend UI]\n API[External API]\n end\n \n subgraph \"Harakat_AI Service\"\n FastAPI[FastAPI App]\n Drain[DrainMiddleware]\n SourceAuth[SourceAuthMiddleware]\n \n subgraph \"Routers\"\n Assistant[assistant_router]\n Indexing[indexing_router]\n Parse[parse_router]\n Clean[clean_router]\n Multimodal[multimodal_router]\n Enrichment[enrichment_router]\n Embed[embed_router]\n Context[context_router]\n end\n \n subgraph \"Core Services\"\n Lifecycle[core/lifecycle.py]\n Config[core/config.py]\n SourceAuth[core/source_auth.py]\n end\n \n subgraph \"Hermes Runtime\"\n Hermes[hermes-native AIAgent]\n Tools[Tool Registry]\n Memory[Memory Provider]\n end\n end\n \n subgraph \"External Systems\"\n LLM[DeepInfra / Google AI]\n MCP[MCP Server]\n Postgres[(Postgres)]\n Neo4j[(Neo4j)]\n PageIndex[PageIndex]\n end\n \n UI --> FastAPI\n API --> FastAPI\n FastAPI --> Drain\n Drain --> SourceAuth\n SourceAuth --> FastAPI\n FastAPI --> Assistant\n FastAPI --> Indexing\n FastAPI --> Parse\n FastAPI --> Clean\n FastAPI --> Multimodal\n FastAPI --> Enrichment\n FastAPI --> Embed\n FastAPI --> Context\n \n Assistant --> Hermes\n Parse --> Lifecycle\n Multimodal --> PageIndex\n Enrichment --> Neo4j\n Hermes --> LLM\n Hermes --> MCP\n MCP --> Postgres\n MCP --> Neo4j\n```\n\n---\n\n## Key Components\n\n### 1. Application Entry Point (`app.py`)\n\nThe main FastAPI application orchestrates all service functionality:\n\n```python\napp = FastAPI(\n title=\"Harakat AI\",\n version=\"1.0.0\",\n description=\"LangGraph-powered AI service for Harakat platform\",\n debug=DEBUG,\n)\n```\n\n**Startup Lifecycle:**\n- `@app.on_event(\"startup\")` installs drain signal handlers and resolves the drain budget\n- `@app.on_event(\"shutdown\")` waits for in-flight requests and reports abandoned turns\n\n**Middleware Stack (outermost to innermost):**\n1. `DrainMiddleware` — Refuses new work during graceful shutdown\n2. `SourceAuthMiddleware` — Authenticates server→AI requests (optional)\n3. `CORSMiddleware` — Cross-origin resource sharing\n4. `SecurityHeadersMiddleware` — Security headers (X-Content-Type-Options, X-Frame-Options, etc.)\n\n### 2. Drain Management (`core/lifecycle.py`)\n\nThe drain system enables graceful shutdown without killing active AI turns:\n\n```python\n# Key functions\ninstall_drain_signal_handlers() # SIGTERM/SIGINT handlers\nshould_refuse(path) # Check if request should be rejected\n_drain_hold(method, path) # Acquire hold for active request\nwait_for_inflight() # Wait for all in-flight requests\nfinish_drain(abandoned) # Report abandoned turns\n```\n\n**Configuration:**\n- `SHUTDOWN_GRACE_SECONDS` (default: 150)\n- `UVICORN_WORKERS` (default: 8)\n- `timeout-graceful-shutdown` = `SHUTDOWN_GRACE_SECONDS * 4 / 5`\n\n### 3. Hermes Agent Runtime (`vendor/hermes-agent`)\n\nThe Hermes agent provides the core agent orchestration:\n\n```python\n# Key entry points\nfrom agents.hermes_native.aia_runner import run_agent\nfrom agents.hermes_native.aia_runner import async_call_llm\n```\n\n**Vendored Dependencies:**\n- `hermes-agent` v0.8.0 (vendored from NousResearch)\n- Local patches for DeepInfra, Groq, Parasail, and OpenRouter compatibility\n\n**Local Patches:**\n- `prompt_cache_key` on chat-completions route (DeepInfra prefix caching)\n- Groq strict-schema compatibility (strips `reasoning_content`, clamps `max_tokens`)\n- Parasail/OpenRouter system message ordering and token accounting\n\n### 4. Tool Registry (`tool_manifest.json`)\n\nDefines all tools available to the AI agent:\n\n```json\n{\n \"tools\": [\n {\n \"toolKey\": \"send_email\",\n \"scope\": \"local\",\n \"minTier\": \"Z\",\n \"equivalentUiPath\": \"/dashboard/mail\",\n \"parameters\": { ... }\n },\n {\n \"toolKey\": \"schedule_meeting\",\n \"scope\": \"local\",\n \"minTier\": \"Z\",\n \"parameters\": {\n \"start_time\": \"ISO 8601 datetime\",\n \"attendees\": [\"email addresses\"],\n \"duration_minutes\": 30\n }\n },\n {\n \"toolKey\": \"query_knowledge_graph\",\n \"scope\": \"local\",\n \"minTier\": \"Z\",\n \"parameters\": {\n \"seed_kind\": \"file|folder|drive|user|task|document_section\",\n \"seed_id\": \"entity identifier\",\n \"max_depth\": 2,\n \"relations\": [\"CONTAINS\", \"BELONGS_TO\", ...]\n }\n }\n ]\n}\n```\n\n**Tool Categories:**\n- **Communication:** `send_email`, `list_inbox`, `search_email`, `read_email`\n- **Tasks:** `list_tasks`, `create_task`, `update_task`\n- **Documents:** `search_documents`, `read_file_content`, `ocr_extract_text`\n- **Knowledge Graph:** `query_knowledge_graph`, `sync_knowledge_graph`\n- **Meetings:** `schedule_meeting`, `list_meetings`, `cancel_meeting`\n- **Governance:** `propose_predictive_capability`, `list_capability_opportunities`\n\n### 5. Document Processing Workflows\n\nThe service supports multiple document formats through dedicated workflows:\n\n```python\n# From requirements.txt\nPyMuPDF>=1.24.0 # PDF processing\npdfplumber>=0.11.0 # PDF text extraction\npython-docx>=1.1.0 # DOCX processing\nopenpyxl>=3.1.0 # XLSX processing\npython-pptx>=1.0.0 # PPTX processing\nextract-msg>=0.50.0 # MSG email processing\n```\n\n**Multimodal Pipeline:**\n1. Parse document (xlsx/docx/csv/pdf)\n2. Convert to PDF via LibreOffice (for vision models)\n3. Run Gemini vision pass\n4. Generate multimodal description\n\n**Rust Extraction Binary:**\n- Built via `cargo build --release` in Docker build stage\n- Activated when `RUST_INGEST_ENABLED` is set\n- Used for memory-hot extractors (xlsx/docx/csv)\n- Binary path: `/usr/local/bin/harakat-ingest`\n\n### 6. LLM Provider Integration\n\nThe service supports multiple LLM providers through the Hermes agent:\n\n```python\n# From logs\nINFO agents.hermes_native.aia_runner: [LLM] selected provider=deepinfra model=Qwen/Qwen3.6-35B-A3B\nINFO app: [compaction] routed to model-of-record Qwen/Qwen3.6-35B-A3B @ https://api.deepinfra.com/v1/openai\n```\n\n**Provider Configuration:**\n- DeepInfra (default for chat completions)\n- Google Cloud AI Platform (for embeddings and batch predictions)\n- Groq (via Hermes agent)\n- OpenRouter (via Hermes agent)\n- Parasail (via Hermes agent)\n\n**Context Compaction:**\n```python\n# From app.py\nos.environ[\"CONTEXT_COMPRESSION_BASE_URL\"] = _compaction_sel.base_url\nos.environ[\"CONTEXT_COMPRESSION_API_KEY\"] = _compaction_sel.api_key\nos.environ[\"CONTEXT_COMPRESSION_MODEL\"] = _compaction_sel.model\n```\n\n---\n\n## API Endpoints\n\n### Health Check\n```http\nGET /health\n```\n**Response:**\n```json\n{\n \"status\": \"ok\"\n}\n```\n**During Drain:**\n```json\n{\n \"status\": \"draining\",\n \"phase\": \"draining\",\n \"inFlight\": 3\n}\n```\n\n### Assistant Routes\n- `POST /assistant/stream` — Streaming assistant responses\n- `POST /assistant/chat` — Non-streaming chat\n- `POST /assistant/tools` — Tool invocation\n\n### Document Routes\n- `POST /parse/extract` — Parse and extract document content\n- `POST /clean` — Clean and normalize documents\n- `POST /multimodal/describe` — Multimodal document description\n- `POST /embed` — Generate embeddings\n\n### Indexing Routes\n- `POST /index` — Index documents\n- `GET /index/status` — Indexing status\n\n### Enrichment Routes\n- `POST /enrich` — Enrich documents with metadata\n- `POST /enrich/summary` — Generate summaries\n\n---\n\n## Configuration\n\n### Environment Variables\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `UVICORN_WORKERS` | 8 | Number of uvicorn workers |\n| `SHUTDOWN_GRACE_SECONDS` | 150 | Grace period for shutdown |\n| `RUST_INGEST_ENABLED` | false | Enable Rust extraction binary |\n| `RUST_INGEST_BIN` | /usr/local/bin/harakat-ingest | Path to Rust binary |\n| `CORS_ORIGINS` | localhost:3000,3001,3002,5173 | Allowed CORS origins |\n| `AI_HOP_ENFORCEMENT` | off | Enable server→AI authentication |\n| `DEBUG` | false | Enable debug mode |\n| `DEBUG_HERMES_ENABLED` | false | Enable Hermes debug router |\n\n### Docker Configuration\n\n```dockerfile\n# Build stage\nFROM rust:1-slim AS rust-build\nWORKDIR /crate\nCOPY rust-ingest ./\nRUN cargo build --release\n\n# Runtime stage\nFROM python:3.11-slim AS base\nRUN apt-get install -y libreoffice-core libreoffice-calc ffmpeg fonts-liberation\nCOPY --from=rust-build /crate/target/release/harakat-ingest /usr/local/bin/harakat-ingest\nENV RUST_INGEST_BIN=/usr/local/bin/harakat-ingest\nENV UVICORN_WORKERS=8\nCMD [\"sh\", \"-c\", \"uvicorn app:app --host 0.0.0.0 --port 8000 --workers ${UVICORN_WORKERS:-8} --timeout-graceful-shutdown $(( ${SHUTDOWN_GRACE_SECONDS:-150} * 4 / 5 ))\"]\n```\n\n---\n\n## Operational Considerations\n\n### Windows Watchdog (`ai_watchdog.ps1`)\n\nA PowerShell script that monitors the AI service on Windows:\n\n```powershell\n# Monitors:\n# - Process listening on port 8000\n# - Total AI tree RSS (max 20GB default)\n# - Free RAM (min 5GB default)\n# - HTTP responsiveness (6-second timeout)\n\n# Triggers restart when:\n# - No listener on :8000\n# - AI tree RSS >= MaxAiGB\n# - Free RAM <= MinFreeGB\n# - Unresponsive for > 45 seconds\n```\n\n**Usage:**\n```powershell\npowershell -NoProfile -ExecutionPolicy Bypass -File ai_watchdog.ps1\n```\n\n### Common Issues\n\n**1. Windows Socket Error (WinError 10022)**\n```\nOSError: [WinError 10022] An invalid argument was supplied\n```\n**Cause:** uvicorn worker startup issue on Windows\n**Solution:** Ensure port 8000 is not in use; check Windows socket limits\n\n**2. Google Cloud Authentication**\n```\nError code: 401 - Request had invalid authentication credentials\n```\n**Cause:** Invalid GCP credentials or API key service blocked\n**Solution:** Verify `service-account.json` permissions and GCP project settings\n\n**3. Missing `fcntl` Module**\n```\nWARNING model_tools: Could not import tool module tools.memory_tool: No module named 'fcntl'\n```\n**Cause:** `fcntl` is Unix-only; not available on Windows\n**Solution:** Use Windows-compatible memory provider or skip memory tool\n\n**4. Session Summarization Failure**\n```\nSession summarization failed after 3 attempts: Error code: 401\n```\n**Cause:** Google AI Platform authentication issue\n**Solution:** Check GCP credentials and API quota\n\n---\n\n## Integration Points\n\n### With StartupsPlatform\n- MCP server at `http://localhost:5000`\n- ERP customization endpoints\n- Context management APIs\n\n### With External Systems\n- **Postgres:** Transactional data and audit logs\n- **Neo4j:** Knowledge graph and entity relationships\n- **PageIndex:** Long-document retrieval\n- **Google Cloud AI Platform:** Embeddings and batch predictions\n- **DeepInfra:** Chat completions and context compaction\n\n### With Hermes Agent\n- `http://localhost:8010` — Hermes runtime API\n- Session management (`/v3/workspaces/startup_*/sessions`)\n- Peer configuration (`/v3/workspaces/startup_*/peers`)\n- Context retrieval (`/v3/workspaces/startup_*/context`)\n\n---\n\n## Development Notes\n\n### Adding New Tools\n\n1. Add tool definition to `tool_manifest.json`\n2. Implement tool handler in Hermes agent\n3. Register tool in `tool_registry.py`\n4. Update `tool_manifest.json` with `equivalentUiPath` if user-facing\n\n### Adding New Routers\n\n1. Create router module in `workflows/`\n2. Add to `app.py` router list\n3. Ensure drain middleware covers the route\n4. Add health check if needed\n\n### Document Processing\n\n1. Add format handler to `workflows/parse_workflow.py`\n2. Register MIME type in `requirements.txt`\n3. Test with sample documents\n4. Update `tool_manifest.json` if new tool needed\n\n### Graceful Shutdown\n\nThe drain system ensures active AI turns complete before shutdown:\n1. SIGTERM received\n2. `DrainMiddleware` refuses new requests\n3. `wait_for_inflight()` waits for active turns\n4. `finish_drain()` reports abandoned turns\n5. Process exits\n\n---\n\n## Security Considerations\n\n### Source Authentication\n- Optional middleware for server→AI request authentication\n- Prevents replay attacks on server→AI hops\n- Configured via `AI_HOP_ENFORCEMENT` environment variable\n\n### CORS\n- Restricted to known frontend origins\n- Configurable via `CORS_ORIGINS` environment variable\n\n### Service Account\n- GCP service account stored in `service-account.json`\n- Should be mounted as volume/secret, not committed to repo\n- Used for Google Cloud AI Platform and Vertex AI\n\n---\n\n## Performance Tuning\n\n### Worker Count\n- Default: 8 workers (2 * nproc + 1 for 4 vCPU)\n- Adjust based on available vCPUs\n- Monitor `inflight_count()` during load\n\n### Document Processing\n- LibreOffice rendering: ~400MB image, seconds per file\n- Rust extraction: Memory-hot, short-lived subprocess\n- Consider `RUST_INGEST_ENABLED` for high-volume xlsx/docx/csv\n\n### LLM Caching\n- DeepInfra prefix caching via `prompt_cache_key`\n- Groq token clamping (8192 max)\n- OpenRouter token accounting (`extra_body.usage = {\"include\": true}`)\n\n---\n\n## Troubleshooting\n\n### Service Won't Start\n1. Check port 8000 availability\n2. Verify Python dependencies: `pip install -r requirements.txt`\n3. Check Docker build logs for Rust compilation errors\n4. Review `uvicorn.enablement*.err` logs\n\n### High Memory Usage\n1. Monitor with `ai_watchdog.ps1` (Windows) or `ps aux` (Linux)\n2. Check `MaxAiGB` threshold\n3. Consider reducing `UVICORN_WORKERS`\n4. Review document processing logs for large files\n\n### Slow Response Times\n1. Check LLM provider latency (DeepInfra, Google AI)\n2. Review document parsing times in logs\n3. Check Neo4j/Postgres query performance\n4. Monitor `inflight_count()` for queue buildup\n\n### Tool Execution Failures\n1. Verify MCP server connectivity\n2. Check tool parameters against `tool_manifest.json`\n3. Review Hermes agent logs for tool errors\n4. Validate tool permissions and tier requirements\n\n---\n\n## Version History\n\n- **v1.0.0** — Initial release with Hermes agent integration\n- **2026-07-13** — DeepInfra prefix caching patch, Groq compatibility\n- **2026-07-14** — Parasail/OpenRouter compatibility, token accounting\n- **2026-08-06** — Context router rescue from `pipeline-defects`\n- **v1 M16** — Graceful shutdown drain implementation (T7.2)\n- **REGISTER B1** — Source authentication for server→AI hops\n\n---\n\n## Related Modules\n\n- `core/lifecycle.py` — Drain and shutdown management\n- `core/config.py` — Configuration and LLM selection\n- `core/source_auth.py` — Source authentication middleware\n- `workflows/` — Individual workflow routers\n- `vendor/hermes-agent` — Vendored Hermes agent runtime\n- `tool_manifest.json` — Tool registry definition","harakat-ai-kubernetes":"# Harakat_AI — kubernetes\n\n\n\n# Harakat_AI — kubernetes\n\n## Overview\n\nThe `kubernetes` module within the `Harakat_AI` project contains Infrastructure-as-Code (IaC) manifests used to deploy and manage containerized workloads on a Kubernetes cluster. This specific module is responsible for defining the desired state of the application infrastructure, ensuring consistency across development, staging, and production environments.\n\n## Module Purpose\n\nThe primary purpose of this module is to provide declarative configuration for Kubernetes resources. Unlike executable code, this module does not contain runtime logic, functions, or classes. Instead, it serves as a static definition of the application's deployment topology.\n\n### Execution Model\n\nAs indicated by the execution flow analysis:\n* **Internal Calls:** None.\n* **Outgoing Calls:** None.\n* **Incoming Calls:** None.\n* **Execution Flows:** No execution flows detected.\n\nThis confirms that the module operates as a configuration artifact rather than a software component. It is processed by external orchestration tools (e.g., `kubectl`, Helm, or CI/CD pipelines) to interact with the Kubernetes API Server.\n\n## Key Components\n\n### `deployment.yaml`\n\nLocated at `Harakat_AI/kubernetes/deployment.yaml`, this file defines the Kubernetes Deployment resource.\n\n* **Function:** Specifies the desired state of the application pods, including image references, resource limits, and scaling policies.\n* **Integration:** This file is the entry point for the application lifecycle within the cluster. It is typically version-controlled alongside the application source code.\n* **Usage:** Applied to the cluster using standard Kubernetes CLI commands or integrated into deployment pipelines.\n\n## Architecture and Integration\n\nThe `kubernetes` module acts as the bridge between the application code and the orchestration layer. It does not execute logic itself but defines the environment in which the application logic runs.\n\n### Deployment Flow\n\nThe following diagram illustrates how the `deployment.yaml` file interacts with the Kubernetes ecosystem:\n\n```mermaid\nsequenceDiagram\n participant DevOps as CI/CD Pipeline\n participant YAML as deployment.yaml\n participant K8s as Kubernetes API Server\n participant Pods as Application Pods\n\n DevOps->>YAML: Validate Manifest\n DevOps->>K8s: Apply Manifest (kubectl apply)\n K8s->>Pods: Create/Update Pods\n Pods-->>K8s: Report Status\n```\n\n## Usage Guidelines\n\n1. **Validation:** Before applying changes, validate the YAML syntax and Kubernetes schema compliance.\n2. **Version Control:** Treat `deployment.yaml` as source code. Changes should be committed to the repository with corresponding code changes.\n3. **Environment Specifics:** While this module provides the core deployment structure, environment-specific variables (e.g., secrets, image tags) should be managed via Kubernetes Secrets or external configuration management tools to maintain security and flexibility.\n4. **No Runtime Logic:** Do not expect this module to perform logic. All orchestration is handled by the Kubernetes control plane based on the definitions in this file.\n\n## Maintenance\n\n* **Updates:** Modify the `deployment.yaml` to update container images, resource requests, or scaling parameters.\n* **Rollbacks:** Use `kubectl rollout undo` or revert the `deployment.yaml` to a previous commit to restore the previous state.\n* **Monitoring:** Monitor the `Pods` status reported back to the `Kubernetes API Server` to ensure the desired state defined in `deployment.yaml` is achieved.","harakat-ai-mcp-client":"# Harakat_AI — mcp_client\n\n\n\n# Harakat_AI — mcp_client Module\n\nThis module provides the client-side interface for invoking Model Context Protocol (MCP) tools via the HarakatOffices_Server proxy. It encapsulates authentication, circuit-breaking logic, and credential management interactions, ensuring that the AI service does not expose credentials or directly dial external MCP endpoints.\n\n## Architecture Overview\n\nThe module operates as a thin client layer. All external MCP endpoints are accessed through the server-side proxy (`HarakatOffices_Server`). This architecture ensures that:\n1. **Credentials** remain encrypted at rest on the server and are never exposed to the agent process.\n2. **Audit** is handled server-side (persisting `McpInvocation` rows).\n3. **Policy** enforcement (tenancy, rate limiting) is centralized on the server.\n\nThe client's primary responsibility is to bundle tool arguments, forward the user's JWT, and parse the structured response into a typed result.\n\n### Component Breakdown\n\n| File | Purpose | Key Exports |\n| :--- | :--- | :--- |\n| `client.py` | Core invocation logic and result types. | `invoke_mcp`, `McpInvocationResult` |\n| `circuit_breaker.py` | Local-side circuit breaker to prevent proxy hammering. | `LocalBreaker` |\n| `credential_vault.py` | Admin-facing wrappers for credential management. | `register_credential`, `list_credentials`, `revoke_credential` |\n| `__init__.py` | Module exports. | All public symbols |\n\n## Circuit Breaker Logic\n\nTo prevent the AI service from hammering the server proxy when the server is in a failure state, a local circuit breaker (`LocalBreaker`) is implemented per `mcp_agent_id`.\n\n### State Machine\n\nThe breaker follows a standard three-state pattern:\n1. **CLOSED**: Normal operation. Requests are allowed.\n2. **OPEN**: Requests are rejected immediately.\n3. **HALF_OPEN**: Testing phase after cooldown. Requests are allowed to test stability.\n\n### Configuration\n\nThe local breaker mirrors the server defaults to ensure consistent behavior:\n* **Failure Threshold**: 5 consecutive failures.\n* **Cooldown Period**: 30 seconds.\n\n### Implementation Details\n\nThe `LocalBreaker` class manages state transitions internally.\n* `should_allow()`: Checks if the current state permits a request. It triggers `_maybe_transition()` to move from OPEN to HALF_OPEN if the cooldown has elapsed.\n* `record_failure()`: Increments the failure count. If the threshold is met, the state transitions to OPEN. If called during HALF_OPEN, it immediately reverts to OPEN.\n* `record_success()`: Resets the state to CLOSED and clears the failure count.\n\n**Note:** The server-side breaker is authoritative. This local breaker is a fast-fail layer to amortize network round-trips when the server returns `CIRCUIT_OPEN`.\n\n## Core Invocation API\n\nThe primary entry point for invoking MCP tools is `invoke_mcp`.\n\n### Function Signature\n\n```python\nasync def invoke_mcp(\n *,\n auth_token: str,\n mcp_agent_id: int,\n tool_name: str | None,\n arguments: dict[str, Any],\n caller_assistant_id: int | None = None,\n timeout_seconds: float = 35.0,\n) -> McpInvocationResult\n```\n\n### Parameters\n\n* `auth_token`: The user's JWT. Required for authorization.\n* `mcp_agent_id`: Unique identifier for the MCP agent. Used to retrieve the specific `LocalBreaker`.\n* `tool_name`: Optional name of the MCP tool to invoke.\n* `arguments`: Dictionary of arguments to pass to the tool.\n* `caller_assistant_id`: Optional ID of the assistant making the call.\n* `timeout_seconds`: HTTP client timeout (default 35.0s).\n\n### Return Value\n\nReturns a `McpInvocationResult` dataclass. Callers should branch on `result.success` rather than catching exceptions.\n\n```python\n@dataclass\nclass McpInvocationResult:\n success: bool\n status: str # e.g., \"SUCCESS\", \"CIRCUIT_OPEN\", \"TIMEOUT\"\n http_status: int | None # Outer HTTP status from proxy\n data: Any | None # Parsed MCP response\n invocation_id: int | None # Server-side audit row ID\n error_message: str | None\n latency_ms: int | None\n```\n\n### Execution Flow\n\nThe following sequence diagram illustrates the flow of `invoke_mcp`, including the interaction with the local circuit breaker.\n\n```mermaid\nsequenceDiagram\n participant Client as AI Service\n participant Breaker as LocalBreaker\n participant Proxy as Server Proxy\n participant MCP as External MCP\n\n Client->>Breaker: should_allow()\n alt Breaker is OPEN\n Breaker-->>Client: False\n Client->>Client: Return McpInvocationResult(status=\"CIRCUIT_OPEN\")\n else Breaker is CLOSED/HALF_OPEN\n Client->>Proxy: POST /api/startup/mcp/{id}/invoke\n Proxy->>MCP: Forward Request\n MCP-->>Proxy: Response\n Proxy-->>Client: Response\n Client->>Breaker: record_success() or record_failure()\n Client-->>Client: Return McpInvocationResult\n end\n```\n\n### Error Handling\n\nNetwork errors, timeouts, and server-side breaker opens are mapped to `success=False`.\n* **TIMEOUT**: Triggered by `httpx.TimeoutException`.\n* **FAILED**: Triggered by `httpx.HTTPError` or non-2xx HTTP status.\n* **CIRCUIT_OPEN**: Triggered when `LocalBreaker` state is OPEN.\n* **REJECTED**: Triggered if `auth_token` is missing.\n\n## Credential Management\n\nThe `credential_vault` module provides typed wrappers for server-side credential management. This is intended for platform tooling (admin wizards, setup scripts) rather than runtime agent logic. Agents should use `invoke_mcp` for tool execution.\n\n### Credential Types\n\n```python\nCredentialType = Literal[\"API_KEY\", \"OAUTH_BEARER\", \"BASIC_AUTH\", \"CUSTOM\"]\n```\n\n### Available Functions\n\n* `list_credentials(auth_token: str)`: Retrieves a list of `CredentialMetadata` objects.\n* `register_credential(auth_token, mcp_agent_id, credential_type, payload)`: Registers or rotates a credential. Returns metadata only; the server never echoes back plaintext or ciphertext.\n* `revoke_credential(auth_token, mcp_agent_id)`: Revokes a credential by ID. Returns `True` on success (200 OK).\n\n### Metadata Structure\n\n```python\n@dataclass\nclass CredentialMetadata:\n id: int\n mcp_agent_id: int\n credential_type: str\n created_at: str\n revoked_at: str | None\n```\n\n## Integration Notes\n\n* **Breaker Initialization**: `LocalBreaker` instances are cached in a global dictionary `_BREAKERS` keyed by `mcp_agent_id`. This ensures a consistent state per agent across multiple invocations.\n* **Server Dependency**: The client relies on `HARAKAT_API_URL` from `core.config`. Ensure this environment variable is set correctly.\n* **Audit**: The `invocation_id` in `McpInvocationResult` links to the server-side audit log. Use this for debugging or compliance tracking.","harakat-ai-providers":"# Harakat_AI — providers\n\n\n\n# Harakat_AI — providers\n\n## Overview\n\nThe `providers` module implements a pluggable LLM provider system that enables swapping inference backends via environment configuration. All providers conform to a unified interface, allowing the rest of the codebase to remain provider-agnostic.\n\n**Key Design Goals:**\n- **Provider Agnosticism**: Core workflows use `get_provider()` or `LLMProvider` interface, not concrete implementations\n- **Environment-Driven Swapping**: Change `LLM_PROVIDER` in `.env` to switch backends without code changes\n- **Unified Data Models**: `ChatMessage`, `ChatResponse`, and `ContentPart` provide consistent message formatting across providers\n- **Sync/Async Parity**: All providers implement both synchronous and asynchronous chat interfaces\n\n## Architecture\n\n```mermaid\ngraph TD\n A[get_provider] --> B{Registry Lookup}\n B -->|name| C[Import Module]\n C --> D[Instantiate Provider]\n D --> E[chat/achat/astream]\n E --> F[Provider-Specific API]\n F --> G[OpenAI SDK / Vertex SDK / Groq SDK]\n```\n\n## Core Interfaces\n\n### `LLMProvider` (base.py)\n\nAll providers must implement this abstract base class. It defines three methods:\n\n| Method | Description |\n|--------|-------------|\n| `chat(messages, *, temperature, max_tokens, **kwargs)` | Synchronous chat completion |\n| `achat(messages, *, temperature, max_tokens, **kwargs)` | Asynchronous chat completion |\n| `astream(messages, *, temperature, max_tokens, **kwargs)` | Asynchronous streaming |\n\n**Common Parameters:**\n- `messages: list[ChatMessage]` — Input conversation history\n- `temperature: float` — Sampling temperature (default: 0.7)\n- `max_tokens: int` — Maximum output tokens (default: 2048)\n- `**kwargs` — Provider-specific overrides\n\n### Data Models\n\n#### `ChatMessage`\n```python\n@dataclass\nclass ChatMessage:\n role: str # \"system\" | \"user\" | \"assistant\"\n content: str | list[ContentPart] = \"\" # text or multimodal parts\n```\n\n#### `ContentPart`\n```python\n@dataclass\nclass ContentPart:\n type: str # \"text\" | \"inline_data\"\n text: str | None = None # For type=\"text\"\n data: bytes | None = None # For type=\"inline_data\" (raw bytes)\n mime_type: str | None = None # For type=\"inline_data\"\n```\n\n#### `ChatResponse`\n```python\n@dataclass\nclass ChatResponse:\n content: str\n model: str\n usage: dict[str, int] | None = None\n raw: Any = None\n```\n\n## Provider Registry\n\n### `__init__.py`\n\nThe registry maps provider names to module/class pairs:\n\n```python\n_REGISTRY: dict[str, tuple[str, str]] = {\n \"gemini\": (\".gemini_provider\", \"GeminiProvider\"),\n \"google_ai_studio\": (\".gemini_provider\", \"GeminiProvider\"),\n \"openai\": (\".openai_provider\", \"OpenAIProvider\"),\n \"vertex_ai\": (\".vertex_ai\", \"VertexAIProvider\"),\n \"groq\": (\".groq_provider\", \"GroqProvider\"),\n \"nemotron\": (\".nemotron_provider\", \"NemotronProvider\"),\n}\n```\n\n### `get_provider()`\n\nFactory function that returns an initialized provider instance:\n\n```python\ndef get_provider(name: str | None = None, *, model: str | None = None) -> LLMProvider:\n \"\"\"Return an initialised provider. Falls back to Gemini if unknown.\"\"\"\n```\n\n**Behavior:**\n1. Reads `LLM_PROVIDER` from environment if `name` not provided\n2. Defaults to `\"gemini\"` if unknown provider name\n3. Optionally passes `model` parameter to provider constructor\n4. Returns provider instance\n\n**Usage:**\n```python\nfrom Harakat_AI.providers import get_provider\n\n# Use default (Gemini)\nprovider = get_provider()\n\n# Explicit provider\nprovider = get_provider(\"openai\")\n\n# With custom model\nprovider = get_provider(\"vertex_ai\", model=\"gemini-2.0-flash\")\n```\n\n### `__getattr__`\n\nSupports direct class imports:\n\n```python\nfrom Harakat_AI.providers import OpenAIProvider\n```\n\n## Provider Implementations\n\n### `OpenAIProvider` (openai_provider.py)\n\nStandard OpenAI SDK wrapper.\n\n**Environment Variables:**\n- `OPENAI_API_KEY` — OpenAI API key\n- `OPENAI_MODEL` — Model name (default: `gpt-4o-mini`)\n\n**Notes:**\n- Uses `openai` SDK (already in requirements.txt)\n- Supports standard OpenAI chat completions API\n- No special auth handling required\n\n### `GeminiProvider` (gemini_provider.py)\n\nLightweight bridge for Google AI Studio via OpenAI-compatible endpoint.\n\n**Environment Variables:**\n- `GEMINI_API_KEY` or `GOOGLE_API_KEY` — API key\n- `GEMINI_BASE_URL` — Base URL (default: `https://generativelanguage.googleapis.com/v1beta/openai`)\n- `GEMINI_MODEL` or `HERMES_MODEL` — Model name (default: `gemini-3.5-flash`)\n\n**Special Handling:**\n- Detects Vertex AI endpoint (`aiplatform.googleapis.com`) and switches to OAuth token auth\n- Refreshes credentials before every call when using Vertex\n- Prefixes model name with `google/` for Vertex OpenAI-compatible endpoint\n\n**Auth Flow:**\n```python\ndef _refresh_auth(self) -> None:\n if not self._is_vertex:\n return\n # Load service account credentials\n # Refresh token if expired\n # Update both sync and async client api_key\n```\n\n### `VertexAIProvider` (vertex_ai.py)\n\nNative Vertex AI SDK for full Gemini support including multimodal input.\n\n**Environment Variables:**\n- `VERTEX_SERVICE_ACCOUNT_JSON` — Path to service account JSON (default: `./service-account.json`)\n- `VERTEX_PROJECT_ID` — GCP project ID\n- `VERTEX_REGION` — Region (default: `global`)\n- `VERTEX_MODEL` — Model name (default: `gemini-3.5-flash`)\n- `VERTEX_THINKING_LEVEL` — Optional: `minimal` | `low` | `medium` | `high`\n\n**Key Features:**\n- Native support for multimodal content (text, images, PDFs, audio, video)\n- Separates system instructions from conversation contents\n- Handles Gemini 3.x parameter changes (temperature/top_p deprecated)\n- Provides `extract_text_from_file()` for document text extraction\n\n**Message Formatting:**\n```python\ndef _to_contents(self, messages: list[ChatMessage]) -> tuple[str | None, list[Content]]:\n # Accumulates system messages into system_instruction\n # Converts user/assistant messages to Content objects\n```\n\n**Response Parsing:**\n```python\ndef _parse_response(self, resp: Any) -> ChatResponse:\n # Handles empty text on MAX_TOKENS, SAFETY, thinking-only completions\n # Returns placeholder strings for empty responses\n```\n\n### `GroqProvider` (groq_provider.py)\n\nFast inference via Groq SDK.\n\n**Environment Variables:**\n- `GROQ_API_KEY` — Groq API key\n- `GROQ_MODEL` — Model name (default: `llama-3.3-70b-versatile`)\n\n**Notes:**\n- Requires `pip install groq`\n- Does not offer embeddings — use `OpenAIProvider` for those\n- Lazy imports to avoid dependency on groq package\n\n### `NemotronProvider` (nemotron_provider.py)\n\nDigitalOcean GenAI inference endpoint.\n\n**Environment Variables:**\n- `DO_MODEL_ACCESS_KEY` — DigitalOcean model access key\n- `NEMOTRON_MODEL` — Model name (default: `nvidia-nemotron-3-super-120b`)\n\n**Notes:**\n- Uses OpenAI-compatible API at `https://inference.do-ai.run/v1`\n- Default temperature is 0.3 (more deterministic)\n- Uses 60s timeout for both sync and async clients\n\n## Integration Points\n\n### Workflows\n\n| Workflow | Usage |\n|----------|-------|\n| `draft_onboarding_governance` | `get_provider()` |\n| `compress_day` | `achat()` |\n| `extract_memories` | `achat()` |\n| `draft_governance_artifact` | `get_provider()`, `achat()` |\n| `generate_mom_structure` | `get_provider()`, `achat()` |\n| `propose_autoload` | `get_provider()`, `achat()` |\n| `summarize_conversation` | `get_provider()`, `achat()` |\n| `note_title` | `achat()` |\n| `enhance_transcript_with_gemini` | `achat()` |\n| `parse_workflow` | `get_provider()` |\n| `feed_proposals` | `achat()` |\n\n### Core Modules\n\n| Module | Usage |\n|--------|-------|\n| `tool_agent.py` | `get_provider()`, `achat()` |\n| `utility_llm.py` | `achat()`, `get_provider()` |\n| `post_meeting.py` | `get_provider()`, `achat()` |\n| `transcription.py` | `achat()` |\n| `document_indexer.py` | `extract_text_from_file()` |\n\n## Usage Patterns\n\n### Basic Chat\n\n```python\nfrom Harakat_AI.providers import get_provider\nfrom Harakat_AI.providers.base import ChatMessage\n\nprovider = get_provider()\n\nmessages = [\n ChatMessage(role=\"system\", content=\"You are a helpful assistant.\"),\n ChatMessage(role=\"user\", content=\"What is the capital of France?\"),\n]\n\nresponse = provider.chat(messages)\nprint(response.content)\n```\n\n### Streaming\n\n```python\nasync def stream_response():\n provider = get_provider()\n messages = [ChatMessage(role=\"user\", content=\"Tell me a story\")]\n \n async for chunk in provider.astream(messages):\n print(chunk, end=\"\", flush=True)\n```\n\n### Multimodal (Vertex AI Only)\n\n```python\nfrom Harakat_AI.providers import VertexAIProvider\nfrom Harakat_AI.providers.base import ContentPart\n\nprovider = VertexAIProvider()\n\n# Create multimodal message\ncontent = [\n ContentPart(type=\"text\", text=\"Extract text from this image\"),\n ContentPart(type=\"inline_data\", data=image_bytes, mime_type=\"image/png\"),\n]\n\nmessages = [ChatMessage(role=\"user\", content=content)]\nresponse = provider.chat(messages)\n```\n\n### Document Text Extraction\n\n```python\nfrom Harakat_AI.providers import VertexAIProvider\n\nprovider = VertexAIProvider()\ntext = await provider.extract_text_from_file(\n file_bytes=pdf_bytes,\n mime_type=\"application/pdf\",\n prompt=\"Extract all text content from this document.\"\n)\n```\n\n## Provider Selection Strategy\n\n| Use Case | Recommended Provider |\n|----------|---------------------|\n| Fast inference, cost-sensitive | `groq` |\n| Standard OpenAI models | `openai` |\n| Google AI Studio / Vertex | `gemini` or `vertex_ai` |\n| Native Gemini with multimodal | `vertex_ai` |\n| DigitalOcean models | `nemotron` |\n\n## Troubleshooting\n\n### Authentication Errors\n\n**GeminiProvider:**\n- Ensure `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set\n- For Vertex, verify `VERTEX_SERVICE_ACCOUNT_JSON` path and service account permissions\n\n**GroqProvider:**\n- Install groq package: `pip install groq`\n- Set `GROQ_API_KEY`\n\n**NemotronProvider:**\n- Set `DO_MODEL_ACCESS_KEY`\n- Verify DigitalOcean inference endpoint is accessible\n\n### Model Name Issues\n\n**Vertex AI:**\n- Gemini 3.x models require `google/` prefix when using Vertex OpenAI-compatible endpoint\n- Check `VERTEX_MODEL` format matches provider expectations\n\n**GeminiProvider:**\n- Model names starting with `gemini-3` are detected as 3.x family\n- Temperature parameter is ignored for 3.x models (use system instructions instead)\n\n### Empty Responses\n\n**Vertex AI:**\n- `__HERMES_EMPTY_COMPLETION__` indicates thinking-only completion\n- `__HERMES_MALFORMED_FUNCTION_CALL__` indicates tool call parsing error\n- Check `finish_reason` in `response.raw` for diagnostics\n\n## Extending the System\n\n### Adding a New Provider\n\n1. Create provider module in `Harakat_AI/providers/`\n2. Implement `LLMProvider` interface (`chat`, `achat`, `astream`)\n3. Register in `_REGISTRY` in `__init__.py`:\n ```python\n _REGISTRY[\"my_provider\"] = (\".my_provider\", \"MyProvider\")\n ```\n4. Add to `_EXPORTS` for direct imports\n5. Update `__all__` list\n\n### Custom Message Formatting\n\nOverride `_fmt()` method in provider to customize message serialization:\n\n```python\ndef _fmt(self, messages: list[ChatMessage]) -> list[dict]:\n # Custom formatting logic\n return [{\"role\": m.role, \"content\": m.text_content} for m in messages]\n```\n\n### Custom Response Parsing\n\nOverride `_parse_response()` (Vertex AI) or response extraction logic to handle provider-specific response structures.","harakat-ai-rust-ingest":"# Harakat_AI — rust-ingest\n\n\n\n# harakat-ingest\n\n## Overview\n\nThe `harakat-ingest` module is a Rust-based document extraction service designed to run as a short-lived subprocess. Its primary purpose is to ingest various document formats (CSV, DOCX, XLSX, PDF) and extract structured text content into a standardized JSON format.\n\nThe module is engineered for **memory efficiency** (reclaiming resources immediately after extraction) and **output parity** (matching the text shape of the existing Python extractors in `workflows/parse_workflow.py`). It is invoked by the Python service via subprocess to offload heavy lifting without maintaining long-running native dependencies.\n\n## Architecture & Execution Flow\n\nThe module consists of a CLI binary (`harakat-ingest`) and a library crate (`harakat_ingest`). The CLI acts as a dispatcher, routing file paths and format kinds to specific extraction handlers.\n\n### Execution Sequence\n\n1. **Invocation:** The Python service calls `harakat-ingest` via `subprocess.run` with `--kind` (format) and `--input` (path).\n2. **Dispatch:** `main.rs` parses arguments and calls the corresponding `extract` function (e.g., `xlsx::extract`, `docx::extract`).\n3. **Extraction:** The handler reads the file, parses the content, and constructs `ExtractOutput`.\n4. **Serialization:** `ExtractOutput::print_stdout` serializes the result to a single JSON line.\n5. **Reclamation:** The Rust process exits, freeing memory.\n\n```mermaid\nsequenceDiagram\n participant Python as Python Service\n participant CLI as harakat-ingest (main.rs)\n participant Handler as Extractor (csv/docx/xlsx/pdf)\n participant Output as output.rs\n\n Python->>CLI: subprocess.run([--kind, --input])\n CLI->>Handler: extract(path, name)\n Handler->>Handler: Parse (zip/xml/csv/pdfium)\n Handler->>Output: Return ExtractOutput\n Output->>CLI: print_stdout()\n CLI->>Python: JSON stdout\n CLI->>CLI: exit(0)\n```\n\n### CLI Dispatch Logic\n\nThe `main` function in `src/main.rs` matches the `--kind` argument to the appropriate module:\n\n```rust\nlet result: anyhow::Result<ExtractOutput> = match cli.kind.as_str() {\n \"xlsx\" => xlsx::extract(&cli.input),\n \"docx\" => docx::extract(&cli.input),\n \"csv\" => csv::extract(&cli.input, &cli.name),\n #[cfg(feature = \"pdf\")]\n \"pdf_text\" => pdf::extract(&cli.input),\n other => Err(anyhow::anyhow!(\"unsupported kind: {other}\")),\n};\n```\n\n## Core Data Structures\n\nAll extractors return a unified `ExtractOutput` defined in `src/output.rs`. This ensures consistent serialization regardless of the input format.\n\n### `Page`\n\nRepresents a single page of extracted content.\n* `page`: 1-based integer index.\n* `text`: The extracted string content.\n\n```rust\npub struct Page {\n pub page: u32,\n pub text: String,\n}\n```\n\n### `ExtractOutput`\n\nThe container for the extraction result.\n* `pages`: Vector of `Page` objects.\n* `parse_method`: String identifier (e.g., `\"calamine\"`, `\"pdfium\"`, `\"csv\"`).\n\n```rust\npub struct ExtractOutput {\n pub pages: Vec<Page>,\n pub parse_method: String,\n}\n```\n\n## Format Handlers\n\n### CSV (`src/csv.rs`)\n\n* **Handler:** `csv::extract`\n* **Dependencies:** `csv`, `anyhow`\n* **Behavior:**\n * Reads file with `utf-8-sig` semantics (strips BOM, lossy decode).\n * Skips empty cells and fully blank rows.\n * Truncates output at 5000 lines to prevent memory bloat.\n * Output format:\n ```text\n CSV: <name>\n Columns: <col1> | <col2>\n ----------------------------------------\n Row 2: <col1>: <val1> | <col2>: <val2>\n ```\n* **Key Logic:** `decode_utf8_sig_lossy` handles BOM stripping.\n\n### DOCX (`src/docx.rs`)\n\n* **Handler:** `docx::extract`\n* **Dependencies:** `zip`, `quick-xml`, `anyhow`\n* **Behavior:**\n * Treats DOCX as a ZIP archive.\n * Parses `word/document.xml` using `quick-xml`.\n * **Excludes tables:** Paragraphs inside `<w:tbl>` are ignored to match Python's `body-level only` logic.\n * Concatenates paragraphs with `\\n\\n`.\n* **Key Logic:** `body_paragraphs` tracks `tbl_depth` to filter out table content.\n\n### XLSX (`src/xlsx.rs`)\n\n* **Handler:** `xlsx::extract`\n* **Dependencies:** `calamine`, `anyhow`\n* **Behavior:**\n * Uses `calamine` for streaming, low-memory parsing.\n * Iterates sheets; each sheet becomes a `Page`.\n * Skips empty sheets and fully blank rows.\n * **Date Handling:** `Data::DateTime` is converted to `YYYY-MM-DD HH:MM:SS` to match `openpyxl`'s `data_only` output.\n * **Number Formatting:** `fmt_num` strips trailing `.0` for whole numbers to match Python stringification.\n* **Key Logic:** `cell_to_string` handles type conversion (Float, Int, Bool, DateTime).\n\n### PDF (`src/pdf.rs`)\n\n* **Handler:** `pdf::extract`\n* **Dependencies:** `pdfium-render` (Optional feature `pdf`)\n* **Behavior:**\n * Binds to system `libpdfium` at runtime.\n * Extracts text per page.\n * **Normalization:** Converts CRLF to LF to match PyMuPDF's `get_text(\"text\")` output.\n* **Key Logic:** `Pdfium::bind_to_system_library` requires `libpdfium.so` on the loader path.\n\n## Testing & Parity Verification\n\nThe module's correctness is validated against the Python extractors in `workflows/parse_workflow.py`.\n\n### Golden Tests (`golden/`)\n\n1. **`run_golden.py`**:\n * Runs against fixtures in `golden/fixtures/`.\n * Compares normalized text (`norm()`) between Python and Rust outputs.\n * Exits 0 only if page counts and text match exactly.\n * Prints unified diffs on failure.\n\n2. **`pdf_parity_probe.py`**:\n * Generates a specific multi-page PDF with two-column layouts.\n * Compares PyMuPDF vs. `pdfium` (Rust) output.\n * Calculates Jaccard similarity and character ratios.\n * Used for Phase 2 parity verdicts regarding reading order.\n\n### Unit Tests (`tests/`)\n\n* **`tests/cli.rs`**: Verifies unsupported kinds exit with non-zero status.\n* **`tests/xlsx.rs`**: Verifies `xlsx::extract` output format (headers, row numbering, blank cell skipping).\n\n## Build & Configuration\n\n### Features\n\n* `pdf`: Enables `pdfium-render` dependency and `src/pdf` module.\n * **Requirement:** `libpdfium.so` must be available on the loader path at runtime.\n * **Default:** Disabled (lean build).\n\n### Dependencies\n\n* **Core:** `clap` (CLI), `serde`/`serde_json` (Serialization), `anyhow` (Error handling).\n* **Formats:**\n * `csv`: CSV parsing.\n * `zip` + `quick-xml`: DOCX parsing.\n * `calamine`: XLSX parsing.\n * `pdfium-render`: PDF parsing (optional).\n\n### Release Profile\n\nOptimized for performance and size:\n* `opt-level = 3`\n* `lto = true`\n* `strip = true`\n\n## Integration with Python Service\n\nThe Python service invokes the Rust binary as a subprocess. If the Rust process exits with a non-zero code, the Python service treats the extraction as failed and falls back to its native implementation.\n\n```python\n# Example invocation pattern (from golden/run_golden.py)\nout = subprocess.run(\n [BIN, \"--kind\", kind, \"--input\", str(path), \"--name\", path.name],\n capture_output=True,\n check=True,\n)\nreturn json.loads(out.stdout)[\"pages\"]\n```\n\n## Contribution Guidelines\n\n1. **Parity First:** Any new format handler must match the Python output format exactly (line endings, whitespace, data types).\n2. **Memory Safety:** Handlers should avoid holding large file handles open longer than necessary.\n3. **Error Handling:** Use `anyhow::Result` and ensure `main.rs` exits with code 1 on error.\n4. **Testing:** Add unit tests in `tests/` and update `golden/fixtures/` if output format changes.","harakat-ai-scripts":"# Harakat_AI — scripts\n\n\n\n# Harakat_AI — scripts Module\n\n## Overview\n\nThe **scripts** module contains a comprehensive suite of self-test programs that validate critical functionality of the Harakat AI system. These tests run in CI on every push, ensuring that changes to the AI half of the system do not introduce regressions.\n\n**Key characteristics:**\n- **No network calls** — all tests run offline\n- **No GPU/LLM dependencies** — pure function tests where possible\n- **Stdlib-only** — no external dependencies that could silently disable tests\n- **No shared fixtures** — each repository gates its own half of the contract\n\n## Purpose\n\nThis module exists to prove that the AI half of the system works correctly by testing:\n\n1. **Authentication & Security** — signature verification, replay protection, ASGI middleware\n2. **Prompt Assembly** — creation kickoff, guided creation, system prompt ordering\n3. **Tool & Grant Management** — grant-gated tools, toolset visibility, policy enforcement\n4. **Document Processing** — evidence rendering, corpus parsing, OCR benchmarking\n5. **Category Agents** — doc subagent, ERP subagent, child agent delegation\n6. **Connector Handling** — connector refusal recognition, structured error propagation\n7. **Durability Contracts** — database paths, state management, migration rules\n\n## Architecture\n\n```mermaid\ngraph TB\n subgraph \"Test Scripts\"\n A[ai_hop_selftest.py]\n B[approval_card_selftest.py]\n C[artifacts_selftest.py]\n D[b3_kickoff_selftest.py]\n E[category_agents_selftest.py]\n F[connector_cause_selftest.py]\n G[corpus_v2_selftest.py]\n H[creation_context_redaction_selftest.py]\n I[doc_evidence_selftest.py]\n end\n \n subgraph \"Core Modules Under Test\"\n J[core.source_auth]\n K[agents.hermes_native]\n L[agents.specialists]\n M[core.tools]\n N[core.vision_reader]\n end\n \n A --> J\n B --> K\n C --> M\n D --> K\n E --> K\n F --> K\n G --> N\n H --> L\n I --> L\n```\n\n## Key Components\n\n### 1. Authentication & Middleware Tests (`ai_hop_selftest.py`)\n\nTests the source authentication system that protects server→AI hop requests.\n\n**What it validates:**\n- **Shared test vectors** — byte-identical signatures between Python and TypeScript\n- **Signature verification** — happy path and all refusal conditions\n- **Replay deduplication** — memory and SQLite backends with durability\n- **Boot refusals** — secret validation, conflict detection\n- **ASGI middleware** — signed/unsigned requests, replay detection, exemptions\n\n**Key functions:**\n```python\nverify_signature(secret, signature, timestamp, nonce, method, path_with_query, body, ...)\nsign(secret, timestamp, nonce, method, path, body)\nMemoryReplayDedupe()\nSqliteReplayDedupe(db_path)\nSourceAuthMiddleware(app, settings, replay_store)\n```\n\n**Refusal conditions tested:**\n- Unsigned requests\n- Unknown scheme (e.g., `v9=...` instead of `v0=...`)\n- Garbage/missing timestamps\n- Stale timestamps (older than window)\n- Future timestamps beyond skew allowance\n- Empty/absurd nonces\n- Tampered body, path, method, or nonce\n- No secret configured\n- Secret conflicts with other API keys\n\n### 2. Approval Card Tests (`approval_card_selftest.py`)\n\nTests the approval card's `why` and `triggered_by` fields in action previews.\n\n**What it validates:**\n- **W1** — `why` is the agent's own words from reasoning\n- **W2** — `why` is spent (one-time use per proposal)\n- **W3** — absent `why` is never rendered as blank\n- **T1** — `triggered_by` names the step or creation target\n- **T2** — `triggered_by` is absent for `tool_always_asks` rules\n\n**Key functions:**\n```python\nsse_events.map_action_preview(tool, args, label, preview, why=None, ...)\nsse_events.PendingPurpose()\n```\n\n### 3. Artifacts Publishing Tests (`artifacts_selftest.py`)\n\nTests the publishing tool's grant-gating and durability contract.\n\n**What it validates:**\n- **Grant-gating** — `publish` tool only exists with a grant\n- **No tier floor** — tier doesn't gate publishing, grants do\n- **Durability contract** — description contains all required sentences\n- **Executor safety** — no filesystem access, no process spawning\n\n**Key functions:**\n```python\nderive_tools(access_tier, role, grant_tools=[])\ncore.tools._publish_app()\n```\n\n### 4. Creation Kickoff Tests (`b3_kickoff_selftest.py`)\n\nTests the `__CREATE__:` sentinel handling and prompt assembly.\n\n**What it validates:**\n- **Prompt ordering** — PLANNER_ROUTING_RULE first, creation prompt appended\n- **flow_mermaid threading** — only for create tools\n- **Request threading** — `creation_kind` and `creation_scope_hint` reach runtime\n\n**Key functions:**\n```python\naia_runner._maybe_append_creation_prompt(body, system_prompt)\nAssistantRequest(task, creation_kind=None, creation_scope_hint=None)\n```\n\n### 5. Category Agents Tests (`category_agents_selftest.py`)\n\nTests the doc subagent and ERP subagent delegation system.\n\n**What it validates:**\n- **Doc subagent** — preview vs execute modes, child tool allowlists\n- **ERP subagent** — child runs with app-specific skills, policy enforcement\n- **Catalog swap** — folded tools leave main toolset when subagent is on\n- **Policy enforcement** — forbidden tools in child context are rejected\n- **Billing** — child costs land on parent budget\n\n**Key functions:**\n```python\nchild_runner.run_child_blocking()\nvalidate_category_child_policy(label, session, selection, ...)\n```\n\n### 6. Connector Cause Tests (`connector_cause_selftest.py`)\n\nTests the structured connector refusal handling.\n\n**What it validates:**\n- **R1** — recognition is structural (names), not textual\n- **R2** — frame carries names, no prose\n- **R3** — `map_action_result` carries connector cause\n- **R4** — `done` frame carries it too\n- **R5** — one card per connector per turn\n\n**Key functions:**\n```python\nsse_events.connector_refusal(data)\nsse_events.map_action_result(tool, success, error, run_id, connector=None, connector_cause=None)\nsse_events.map_connector_required(connector, cause, tool, run_id)\n```\n\n### 7. Corpus V2 Tests (`corpus_v2_selftest.py`)\n\nTests corpus parsing and DXF continuation folding.\n\n**What it validates:**\n- **Facts delimiter** — handles all failure shapes with named reasons\n- **Parse methods** — three methods, four distinct failure modes\n- **DXF repair** — lossless on well-formed input\n- **Mutants** — four broken variants that must fail\n\n**Key functions:**\n```python\nsplit_card_and_facts(card, facts_delimiter, facts_json)\n_fold_leaked_continuations(dxf_content)\n```\n\n### 8. Creation Context Redaction Tests (`creation_context_redaction_selftest.py`)\n\nTests that creation runs don't leak existing rule bodies.\n\n**What it validates:**\n- **Normal runs** — existing rules are instructions, keep them\n- **Creation runs** — titles visible, bodies withheld\n\n**Key functions:**\n```python\n_build_envelopes(context, redact_rule_bodies=False)\n_render_context(envelopes)\n```\n\n### 9. Document Evidence Tests (`doc_evidence_selftest.py`)\n\nTests document evidence rendering and pack normalization.\n\n**What it validates:**\n- **Figures survive** — verbatim transit from researcher to composer\n- **Source attribution** — every fact names its source\n- **Pack normalization** — safe degradation of malformed input\n- **Layout propagation** — structure observations reach composer\n\n**Key functions:**\n```python\n_normalize_pack(pack)\nrender_evidence_block(pack)\n```\n\n## Common Patterns\n\n### Test Helper Functions\n\nAll scripts use a consistent `check()` pattern:\n\n```python\ndef check(name: str, condition: bool, detail: str = \"\") -> None:\n if condition:\n print(f\" ✓ {name}\")\n else:\n print(f\" ✗ {name}{(' — ' + detail) if detail else ''}\")\n```\n\n### Module Loading Strategy\n\nScripts that need to test internal modules use a stub loading pattern to avoid heavy dependencies:\n\n```python\ndef _load_module(name: str, rel: str):\n spec = importlib.util.spec_from_file_location(name, ROOT / rel)\n mod = importlib.util.module_from_spec(spec)\n sys.modules[name] = mod\n spec.loader.exec_module(mod)\n return mod\n```\n\n### Environment Configuration\n\nScripts that need external services stub them:\n\n```python\ndef _stub_httpx() -> None:\n stub = types.ModuleType(\"httpx\")\n stub.AsyncClient = lambda *_a, **_k: (_ for _ in ()).throw(RuntimeError(\"no network\"))\n sys.modules[\"httpx\"] = stub\n```\n\n## Integration Points\n\n### Core Modules\n\n| Script | Core Module | Purpose |\n|--------|-------------|---------|\n| `ai_hop_selftest.py` | `core.source_auth` | Authentication, signatures, replay |\n| `approval_card_selftest.py` | `agents.hermes_native.sse_events` | Action preview generation |\n| `artifacts_selftest.py` | `core.tools`, `agents.specialists.tools` | Publishing tool, grants |\n| `b3_kickoff_selftest.py` | `agents.hermes_native.aia_runner` | Creation kickoff |\n| `category_agents_selftest.py` | `agents.hermes_native.category_agents` | Doc/ERP subagents |\n| `connector_cause_selftest.py` | `agents.hermes_native.sse_events` | Connector refusal |\n| `corpus_v2_selftest.py` | `core.vision_reader` | Corpus parsing |\n| `creation_context_redaction_selftest.py` | `agents.specialists.internal_state` | Context rendering |\n| `doc_evidence_selftest.py` | `agents.hermes_native.category_agents.doc_subagent` | Evidence rendering |\n\n### External Dependencies\n\nScripts avoid external dependencies by:\n1. Using stdlib stubs for network calls\n2. Stubbing heavy packages (httpx, pydantic, dotenv)\n3. Running pure function tests where possible\n4. Using `types.ModuleType` for type-only imports\n\n## Running the Tests\n\n```bash\n# Run a specific test\npython scripts/ai_hop_selftest.py\npython scripts/category_agents_selftest.py\n\n# Run all tests\nfor script in scripts/*.py; do\n echo \"=== $script ===\"\n python \"$script\"\ndone\n```\n\n## CI Integration\n\nAll scripts are designed to run in CI with:\n- No pip installs (stdlib only)\n- No GPU access\n- No network calls\n- Deterministic results\n\n## Design Principles\n\n1. **No shared fixtures** — each repository gates its own half\n2. **Fail loudly** — tests that don't run are worse than tests that fail\n3. **Byte-identical contracts** — shared vectors between repos\n4. **Pure functions** — testable without side effects\n5. **Stdlib-only** — no dependency that could silently disable tests\n\n## Contributing\n\nWhen adding new tests:\n1. Follow the `check()` pattern\n2. Use stubs for external dependencies\n3. Keep tests offline and deterministic\n4. Document the contract being tested\n5. Ensure tests run without network/GPU\n\n## Related Documentation\n\n- `HarakatOffices_Server/src/scripts/ai-hop-selftest.ts` — TypeScript half of authentication tests\n- `StartupsPlatform/lib/assistant/__tests__/chat-placement.test.ts` — Client half of approval card tests\n- `Harakat_AI/core/source_auth.py` — Source authentication implementation\n- `Harakat_AI/agents/hermes_native/sse_events.py` — SSE event generation","harakat-ai-src":"# Harakat_AI — src\n\n\n\n# Harakat_AI Runtime Module\n\n## Overview\nThe `Harakat_AI/src/runtime` module serves as the execution engine for AI tasks within the Harakat ecosystem. It exposes a REST API to accept task requests, routes them to specific agent workspaces based on the agent identifier, executes the logic, and reports completion status back to the Harakat orchestration layer.\n\n## Architecture\nThe runtime follows a layered architecture:\n1. **API Layer**: `main.py` handles HTTP requests and response serialization.\n2. **Controller Layer**: `UVController` manages the lifecycle of a task execution.\n3. **Workspace Layer**: `WorkspaceManager` resolves the appropriate execution environment (Space) based on the agent type.\n4. **Executor Layer**: Specific `Space` classes (e.g., `ExpertSpace`, `VirtualAssistantSpace`) contain the actual logic or delegate to external services.\n\n### Execution Flow\n```mermaid\nsequenceDiagram\n participant Client\n participant API as FastAPI (/run)\n participant Ctrl as UVController\n participant WM as WorkspaceManager\n participant Space as Space (Executor)\n participant Harakat as Harakat API\n\n Client->>API: POST /run (AITask)\n API->>Ctrl: execute_task(task)\n Ctrl->>WM: resolve_workspace(agent)\n WM->>Space: run(task)\n Space-->>Ctrl: result\n Ctrl->>Harakat: PATCH /ai/task/complete\n Harakat-->>Ctrl: 200 OK\n Ctrl-->>API: result\n API-->>Client: result\n```\n\n## Key Components\n\n### FastAPI Application (`runtime/main.py`)\nThe entry point for the runtime service.\n\n* **`app`**: A `FastAPI` instance configured with the title \"Harakat AI Runtime\".\n* **`AITask`**: Pydantic model defining the input payload:\n * `requestId`: Unique identifier for the task.\n * `officeId`: Integer identifier for the office.\n * `agent`: String identifier for the target agent (e.g., \"assistant\", \"marketing\").\n * `taskType`: String describing the task type.\n * `payload`: Dictionary containing task-specific data.\n* **`/run` (POST)**:\n * Accepts an `AITask` instance.\n * Calls `UVController().execute_task(task)`.\n * **Callback Logic**: After execution, it attempts to notify the Harakat orchestration layer via a PATCH request to `{HARAKAT_API_URL}/ai/task/complete` with the status \"SUCCEEDED\" and the result.\n * **Environment Variables**: Requires `HARAKAT_API_URL` and `HARAKAT_API_KEY` to function. If missing, a warning is logged and the callback is skipped.\n\n### UV Controller (`runtime/uv/controller.py`)\nThe controller orchestrates the execution logic.\n\n* **`UVController`**:\n * **`__init__`**: Initializes a `WorkspaceManager` instance.\n * **`execute_task(task)`**:\n 1. Calls `self.manager.resolve_workspace(task.agent)` to get the appropriate `Space` instance.\n 2. Invokes `space.run(task)` and returns the result.\n\n### Workspace Manager (`runtime/uv/workspace_manager.py`)\nManages the registry of available agent workspaces and handles path resolution for internal agent imports.\n\n* **`WorkspaceManager`**:\n * **`__init__`**: Instantiates `VirtualAssistantSpace`, `ExpertSpace`, and `PricingSpace` and stores them in a `self.spaces` dictionary.\n * **`resolve_workspace(agent)`**: Maps the `agent` string to a specific workspace:\n * `\"assistant\"` or `\"va\"` → `VirtualAssistantSpace`\n * `\"marketing\"` or `\"finance\"` → `ExpertSpace`\n * `\"pricing\"` → `PricingSpace`\n * *Error*: Raises `ValueError` for unknown agents.\n * **Path Resolution**: Modifies `sys.path` to include the `agents` directory located at `BASE_DIR/agents`. This allows `ExpertSpace` to import internal agent modules (e.g., `Harakat_AI.agents.MarketingAgent.agent`).\n\n### Executors (`runtime/uv/executor.py`)\nContains the concrete implementations of the workspaces.\n\n* **`VirtualAssistantSpace`**:\n * **`run(task)`**: Returns a static response indicating the workspace and a generic output message.\n* **`ExpertSpace`**:\n * **`run(task)`**: Routes based on `task.agent.lower()`:\n * `\"virtual-ea\"`: Sends a POST request to `http://localhost:8080/run` with the task payload. Returns the JSON response or an error object if the request fails.\n * `\"marketing\"`: Imports and executes `Harakat_AI.agents.MarketingAgent.agent.run(task.payload)`.\n * *Default*: Returns a failure status with an error message.\n* **`PricingSpace`**:\n * **`run(task)`**: Returns a static response estimating 15 credits.\n\n## Configuration\nThe runtime relies on the following environment variables:\n\n| Variable | Description | Required |\n| :--- | :--- | :--- |\n| `HARAKAT_API_URL` | Base URL for the Harakat orchestration API. | Yes (for callback) |\n| `HARAKAT_API_KEY` | Bearer token for authenticating with the Harakat API. | Yes (for callback) |\n\n## Integration Points\n* **Harakat Orchestration**: The runtime acts as a worker node. It receives tasks and reports completion via the `/ai/task/complete` endpoint.\n* **External Microservices**: The `ExpertSpace` class supports delegating specific tasks (e.g., `virtual-ea`) to external Docker microservices running on `localhost:8080`.\n* **Internal Agents**: The `ExpertSpace` class supports direct imports of internal agent logic (e.g., `MarketingAgent`), provided the `agents` directory is correctly mounted in the runtime environment.\n\n## Error Handling\n* **Unknown Agent**: If `resolve_workspace` cannot map the `agent` string, a `ValueError` is raised.\n* **External Service Failure**: If the `virtual-ea` HTTP request fails, `ExpertSpace` returns a JSON object with `status: \"failed\"` and the error message.\n* **Callback Failure**: If the Harakat API callback fails (e.g., network error), a warning is printed to the console, but the task result is still returned to the client.","harakat-ai-tests":"# Harakat_AI — tests\n\n\n\n# Harakat_AI/tests Module Documentation\n\n## Overview\n\nThe `Harakat_AI/tests` module contains the complete test suite for the Harakat AI system. It validates functionality across multiple subsystems including tool bridging, document processing, procedure execution, memory management, and runtime budgeting.\n\n## Purpose\n\nThis test module ensures:\n- **Correctness**: Functional behavior matches specifications\n- **Safety**: No accidental tool/graph imports in stateless endpoints\n- **Performance**: Budget controls prevent runaway costs\n- **Integration**: Hermes-native components work correctly with Harakat tools\n- **Regression**: Critical behaviors (like search deduplication) remain intact\n\n## Module Structure\n\n```\nHarakat_AI/tests/\n├── __init__.py\n├── test_action_result_event.py # SSE event formatting\n├── test_batch_embed.py # Batch embedding logic\n├── test_batch_thinking_config.py # Gemini thinking config\n├── test_coerce_procedure_steps.py # Procedure step coercion\n├── test_create_skill_binds.py # Skill binding validation\n├── test_embed_batch_endpoints.py # FastAPI embed endpoints\n├── test_erp_customize_discoverable.py # ERP tool discovery\n├── test_extract_dispatch.py # Document extraction (XLSX)\n├── test_feed_proposals.py # Autoload proposal generation\n├── test_governance_draft.py # Governance artifact drafting\n├── test_hermes_native_phase2.py # Tool bridge (Phase 2)\n├── test_hermes_native_phase3.py # Memory provider (Phase 3)\n├── test_hermes_native_phase4.py # SSE callbacks (Phase 4)\n├── test_hermes_native_phase5.py # AIAgent runner (Phase 5)\n├── test_honcho_user_memory.py # Per-user memory (Phase 10)\n├── test_local_guard.py # Local profile coherence\n├── test_parse_workflow_docx.py # DOCX extraction\n├── test_procedure_executor.py # Procedure execution (Phase 7)\n├── test_registry_reassert_r2.py # Toolset registry safety\n├── test_run_budget.py # Run budget management\n├── test_run_id_header.py # Run ID header propagation\n├── test_rust_ingest.py # Rust document ingestion\n├── test_search_documents_r2.py # Document search (retrieval v2)\n├── test_search_failsoft.py # Search failsoft behavior\n├── test_stt_bridge.py # Speech-to-text bridge\n└── test_surface_prompt.py # Dashboard surface prompt\n```\n\n## Key Components\n\n### 1. Hermes-Native Integration Tests\n\nThe `test_hermes_native_phase*.py` files validate the integration between Hermes agent and Harakat tools across five phases:\n\n```mermaid\ngraph LR\n A[SessionContext] --> B[Phase 2: Tool Bridge]\n B --> C[Phase 3: Memory Provider]\n C --> D[Phase 4: SSE Callbacks]\n D --> E[Phase 5: AIAgent Runner]\n E --> F[Phase 10: Honcho Memory]\n```\n\n**Phase 2** (`test_hermes_native_phase2.py`):\n- Validates tier-based tool filtering\n- Tests JWT-scoped context propagation\n- Verifies preview-gated tool execution\n\n**Phase 3** (`test_hermes_native_phase3.py`):\n- Tests memory provider initialization\n- Validates system prompt block construction\n- Verifies prefetch combines autoload docs and tools\n\n**Phase 4** (`test_hermes_native_phase4.py`):\n- Tests SSE callback bridge\n- Validates unfulfilled claim correction\n- Verifies tool call tracking\n\n**Phase 5** (`test_hermes_native_phase5.py`):\n- Tests AIAgent runner event streaming\n- Validates runtime policy enforcement\n- Verifies shell context and skills sync\n\n### 2. Procedure Execution Tests\n\n`test_procedure_executor.py` validates the ERI Phase 7 procedure execution system:\n\n```mermaid\ngraph TD\n A[Procedure ID] --> B[Start & Drive]\n B --> C{Step Type}\n C -->|Human Input| D[Collect Input]\n C -->|Decision| E[Route Branch]\n C -->|Assistant Action| F[Execute Tool]\n F --> G{Preview Required?}\n G -->|Yes| H[Pause for Approval]\n G -->|No| I[Continue]\n H --> I\n I --> J{Success?}\n J -->|Yes| K[Next Step]\n J -->|No| L{Failure Route}\n L -->|retry| K\n L -->|escalate| M[End]\n```\n\nKey validations:\n- Risk manifest classification\n- Argument resolution from step context\n- Bounded result truncation\n- Preview frame emission from tool results\n- Failure routing (retry, escalate, abort)\n\n### 3. Run Budget Management\n\n`test_run_budget.py` implements critical cost-control logic:\n\n```mermaid\ngraph TD\n A[RunStart] --> B[Initialize Budget]\n B --> C{Model Call}\n C -->|Success| D[Track Tokens]\n C -->|Fail| E[Fail Fast]\n D --> F{Retrieval Result}\n F -->|New Content| G[Increment Progress]\n F -->|Repeat| H[Increment No-Progress]\n H --> I{Streak > Threshold?}\n I -->|Yes| J[Exhausted Payload]\n I -->|No| K[Continue]\n G --> K\n J --> L[Ground Question]\n K --> M{Cost > Cap?}\n M -->|Yes| N[Hard Cap Trip]\n M -->|No| K\n```\n\nKey behaviors:\n- **Converging searches**: Multiple document reads don't trip budget\n- **Dead-end detection**: Repeated empty searches trigger exhaustion\n- **External cost tracking**: Delegated child runs count toward budget\n- **Live child pricing**: Child spend visible while running, not just after return\n\n### 4. Document Processing Tests\n\nMultiple files validate document extraction and search:\n\n| File | Purpose |\n|------|---------|\n| `test_extract_dispatch.py` | XLSX extraction with Rust fallback |\n| `test_parse_workflow_docx.py` | DOCX extraction with null-relationship repair |\n| `test_search_documents_r2.py` | Retrieval v2 with repeat prevention |\n| `test_search_failsoft.py` | Failsoft behavior when v2 unavailable |\n| `test_rust_ingest.py` | Rust binary availability checks |\n\n### 5. Stateless Endpoint Tests\n\n`test_feed_proposals.py` and `test_governance_draft.py` validate stateless endpoints that must NOT import tool/graph machinery:\n\n```python\nforbidden = (\"core.tools\", \"agents.hermes_native\", \"agents.base\", \"langgraph\")\nfor name in forbidden:\n assert name not in src, f\"must not reference {name}\"\n```\n\n## Testing Patterns\n\n### 1. Mocking External Services\n\n```python\n# Hermes provider mocking\nclass _FakeProvider:\n async def achat(self, messages, **kwargs):\n return _FakeResponse(_CANNED)\n\nmonkeypatch.setattr(fp, \"get_provider\", lambda *a, **k: fake)\n```\n\n### 2. Live Server Tests\n\n```python\n@live\ndef test_live_drive_search_then_email_pauses_on_preview():\n session = _login()\n ctx = _ctx(session)\n procedure_id = _seed_procedure(session, [...], f\"p7-exec-preview-{os.getpid()}\")\n report = run(start_and_drive(ctx, procedure_id))\n assert report[\"status\"] == \"paused_awaiting_approval\"\n```\n\n### 3. Environment-Based Skipping\n\n```python\nlive = pytest.mark.skipif(\n os.getenv(\"HONCHO_ENABLED\", \"off\").lower() != \"on\" or not _honcho_up(),\n reason=\"HONCHO_ENABLED=on + honcho :8010 required\"\n)\n```\n\n### 4. AST-Based Code Validation\n\n```python\ntree = ast.parse(inspect.getsource(tools._create_skill))\n# Walk AST to verify _call_backend invocation with correct bindings\n```\n\n## Integration Points\n\n### Core Modules Tested\n\n| Test File | Core Module |\n|-----------|-------------|\n| `test_batch_embed.py` | `core.batch_embed` |\n| `test_batch_thinking_config.py` | `core.batch_desc`, `core.batch_entities` |\n| `test_coerce_procedure_steps.py` | `core.tools._coerce_procedure_steps` |\n| `test_run_budget.py` | `agents.hermes_native.run_budget` |\n| `test_search_documents_r2.py` | `core.tools._search_documents` |\n\n### Hermes Agent Integration\n\nTests import from `vendor/hermes-agent`:\n```python\nfrom agents.hermes_native import (\n mcp_toolset,\n tier_guard,\n harakat_toolset,\n tool_bridge,\n sse_events,\n procedure_executor,\n)\n```\n\n### FastAPI Integration\n\n```python\nfrom fastapi.testclient import TestClient\nfrom app import app\n\nclient = TestClient(app)\nr = client.post(\"/embed/batch/submit\", json={\"texts\": [\"hi\", \"there\"]})\n```\n\n## Running Tests\n\n### Unit Tests\n```bash\npytest Harakat_AI/tests/ -v\n```\n\n### Live Tests (Server Required)\n```bash\nHARAKAT_API_URL=http://localhost:5000 \\\nTOOL_GATEWAY=on \\\npython -m pytest Harakat_AI/tests/test_procedure_executor.py -v\n```\n\n### Honcho Memory Tests\n```bash\nHONCHO_ENABLED=on \\\npython -m pytest Harakat_AI/tests/test_honcho_user_memory.py -v\n```\n\n## Key Invariants\n\n1. **Stateless endpoints** must not import `core.tools`, `agents.hermes_native`, or `langgraph`\n2. **Search deduplication** must be per-normalized-query, not per-call\n3. **Run budget** must track external costs from delegated children\n4. **Tier filtering** must match between Hermes-native and core tools\n5. **Local profile** must not drift to production URLs\n6. **Procedure preview** must pause execution until approved\n\n## Regression Guards\n\nSeveral tests explicitly guard against future regressions:\n\n- `test_registry_reassert_r2.py`: Concurrent-run toolset collision\n- `test_run_budget.py`: 2026-07-13 search eval regression\n- `test_local_guard.py`: Production URL drift prevention\n- `test_search_documents_r2.py`: Legacy multi-surface removal\n\n## Contributing\n\nWhen adding new tests:\n\n1. **Isolate dependencies**: Load modules directly to avoid pulling in the whole AI service\n2. **Mock external calls**: Use `monkeypatch` for HTTP/backend calls\n3. **Validate shapes**: Assert exact payload structures, not just truthiness\n4. **Test edge cases**: Empty results, errors, timeouts, budget exhaustion\n5. **Document invariants**: Add comments explaining why a test exists\n\n```python\n\"\"\"TDD for the stateless autoload-proposal (feed) endpoint.\n\nMirrors the statelessness of /assistant/draft-onboarding-governance:\nprovider-direct, no tools, no graph, no session.\n\"\"\"\n```\n\n## Dependencies\n\nTests depend on:\n- `pytest` with `monkeypatch` support\n- `fastapi.testclient` for API testing\n- `httpx` for live server tests\n- `ast` and `inspect` for code validation\n- `docx` for document extraction tests\n- `hermes-agent` from `vendor/` directory\n\n## Notes\n\n- Tests use `importlib.util.spec_from_file_location` to load modules directly\n- Live tests are automatically skipped when dependencies unavailable\n- Some tests use `SimpleNamespace` to mock complex objects\n- The `run()` helper wraps `asyncio.run()` for test convenience","harakat-ai-workers":"# Harakat_AI — workers\n\n\n\n# Harakat_AI.workers.document_indexer\n\nThe `document_indexer` module implements the core ingestion pipeline for the Harakat Knowledge Graph. It is designed as an asynchronous background worker that accepts document jobs, retrieves files from external sources, extracts text using appropriate methods (Gemini OCR, pdfplumber, or native parsers), structures the content into a semantic tree using PageIndex, and persists the resulting sections into the backend Knowledge Graph.\n\n## Overview\n\nThe module handles the full lifecycle of document indexing:\n1. **Ingestion:** Securely downloads files from external URLs.\n2. **Extraction:** Converts binary files into text using MIME-type-aware strategies.\n3. **Structuring:** Generates a hierarchical section tree via PageIndex.\n4. **Persistence:** Upserts section nodes into the Knowledge Graph.\n5. **Reporting:** Updates the execution queue status upon completion or failure.\n\nThe module is intended to be invoked via FastAPI background tasks:\n```python\nfrom workers.document_indexer import index_document\nasyncio.create_task(index_document(job))\n```\n\n## Architecture\n\nThe indexing process follows a linear pipeline with specific fallback logic for text extraction.\n\n```mermaid\nflowchart TD\n Start[Start index_document] --> Download[_download_file]\n Download --> Extract{Select Extractor}\n Extract -->|Image| Gemini[extract_text_gemini]\n Extract -->|PDF| CheckPDF{Born-digital?}\n CheckPDF -->|Yes| PdfPlumber[_extract_text_pdfplumber]\n CheckPDF -->|No| Gemini\n Extract -->|Office/Plain| Native[_OFFICE_EXTRACTORS]\n Gemini --> Build[_build_page_index]\n PdfPlumber --> Build\n Native --> Build\n Build --> Upsert[_upsert_knowledge_nodes]\n Upsert --> Queue[Update Queue Status]\n Queue --> End[Return Result]\n```\n\n## Core Components\n\n### `IndexingJob`\nA dataclass representing a pending indexing task. It encapsulates the necessary metadata to retrieve and process a document.\n\n```python\nclass IndexingJob:\n document_id: int\n startup_id: int\n file_url: str\n filename: str\n access_tier: str\n auth_token: str\n queue_id: int | None\n```\n\n### `index_document(job)`\nThe primary entry point. It is an `async` function that never raises exceptions; instead, it logs errors and returns a result dictionary.\n\n**Execution Flow:**\n1. **Download:** Calls `_download_file` to retrieve content.\n2. **Extraction:** Determines the MIME type and selects the appropriate extractor (`_extract_text_gemini`, `_extract_text_pdfplumber`, or `_OFFICE_EXTRACTORS`).\n3. **Indexing:** Calls `_build_page_index` to generate section nodes.\n4. **Upsert:** Calls `_upsert_knowledge_nodes` to persist data.\n5. **Status:** Calls `_update_queue_status` to report completion.\n\n### Text Extraction Strategy\nThe module employs a tiered extraction strategy to balance speed and accuracy.\n\n| File Type | Primary Method | Fallback |\n| :--- | :--- | :--- |\n| **Image** | `VertexAIProvider` (Gemini) | N/A |\n| **PDF** | `pdfplumber` (Born-digital) | `VertexAIProvider` (Scanned) |\n| **Office** | `docx` / `openpyxl` | N/A |\n| **Plain** | UTF-8 Decode | N/A |\n\n**PDF Handling Logic:**\n1. The module attempts `pdfplumber` first for speed.\n2. If the extracted text is less than 200 characters, it assumes the PDF is scanned and falls back to `Gemini` (`_extract_text_gemini`).\n3. `_extract_text_gemini` calls `VertexAIProvider.extract_text_from_file`. If this fails, it falls back to `pdfplumber`.\n\n**Security Enhancements:**\n* **Redirects:** `_download_file` explicitly refuses redirects to prevent token leakage via re-direction.\n* **Secrets:** The `auth_token` is passed as a `secret` to `egress_request`, preventing it from appearing in logs or error strings.\n* **Byte Cap:** The response content is capped to prevent memory exhaustion from hostile endpoints.\n\n### PageIndex Integration\nThe `_build_page_index` function converts extracted text into a structured format.\n1. Writes text to a temporary Markdown file.\n2. Initializes `PageIndexClient` using the `VERTEX_MODEL`.\n3. Retrieves the document structure.\n4. **Fallback:** If PageIndex fails, it returns a single flat node containing the first 8000 characters of the text to ensure the document remains searchable.\n\n### Knowledge Graph Upsert\nThe `_upsert_knowledge_nodes` function POSTs section nodes to the backend API (`/api/startup/knowledge/graph/node`).\n\n**Payload Structure:**\n```json\n{\n \"kind\": \"document_section\",\n \"externalId\": \"doc-{doc_id}-sec-{order}\",\n \"internalId\": <int: document_id>,\n \"name\": \"<section title>\",\n \"summary\": \"<summary>\",\n \"accessTier\": \"A\",\n \"metadata\": {\n \"level\": <int>,\n \"order\": <int>,\n \"content_preview\": \"<first 400 chars>\",\n \"filename\": \"<original filename>\"\n }\n}\n```\n*Note: `internalId` is sent as an integer. Sending a stringified ID previously caused 500 errors.*\n\n## Security & Configuration\n\n### Environment Variables\nThe module relies on the following configuration:\n* `HARAKAT_API_URL`: Base URL for the backend API.\n* `VERTEX_SERVICE_ACCOUNT_JSON`: Path to the Google Cloud service account JSON.\n* `VERTEX_MODEL`: The model identifier for Vertex AI (e.g., `gemini-1.5-pro`).\n* `PAGEINDEX_MODEL`: The model identifier for PageIndex (defaults to `vertex_ai/{VERTEX_MODEL}`).\n\n### Egress Policy\nAll external requests are guarded by `core.security.egress.policies.document_download_policy`. This ensures that URLs are validated before the worker attempts to fetch them.\n\n## Error Handling\nThe module follows a \"fail-soft\" strategy.\n* **Exceptions:** Caught at the `index_document` level.\n* **Logging:** Errors are logged with `logger.error` and `logger.warning` depending on severity.\n* **Queue Status:** If a `queue_id` is present, the status is updated to `\"failed\"` with the error message.\n* **Result:** Returns a dictionary containing `document_id` and either `sections_indexed` or `error`.\n\n## Dependencies\n* **Internal:** `core.config`, `core.security.egress`, `providers.vertex_ai`.\n* **External:** `httpx`, `pdfplumber`, `docx`, `openpyxl`.\n* **Sub-repo:** `PageIndex` (imported dynamically via `sys.path` modification).","harakat-ai-workflows":"# Harakat_AI — workflows\n\n\n\n# Harakat_AI — Workflows Module\n\n## Overview\n\nThe `workflows` module provides FastAPI routers for all AI service endpoints. It organizes the AI service's HTTP surface into discrete, purpose-built routers that handle document processing, assistant interactions, embeddings, and governance operations.\n\nThe module exports 9 active routers:\n\n```\nworkflows/\n├── __init__.py # Exports all routers\n├── assistant_workflow.py # Virtual assistant (Hermes-native)\n├── browser_workflow.py # Browser agent for meetings\n├── clean_workflow.py # Deterministic text cleaning\n├── context_workflow.py # Context gate (usage event judge)\n├── embed_workflow.py # Vector embedding generation\n├── enrichment_workflow.py # Gemini structured output\n├── feed_proposals.py # Stateless autoload proposals\n├── governance_draft.py # Governance artifact drafting\n├── hermes_debug_workflow.py # Debug-only Hermes endpoint\n├── indexing_workflow.py # Document indexing queue\n├── meeting_workflow.py # Meeting operations\n├── multimodal_description.py # Document visual descriptions\n└── parse_workflow.py # Document parsing\n```\n\n## Architecture\n\n### Router Mounting\n\nAll routers are mounted under their respective prefixes in the main application. The `__init__.py` exports them as named routers for programmatic mounting:\n\n```python\nfrom .assistant_workflow import router as assistant_router\nfrom .indexing_workflow import router as indexing_router\nfrom .parse_workflow import router as parse_router\nfrom .clean_workflow import router as clean_router\nfrom .multimodal_description import router as multimodal_router\nfrom .enrichment_workflow import router as enrichment_router\nfrom .embed_workflow import router as embed_router\nfrom .context_workflow import router as context_router\n```\n\n### Execution Flow Pattern\n\nMost endpoints follow one of two patterns:\n\n1. **Stateless Provider-Direct**: Single LLM call via `get_provider().achat()` with no agent graph, no tools, no session. Used for lightweight operations like memory extraction, summarization, and governance drafting.\n\n2. **Hermes-Native**: Full runtime via `agents.hermes_native.aia_runner` with tool registry, session context, and approval gating. Used for the main assistant interaction.\n\n```mermaid\nflowchart TD\n A[Request] --> B{Pattern?}\n B -->|Stateless| C[get_provider().achat]\n B -->|Hermes| D[build_session_context]\n D --> E[retrieve_internal_state]\n E --> F[derive_tools_from_vessel]\n F --> G[stream_native_aia_agent_events]\n C --> H[Parse JSON response]\n G --> I[Translate to SSE]\n H --> J[Return Response]\n I --> J\n```\n\n## Key Routers\n\n### Assistant Workflow (`assistant_workflow.py`)\n\nThe primary assistant interaction router. All assistant endpoints live under `/assistant`.\n\n#### Endpoints\n\n| Method | Path | Description |\n|--------|------|-------------|\n| POST | `/assistant/run` | Synchronous assistant execution |\n| POST | `/assistant/stream` | SSE streaming for real-time updates |\n| POST | `/assistant/extract-memories` | Lightweight memory extraction (no graph) |\n| POST | `/assistant/compress-day` | Daily conversation compression |\n| POST | `/assistant/summarize-conversation` | Structured conversation lifecycle summary |\n| POST | `/assistant/checkpoint-summarize` | Hermes ContextCompressor summary |\n| POST | `/assistant/note-title` | Short title generation |\n| POST | `/assistant/draft-governance-artifact` | Governance artifact drafting |\n| POST | `/assistant/draft-onboarding-governance` | Onboarding governance drafting |\n| POST | `/assistant/propose-autoload` | Grounded autoload proposals |\n| POST | `/assistant/drive-procedure-run` | Headless procedure execution |\n| POST | `/assistant/transcribe` | Audio transcription |\n| GET | `/assistant/tool-manifest` | Tool registry manifest |\n\n#### Hermes Session Construction\n\nThe `AssistantRequest` body is transformed into a Hermes `SessionContext` via `_build_hermes_session()`:\n\n```python\nsession = build_session_context(body)\nmemory = AssistantMemoryStore(\n startup_id=body.startup_id,\n assistant_id=body.assistant_id,\n user_id=body.user_id,\n user_tier=body.access_tier or \"Z\",\n branch_id=body.branch_id,\n jwt=body.auth_token,\n)\n```\n\nThe session is then enriched with:\n- **Internal state**: `retrieve_internal_state()` returns tier/branch-filtered context\n- **Tool registry**: `derive_tools_from_vessel()` builds available tools from the vessel\n- **Grant context**: `narrate_for_model()` adds grant capability notes to the system prompt\n\n#### Approved Action Execution\n\nWhen a user approves an action preview, the request arrives with `__ACTION_APPROVED__:` prefix. The `_hermes_run_approved_action()` function:\n\n1. Parses the approval payload (preferring server-validated `approved_action` over client sentinels)\n2. Builds a tool bridge via `build_tool_registry(session)`\n3. Executes via `governed_call()` if `procedure_run_id` is present, otherwise via `bridge.executor()`\n4. For procedure-bound approvals, calls `resume_after_approval()` to continue the paused run\n\n```mermaid\nsequenceDiagram\n participant Client\n participant Assistant\n participant Hermes\n participant ToolBridge\n \n Client->>Assistant: POST /assistant/run with __ACTION_APPROVED__\n Assistant->>Hermes: _hermes_run_approved_action\n Hermes->>ToolBridge: build_tool_registry\n ToolBridge->>Hermes: governed_call / bridge.executor\n ToolBridge-->>Hermes: result\n Hermes-->>Assistant: SSE events\n Assistant-->>Client: StreamingResponse\n```\n\n#### Streaming Endpoint\n\nThe `/assistant/stream` endpoint uses `_hermes_event_generator()` which:\n\n1. Detects action approval sentinels and routes to `_hermes_run_approved_action()`\n2. Detects `/search` commands and routes to `stream_search_command_events()`\n3. Otherwise calls `stream_native_aia_agent_events()`\n4. Translates Hermes events to legacy SSE format via `_hermes_event_to_legacy_sse()`\n\n### Clean Workflow (`clean_workflow.py`)\n\nDeterministic text cleaning with **no LLM**. Applied to `ParsedDocument.content` before retrieval.\n\n#### The 9 Cleaning Rules\n\nApplied in order to each page's text:\n\n1. **NFKC normalization** - Unicode normalization\n2. **Control character strip** - NUL + control chars (preserving `\\t`, `\\n`, `\\r`)\n3. **Whitespace normalization** - Collapse multiple spaces/newlines\n4. **Header/footer detection** - Lines repeating across ≥30% of pages dropped\n5. **Hyphenation rejoin** - `word-\\nbreak` → `wordbreak`\n6. **Adjacent paragraph dedup** - Exact repeats within 3-paragraph window dropped\n7. **Drive-export tail strip** - Google Docs export footers removed\n8. **MIME-specific prefix collapse** - Excel/PowerPoint prefixes removed\n9. **Length floor** - `< 50 chars` → `needsMultimodalRescue = true`\n\n#### Implementation\n\n```python\ndef clean_parsed_content(raw_content: dict[str, Any], mime_type: str) -> CleanResponse:\n # 1. NFKC\n text = _nfkc(text)\n # 2. Control chars\n text = _strip_controls(text)\n # 3. Whitespace\n text = _normalize_whitespace(text)\n # 4. Header/footer\n text, dropped = _drop_lines(text, repeated)\n # 5. Hyphenation\n text = _rejoin_hyphenation(text)\n # 6. Dedup\n text = _dedup_adjacent_paragraphs(text)\n # 7. Drive tail\n text = _strip_drive_tail(text)\n # 8. MIME rules\n text = _apply_mime_rules(text, mime_type)\n```\n\n### Embed Workflow (`embed_workflow.py`)\n\nInternal endpoint for vector embedding generation. Called by `HarakatOffices_Server` which owns Qdrant.\n\n#### Contract\n\n- **`dim` parameter**: The vector space width. Must match Qdrant collection dimension exactly.\n- **`taskType`**: Either `RETRIEVAL_DOCUMENT` (ingest) or `RETRIEVAL_QUERY` (search)\n- **Max batch**: 512 texts per request\n\n#### Error Handling\n\n| Error | Status | Cause |\n|-------|--------|-------|\n| `AmbiguousSpace` | 500 | Deployment configured with multiple geometries for one width |\n| `UnsupportedDimension` | 422 | Requested dimension not served by this deployment |\n| Provider error | 502 | Embedding provider failure |\n\n```python\nspace = resolve_space(req.dim)\nvectors = await embed_texts(req.texts, task_type=req.taskType, dim=req.dim)\n```\n\n### Enrichment Workflow (`enrichment_workflow.py`)\n\nFour Gemini-backed endpoints for structured output. All use the `_gemini_json()` helper which:\n\n1. Calls `utility_text()` via `core.utility_llm`\n2. Strips markdown fences\n3. Parses JSON with fallback to empty dict on failure\n4. Logs safety filter blocks as warnings (not errors)\n\n#### Endpoints\n\n| Endpoint | Purpose |\n|----------|---------|\n| `/parse/abstract` | One-paragraph summary + topic tags + key entities |\n| `/parse/folder-rollup` | Synthesized rollup from child summaries |\n| `/parse/tenant-toc` | Tenant-wide digest + folder structure |\n| `/parse/entities` | Entities + relations extraction |\n\n### Context Workflow (`context_workflow.py`)\n\nThe server's converge pass endpoint. Judges whether a usage event may enter a node's learned vocabulary.\n\n```python\nresult = await judge_proposal(\n event=body.event,\n node_name=body.node_name,\n node_path=body.node_path,\n search_card=body.search_card,\n current_context=body.current_context,\n)\n```\n\nReturns `verdict: \"allow\" | \"block\"` with optional `block` payload and `reason`.\n\n### Feed Proposals (`feed_proposals.py`)\n\nStateless autoload proposals for onboarding. Every proposal must cite:\n- The owner's goals\n- A synced document title\n- The company's vertical\n\nGeneric advice without grounding is dropped.\n\n### Governance Draft (`governance_draft.py`)\n\nDrafts illustrative governance artifacts (skill/policy/procedure) with:\n- `system_prompt_fragment` - Instruction block for the artifact\n- `content` - Plain description\n- `steps` - For procedures, canonical step objects\n- `friendly_tools` - Human-readable tool labels (raw keys never exposed)\n\n### Browser Workflow (`browser_workflow.py`)\n\nBrowser agent for Google Meet sessions.\n\n#### Endpoints\n\n| Method | Path | Description |\n|--------|------|-------------|\n| POST | `/browser/join-meeting` | Start a meeting session (SSE) |\n| GET | `/browser/session/{id}` | Resume SSE stream |\n| POST | `/browser/session/{id}/stop` | Stop a running session |\n\nThe `_session_event_generator()` yields:\n- `browser_session_start` - Initial session info\n- `browser_step` - Live screenshots/status updates\n- `heartbeat` - Keepalive every 120s timeout\n\n### Hermes Debug Workflow (`hermes_debug_workflow.py`)\n\nDebug-only endpoint for Phase 1 validation. Runs Hermes without tenant tools, memory, or SSE.\n\n#### Key Features\n\n- **Read-only toolset**: `DEBUG_READ_ONLY_TOOLS` list (search, list, read operations only)\n- **JWT required** when `enable_harakat_tools=true`\n- **Memoryless**: `skip_memory=True`, `persist_session=False`\n- **Max 3 iterations** via `IterationBudget(3)`\n\n### Indexing Workflow (`indexing_workflow.py`)\n\nBackground document indexing.\n\n#### Security\n\nValidates egress URLs via `assert_egress_url_at_rest()` before enqueueing:\n\n```python\nfile_url = assert_egress_url_at_rest(body.file_url, document_download_policy(), \"file_url\")\n```\n\nEnqueues via `BackgroundTasks.add_task(index_document, job)`.\n\n## Integration Points\n\n### Core Modules\n\n| Module | Usage |\n|--------|-------|\n| `core.grant` | `narrate_for_model()` for grant context |\n| `core.tool_risk` | `build_tool_manifest()` for tool registry |\n| `core.tracing` | `langwatch_trace()` for distributed tracing |\n| `core.posthog_tracker` | `capture_agent_run()` for analytics |\n| `core.embeddings` | `embed_texts()` for vector generation |\n| `core.embed_space` | `resolve_space()`, `active_dim()` for embedding config |\n| `core.context_gate` | `judge_proposal()` for usage event gating |\n| `core.browser_agent` | `get_browser_agent_manager()` for browser sessions |\n\n### Agents Module\n\n| Module | Usage |\n|--------|-------------|\n| `agents.hermes_native.aia_runner` | `stream_native_aia_agent_events()` |\n| `agents.hermes_native.tool_bridge` | `build_tool_registry()`, `governed_call()` |\n| `agents.hermes_native.procedure_executor` | `drive_run()`, `resume_after_approval()` |\n| `agents.hermes_native.search_command` | `stream_search_command_events()` |\n| `agents.hermes_native.request_context` | `build_session_context()` |\n| `agents.hermes_native.memory_store` | `AssistantMemoryStore` |\n\n### Providers Module\n\n| Module | Usage |\n|--------|-------------|\n| `providers` | `get_provider()` for LLM abstraction |\n| `providers.base` | `ChatMessage` for message formatting |\n| `core.utility_llm` | `utility_text()` for Gemini calls |\n\n## Best Practices\n\n### 1. Stateless Endpoints\n\nFor lightweight operations, avoid the agent graph entirely:\n\n```python\n# ✅ Good: Stateless, no graph, no tools\nasync def extract_memories(body: MemoryExtractRequest):\n provider = get_provider()\n resp = await provider.achat([ChatMessage(role=\"user\", content=prompt)])\n # Parse and return\n\n# ❌ Bad: Spins full graph for one-shot classification\nasync def extract_memories(body: MemoryExtractRequest):\n # POST to /assistant/run and spin LangGraph\n```\n\n### 2. Hermes Session Construction\n\nUse `_build_hermes_session()` as the single source of truth:\n\n```python\n# ✅ Good: One constructor\nsession = build_session_context(body)\nmemory = AssistantMemoryStore(...)\n\n# ❌ Bad: Duplicate construction\nsession1 = SessionContext(...)\nsession2 = SessionContext(...)\n```\n\n### 3. Error Handling\n\nStateless endpoints should never fail the caller:\n\n```python\ntry:\n provider = get_provider()\n resp = await provider.achat(...)\nexcept Exception:\n # Return empty-but-valid shape\n return EmptyResponse()\n```\n\n### 4. Tool Key Exposure\n\nRaw tool keys must never leave the AI service:\n\n```python\n# ✅ Good: Friendly labels only\nfriendly_tools = _shape_friendly_tools(suggested_keys, tool_blurbs, label_by_key)\n\n# ❌ Bad: Raw keys in response\nreturn {\"tools\": suggested_keys}\n```\n\n## Testing\n\nKey test files:\n- `tests/test_parse_workflow_docx.py` - Docx parsing validation\n- `scripts/selfhosted_pipeline_selftest.py` - Full pipeline selftest\n\n## Migration Notes\n\n### Phase 8 Cutover\n\nThe legacy LangGraph tree (`agents/{assistant,resolver,composer,base,hermes_runtime}` and `core/graph_builder.py`) is **deleted**. The `/assistant/stream` endpoint now uses Hermes-only routing.\n\n**Rollback**: If Hermes hits a regression, switch `/assistant/stream` back to the previous branch.\n\n### Phase 6 Tool Gateway\n\nApproved actions now prefer server-validated `approved_action` payload over client sentinels. The `__ACTION_APPROVED__:` prefix is still supported for legacy clients but is not server-validated.\n\n### Phase 7 Procedure Bound Approvals\n\nProcedure-bound approvals execute via `governed_call()` so the ToolExecution ledger row carries the procedure binding.\n\n## Troubleshooting\n\n### Hermes Debug Endpoint\n\nIf `GEMINI_API_KEY` is not configured, the debug endpoint returns 503:\n\n```bash\nexport GEMINI_API_KEY=your_key\nexport GEMINI_BASE_URL=https://generativelanguage.googleapis.com\nexport HERMES_MODEL=gemini-3.5-flash\n```\n\n### Embedding Dimension Mismatch\n\nIf `dim` is omitted, the endpoint returns the deployment's configured space. If the caller needs a specific dimension, ensure it matches the Qdrant collection exactly.\n\n### Safety Filter Blocks\n\nGemini safety filters may block responses containing real-world tenant data. The `_gemini_json()` helper absorbs these as empty results so the pipeline continues.\n\n## API Reference\n\n### Request Models\n\n```python\nclass AssistantRequest(BaseModel):\n task: str\n history: Optional[list[dict]] = None\n assistant_name: Optional[str] = None\n assistant_description: Optional[str] = None\n assistant_id: Optional[int] = None\n auth_token: Optional[str] = None\n delegation_token: Optional[str] = None\n delegation: Optional[dict] = None\n room_id: Optional[str] = None\n creation_kind: Optional[str] = None\n creation_scope_hint: Optional[str] = None\n creation_scope: Optional[dict] = None\n surface: Optional[str] = None\n page_context: Optional[dict] = None\n memories_context: Optional[str] = None\n access_tier: Optional[str] = None\n user_role: Optional[str] = None\n branch_id: Optional[int] = None\n startup_id: Optional[int] = None\n user_id: Optional[int] = None\n conversation_id: Optional[int] = None\n run_id: Optional[str] = None\n system_prompt_override: Optional[str] = None\n suppress_action_previews: Optional[bool] = False\n approved_action: Optional[dict] = None\n```\n\n### Response Models\n\n```python\nclass AssistantResponse(BaseModel):\n output: str\n intent: Optional[str] = None\n thinking_steps: Optional[list[str]] = None\n action_result: Optional[dict] = None\n```\n\n## Version History\n\n- **2026-08-06**: Rescued from `pipeline-defects` with server half\n- **Phase 8**: Hermes-only assistant routing (LangGraph deleted)\n- **Phase 7**: Procedure-bound approvals via `governed_call()`\n- **Phase 6**: Server-validated approved actions\n- **Phase 5**: Retrieval v2 routers under `/parse` prefix","harakat-ai":"# Harakat_AI\n\n\n\n# Harakat_AI\n\n## Overview\nThe `Harakat_AI` module group serves as the central orchestration layer for the AI runtime, managing task execution, agent logic, knowledge ingestion, and infrastructure deployment. It bridges high-level API requests with specialized agent workspaces, LLM inference backends, and data processing pipelines.\n\n## Architecture\n\n### Runtime Execution\nThe execution engine resides in the `src` module, exposing a REST API that routes incoming tasks to specific agent workspaces via `WorkspaceManager`. This layer manages the lifecycle of task execution, from request handling to completion reporting.\n\n### Agent & Tooling\nAgents interact with the system through standardized data contracts defined in `envelopes`, ensuring disparate outputs can be stitched together by the composer and planner. For external tooling, the `mcp_client` module provides a secure proxy interface for Model Context Protocol (MCP) tools, handling authentication and audit logging without exposing credentials to the agent process.\n\n### Knowledge & Data\nData ingestion is handled by the `workers/document_indexer`, which processes files and structures content for the Knowledge Graph. Heavy extraction tasks are offloaded to the `rust-ingest` subprocess for memory efficiency. The `capabilities` module manages the lifecycle of learned skills, while the `providers` module abstracts LLM inference backends, allowing runtime swapping via environment configuration.\n\n### Infrastructure & Documentation\nDeployment is managed through `kubernetes` manifests, ensuring consistent infrastructure across environments. Developer guidance is provided in the `docs` module, covering agent development workflows and system setup.\n\n## Integration Flow\n\n1. **Task Submission:** Requests enter via the `src` runtime API.\n2. **Dispatch:** The runtime routes tasks to `agents` based on workspace requirements.\n3. **Inference & Tools:** Agents utilize `providers` for LLM calls and `mcp_client` for external tool access.\n4. **Data Processing:** Background `workers` ingest and structure data, potentially using `rust-ingest` for parsing.\n5. **Standardization:** All agent outputs conform to `envelopes` for unified consumption.\n\n```mermaid\ngraph TD\n API[Runtime API (src)] -->|Routes Task| Runtime[Workspace Manager]\n Runtime -->|Dispatches| Agents[Agent Logic]\n Agents -->|Inference| Providers[LLM Providers]\n Agents -->|Tools| MCP[mcp_client Proxy]\n Agents -->|Data| Envelopes[Result Envelopes]\n Workers[Document Indexer] -->|Ingests| Rust[rust-ingest]\n Workers -->|Stores| KG[Knowledge Graph]\n Infrastructure[Kubernetes] -->|Deploys| All[All Components]\n```\n\n## Sub-Modules\n\n* [src](src.md): Runtime execution engine and API layer.\n* [providers](providers.md): Pluggable LLM backend interface.\n* [workers](workers.md): Background document indexing and ingestion.\n* [mcp_client](mcp_client.md): Secure MCP tool invocation.\n* [envelopes](envelopes.md): Standardized data contracts for agent communication.\n* [capabilities](capabilities.md): Lifecycle management for learned skills.\n* [kubernetes](kubernetes.md): Infrastructure-as-Code manifests.\n* [rust-ingest](rust-ingest.md): High-performance document extraction.\n* [docs](docs.md): Developer reference and guides.\n* [agents](agents.md): Agent logic and workflows.\n* [bench](bench.md): Benchmarking utilities.\n* [core](core.md): Core system logic.\n* [scripts](scripts.md): Utility scripts.\n* [tests](tests.md): Test suites.\n* [workflows](workflows.md): Workflow definitions.\n* [erp-mcp](erp-mcp.md): ERP-specific MCP integration.\n* [erp-skills](erp-skills.md): ERP-specific skills.","harakat-kernel-apps":"# Harakat_Kernel — apps\n\n\n\n# Harakat_Kernel — apps/accounts\n\n## Overview\n\nThe `accounts` module implements the double-entry ledger foundation for Harakat_Kernel, providing the core accounting primitives: `account.move`/`line`, chart of accounts, journals, tax, and currency. This documentation focuses on the **allocation engine** (P3), which handles money-splitting across funds, cost centers, and projects.\n\nThe allocation engine transforms spreadsheet-based distribution logic into a typed, auditable, and reversible system where rules are records, splits are arithmetic the kernel performs, and results are posted, reversible, and auditable entries.\n\n## Architecture\n\n```mermaid\ngraph TD\n A[AllocationRule] --> B[AllocationRuleLine]\n A --> C[AllocationRun]\n D[Fund] --> C\n E[MembershipBasis] --> C\n C --> F[account.move]\n F --> G[account.move.line]\n H[MembershipSnapshot] --> C\n```\n\n## Core Models\n\n### Fund (`account.fund`)\n\nA pot of money that postings can be attributed to. The `dimension` discriminator allows the same model to serve funds, statutory channels, cost centers, and projects.\n\n```rust\npub struct Fund {\n pub name: String,\n pub code: Option<String>,\n pub dimension: String, // \"fund\", \"channel\", \"cost_center\", \"project\"\n pub restricted: bool, // Money may flow IN freely; paying OUT needs signature\n pub parent_id: Option<Uuid>,\n pub active: bool,\n}\n```\n\n**Key Invariants:**\n- `restricted` is a **lifecycle flag**, not an ordinary boolean. Lifting restriction requires a passkey-attested `unrestrict` action.\n- Marking a pot restricted is an ordinary edit; clearing it is a signed decision.\n\n**Trial Balance Report:**\n```rust\npub const TRIAL_BALANCE: &str = \"trial_balance\";\n```\n\nThe `fund_trial_balance` function reads `account.move.line`, `account.move`, and `account.fund` to produce a grouped statement showing debit/credit totals per fund, with separate rollups for restricted/unrestricted/unattributed amounts.\n\n### AllocationRule (`allocation.rule`)\n\nThe written rule for splitting a pot. Frozen once it has posted a run.\n\n```rust\npub struct AllocationRule {\n pub name: String,\n pub state: String, // \"draft\", \"active\", \"archived\"\n pub dimension: String,\n pub basis: String, // \"net_revenue\", \"gross\", \"fixed_pool\", \"dynamic_membership\"\n pub source_fund_id: Option<Uuid>,\n pub source_account_id: Option<Uuid>,\n pub rounding_policy: String,\n pub requires_authorization: bool,\n pub date_from: Option<chrono::NaiveDate>,\n pub date_to: Option<chrono::NaiveDate>,\n pub version: i64,\n pub supersedes_id: Option<Uuid>,\n // P3.6: Membership basis fields (nullable)\n pub membership_model: Option<String>,\n pub membership_mode: Option<String>,\n pub membership_unit: Option<String>,\n pub membership_state_field: Option<String>,\n pub membership_state_value: Option<String>,\n pub membership_size_field: Option<String>,\n pub membership_layer_field: Option<String>,\n pub membership_layer_weights: Option<serde_json::Value>,\n pub membership_partner_field: Option<String>,\n pub membership_fund_id: Option<Uuid>,\n pub membership_from_field: Option<String>,\n pub membership_to_field: Option<String>,\n}\n```\n\n**Lifecycle Actions:**\n- `activate`: Draft → Active\n- `archive`: Draft/Active → Archived\n- `waive_authorization`: Requires passkey attestation (P3.6)\n\n**Freeze Behavior:**\nA rule that has posted a run is frozen. Editing it would make past distributions irreproducible. Changes require a new version that `supersedes` the old one.\n\n**Authorization Gate:**\n```rust\nfn assert_rule_authorization_not_dropped(record: &ValueMap, values: &ValueMap) -> Result<(), KernelError>\n```\n\nClearing `requires_authorization` is not an ordinary edit—it goes through the `waive_authorization` action.\n\n### AllocationRuleLine (`allocation.rule.line`)\n\nOne claim on the pool. Two kinds: **fixed** (absolute amount) and **ratio** (weight).\n\n```rust\npub struct AllocationRuleLine {\n pub rule_id: Uuid,\n pub sequence: i64,\n pub line_type: String, // \"ratio\" or \"fixed\"\n pub ratio: Option<Decimal>,\n pub amount: Option<Decimal>,\n pub target_fund_id: Uuid,\n pub target_account_id: Option<Uuid>,\n pub priority: i64,\n pub is_fallback: bool, // Absorbs rounding leftover\n pub partner_id: Option<Uuid>,\n pub min_amount: Option<Decimal>, // Declared, not yet enforced\n pub max_amount: Option<Decimal>, // Declared, not yet enforced\n}\n```\n\n**Ordering:** Claims are satisfied in ascending priority, then ascending sequence. Sequence alone remains the deterministic tie-break for remainder assignment.\n\n### AllocationRun (`allocation.run`)\n\nOne execution of one rule against one source. Idempotent by construction.\n\n```rust\npub struct AllocationRun {\n pub rule_id: Uuid,\n pub rule_version: i64,\n pub source_ref: String, // e.g., \"2026-Q3\"\n pub run_key: String, // \"{rule_id}:{source_ref}\" — UNIQUE per tenant\n pub pool_amount: Option<Decimal>,\n pub move_id: Option<Uuid>,\n pub reversal_move_id: Option<Uuid>,\n pub run_date: Option<chrono::NaiveDate>,\n pub membership_snapshot: Option<serde_json::Value>,\n pub membership_digest: Option<String>,\n}\n```\n\n**Idempotency:** The `run_key` is a per-tenant UNIQUE column derived from `rule_id` and `source_ref`. A second run for the same source is refused before it can post.\n\n**Reversal:** `reverse` posts a mirrored contra-entry rather than editing history (RFC 008).\n\n## Allocation Workflow\n\n### 1. Rule Authoring\n\n```mermaid\ngraph LR\n A[Create Rule] --> B{Has Membership Basis?}\n B -->|Yes| C[Set basis = dynamic_membership]\n B -->|No| D[Set basis = fixed_pool]\n C --> E[Define Lines]\n D --> E\n E --> F{Requires Authorization?}\n F -->|Yes| G[Set requires_authorization = true]\n F -->|No| H[Leave false]\n G --> I[Activate Rule]\n H --> I\n I --> J[Run Distribution]\n```\n\n### 2. Distribution Execution\n\n```mermaid\ngraph TD\n A[run_distribution] --> B{Already Posted?}\n B -->|Yes| C[Refuse: money moved]\n B -->|No| D{Rule Active?}\n D -->|No| E[Refuse: rule not active]\n D -->|Yes| F{In Date Range?}\n F -->|No| G[Refuse: outside effective dates]\n F -->|Yes| H{Pool > 0?}\n H -->|No| I[Refuse: no money to split]\n H -->|Yes| J{Authorization Gate}\n J -->|Restricted or requires_auth| K{Passkey Signed?}\n K -->|No| L[Refuse: needs signature]\n K -->|Yes| M[Proceed]\n J -->|No| M\n M --> N{Membership Basis?}\n N -->|Yes| O[Resolve Membership]\n N -->|No| P[Use Stored Lines]\n O --> Q[Generate Claims]\n P --> Q\n Q --> R[Split Pool]\n R --> S[Post Entry]\n S --> T[Stamp Run]\n T --> U[Return Effects]\n```\n\n### 3. Authorization Gate\n\nThe authorization gate is enforced at the app layer, not the posting guard:\n\n```rust\nlet source_restricted = matches!(source_fund_row.get(\"restricted\"), Some(Value::Bool(true)));\nlet requires_authorization = matches!(rule.get(\"requires_authorization\"), Some(Value::Bool(true)));\nif (source_restricted || requires_authorization) && !passkey_signed(args) {\n return Err(KernelError::invalid(format!(\n \"cannot run '{rule_name}': {why} — disbursing out of it needs an authorised signature\"\n )));\n}\n```\n\n**Passkey Verification:**\n```rust\nfn passkey_signed(args: &serde_json::Map<String, Value>) -> bool {\n args.get(kernel_core::mutation::VERIFIED_APPROVAL_ARG)\n .and_then(|v| v.as_str())\n == Some(\"passkey_attestation\")\n}\n```\n\nThe `VERIFIED_APPROVAL_ARG` is injected by the API layer only after `check_approval` and attestation binding have both passed.\n\n### 4. Reversal\n\n```mermaid\ngraph LR\n A[reverse_distribution] --> B{Already Reversed?}\n B -->|Yes| C[Refuse: already undone]\n B -->|No| D{Would Debit Restricted?}\n D -->|Yes| E{Passkey Signed?}\n E -->|No| F[Refuse: needs signature]\n E -->|Yes| G[Post Contra Entry]\n D -->|No| G\n G --> H[Stamp Reversal]\n H --> I[Return Effects]\n```\n\nA reversal needs a signature when:\n- The rule sets `requires_authorization`, OR\n- The rule's source pot is restricted, OR\n- The original entry CREDITED a restricted pot (contra will debit it)\n\n## Membership Basis (P3.6)\n\nThe membership basis allows rules to derive weights from live counts of eligible records instead of carrying typed-in ones.\n\n### Declaration Fields\n\n```rust\nconst MEMBERSHIP_FIELDS: [&str; 13] = [\n \"name\",\n \"membership_model\",\n \"membership_mode\",\n \"membership_unit\",\n \"membership_state_field\",\n \"membership_state_value\",\n \"membership_size_field\",\n \"membership_layer_field\",\n \"membership_layer_weights\",\n \"membership_partner_field\",\n \"membership_fund_id\",\n \"membership_from_field\",\n \"membership_to_field\",\n];\n```\n\n### Modes\n\n- **Per capita (حشري):** Every eligible person draws one share\n- **By layer (طبقي):** Layers are apportioned first, then shared inside each layer\n\n### Preview Report\n\n```rust\nfn membership_preview(ctx: &ReadContext, params: &ValueMap, source: Option<&str>) -> Result<Value, KernelError>\n```\n\nThe preview shows who would share and what they'd get. It is a READ, not an action, to avoid locking the record and emitting outbox events for a preview.\n\n**Safety:** The preview shows only the records visible to the caller. The run recomputes the digest over the full register and refuses on any difference.\n\n## Guarding the Guards (P3.6)\n\nThe two switches that decide whether a distribution needs a signature—`restricted` on funds and `requires_authorization` on rules—were themselves ordinary writable booleans. P3.6 makes loosening either a declared, passkey-attested ACTION.\n\n### Asymmetry\n\n| Operation | Action Required |\n|-----------|----------------|\n| Mark fund restricted | Ordinary edit |\n| Lift fund restriction | `unrestrict` action (passkey) |\n| Set `requires_authorization` | Ordinary edit |\n| Clear `requires_authorization` | `waive_authorization` action (passkey) |\n\n**Rationale:** Tightening controls is low-risk (worst case: signature nobody needed). Loosening controls is high-risk (opens the way to unsigned disbursements).\n\n### Implementation\n\n```rust\nfn assert_fund_restriction_not_dropped(record: &ValueMap, values: &ValueMap) -> Result<(), KernelError> {\n let was = matches!(record.get(\"restricted\"), Some(Value::Bool(true)));\n let dropping = matches!(values.get(\"restricted\"), Some(Value::Bool(false)) | Some(Value::Null));\n if !was || !dropping {\n return Ok(());\n }\n Err(KernelError::invalid(format!(\n \"'{name}' is a restricted fund, and lifting that restriction is not an ordinary edit\"\n )))\n}\n```\n\n## Posting Shape\n\nA run produces one balanced journal entry:\n\n```\nDr Fund Balances 8,000.00 [F0]\n Cr Fund Balances 800.00 [F-RES]\n Cr Fund Balances 400.00 [F-NAZ]\n Cr Fund Balances 1,200.00 [F-DEV] Σ 8,000 = 8,000 ✓\n```\n\nThe source pot is DEBITED for the whole pool; each target pot is CREDITED its share, on the net-asset (\"Fund Balances\") account.\n\n## Dependencies\n\n| Dependency | Purpose |\n|------------|---------|\n| `kernel-core` | Model handling, error types, value types |\n| `kernel-macros` | Model definition macros |\n| `chrono` | Date handling |\n| `uuid` | Record identifiers |\n| `rust_decimal` | Exact decimal arithmetic |\n| `serde_json` | JSON serialization |\n| `sha2` | Membership digest computation |\n| `entities` | Dev-only: membership basis tests |\n| `project` | Dev-only: project dimension tests |\n\n## Key Functions\n\n### `run_distribution`\n\nExecutes a distribution and posts the entry. All refusals happen before anything is written.\n\n```rust\nfn run_distribution(\n ctx: &LogicCtx,\n record: &ValueMap,\n related: &Related,\n args: &serde_json::Map<String, Value>,\n) -> Result<ActionEffects, KernelError>\n```\n\n### `reverse_distribution`\n\nUndoes a distribution by posting a mirrored contra-entry.\n\n```rust\nfn reverse_distribution(\n ctx: &LogicCtx,\n record: &ValueMap,\n related: &Related,\n args: &serde_json::Map<String, Value>,\n) -> Result<ActionEffects, KernelError>\n```\n\n### `fund_trial_balance`\n\nBuilds the fund trial balance report.\n\n```rust\nfn fund_trial_balance(\n ctx: &ReadContext,\n params: &ValueMap,\n) -> Result<Value, KernelError>\n```\n\n### `distribute`\n\nSplits a pool among claims exactly.\n\n```rust\nfn distribute(pool: Decimal, claims: &[Claim]) -> Result<Vec<Decimal>, KernelError>\n```\n\n## Integration Points\n\n### With `account.move`\n\nThe allocation engine posts entries via `post_move`. The `account.move.post` guard reads the `allocation_run` stamp to know the signature was taken at the run level.\n\n### With `account.fund`\n\nThe `fund_id` columns on `account.move[_line]` are present but unread until P3. The allocation engine is the first to use them.\n\n### With `kernel-api`\n\nThe API layer injects the `VERIFIED_APPROVAL_ARG` for passkey-attested actions. The allocation engine reads this to decide whether to proceed.\n\n### With `kernel-storage`\n\nThe `run_key` unique index is enforced at the storage layer. The engine derives it at create time to ensure idempotency.\n\n## Testing\n\nKey test files:\n- `tests/fund_trial_balance.rs` — Trial balance report\n- `tests/allocation_gate_api.rs` — Authorization gates\n- `tests/job_totals.rs` — Job dimension tests\n\n## Design Notes\n\n### Why Not Odoo's `analytic_distribution`\n\nOdoo attributes analytics through a JSON widget on the line. A JSON blob cannot be indexed, cannot carry referential integrity, and cannot be required at post. Fund accounting needs exactly those three. A real typed `fund_id` column with `on_delete = \"restrict\"` gives all three.\n\n### Why Freeze After Posting\n\nA rule that has posted a run is history: money moved on the split it described. Editing it would leave the books saying one thing and the rule saying another, and would make a past run irreproducible. The design's answer is versioning: create a NEW rule that `supersedes` the old one.\n\n### Why Membership Preview is a Read\n\nThe alternative—a declared action—locks the record, writes an audit row, and emits an outbox event just to look at a list. It also needs an `allocation.run` row to exist first, which would mean writing a permanent run record for an attempt that has not been approved yet.\n\n## Contributing\n\nWhen adding new features to the allocation engine:\n\n1. **Preserve idempotency:** Any new action that moves money must have a unique key\n2. **Respect the freeze:** Rules that have posted cannot be edited\n3. **Guard the guards:** Any control that prevents unsigned disbursements must be protected\n4. **Audit trail:** Every decision that affects money flow must be named in the audit log\n5. **Decimal precision:** Use `rust_decimal`, never `f64`, for all money amounts","harakat-kernel-crates":"# Harakat_Kernel — crates\n\n\n\n# Harakat_Kernel — crates/kernel-api\n\n## Overview\n\nThe `kernel-api` crate implements the HTTP API surface for the Harakat Kernel. It provides gateway-compatible endpoints for executing operations, managing claims, authoring custom entities, and system-level identity operations. The crate enforces strict security boundaries through constant-time secret verification, SVID validation, claims resolution, and attestation binding.\n\n## Architecture\n\n```mermaid\ngraph TB\n A[HTTP Request] --> B{execute.rs}\n B --> C[Gateway Secret Check]\n B --> D[SVID Verification]\n B --> E[Tenant Identity]\n B --> F[Claims Resolution]\n B --> G[Model/Entitlement Gate]\n B --> H[Dispatch Resolution]\n H --> I[Read Path]\n H --> J[Mutation Path]\n H --> K[Capability Path]\n H --> L[Copy Path]\n I --> M[reads.rs]\n J --> N[mutations.rs]\n K --> O[chatter.rs]\n L --> N\n C --> P{Fail-Closed}\n D --> P\n E --> P\n F --> P\n G --> P\n```\n\n## Core Components\n\n### 1. Binding Module (`binding.rs`)\n\nProvides canonical operation hashing for attestation-bound approval grants (RFC 001).\n\n#### `canonical_op_hash`\n\nComputes SHA-256 over the canonical JSON representation of an operation:\n\n```rust\npub fn canonical_op_hash(\n tenant: i64,\n model: &str,\n method: &str,\n args: &[serde_json::Value],\n kwargs: &serde_json::Map<String, serde_json::Value>,\n) -> String\n```\n\n**Canonical Form:** `{\"tenant\":…,\"model\":…,\"method\":…,\"args\":…,\"kwargs\":…}`\n\nKey requirements:\n- Keys must be in EXACT order (serde_json preserve_order)\n- No whitespace in JSON\n- Args/kwargs must use JSON-integer and string scalars\n- Cross-language float formatting is NOT normalized\n\n**Usage:** The platform server computes the same hash over the operation shown to the human during passkey ceremony. The kernel recomputes it over the operation actually received. `kernel_core::mutation::check_attestation_binding` compares them — a grant minted for op A can never authorize op B.\n\n#### `entity_def_hash`\n\nComputes SHA-256 over an entity definition's canonical, shape-bearing form (RFC 010). This is the key that makes a re-define of the identical shape idempotent and a re-define of a different shape a `conflict`.\n\n```rust\npub fn entity_def_hash(def: &kernel_core::entity::EntityDef) -> String\n```\n\n**Important:** The hash excludes `status` and `def_hash` — these are lifecycle fields, not shape fields.\n\n### 2. Claims Route (`claims_route.rs`)\n\nHandles platform claims projection push (K2 opener).\n\n#### `POST /claims`\n\nThe platform server pushes the FULL claim set for one tenant (replace semantics — self-healing, no incremental drift).\n\n**Request:**\n```json\n{\n \"secret\": \"gateway-secret\",\n \"tenant\": 123,\n \"version\": 42,\n \"claims\": [\n {\n \"uid\": 1,\n \"role\": \"admin\",\n \"level\": 10,\n \"branch_id\": 5,\n \"branch_name\": \"HQ\",\n \"subordinate_ids\": [2, 3, 4]\n }\n ]\n}\n```\n\n**Security:**\n- Gateway secret required (body or `x-gateway-secret` header)\n- Constant-time comparison via `subtle::ConstantTimeEq`\n- Stale pushes (version < stored) are rejected\n- Equal re-pushes are allowed (idempotent retry)\n- Fail-closed: no sink wired → deny every push\n\n**Response:**\n```json\n{\n \"ok\": true,\n \"tenant\": 123,\n \"version\": 42,\n \"count\": 1\n}\n```\n\n### 3. Entity Routes (`entity_routes.rs`)\n\nCustom-entity HTTP authoring surface (RFC 010 E2). Enables pack/tenant to bring new record types into existence WITHOUT a Rust crate, migration, or redeploy.\n\n#### Routes\n\n| Method | Path | Description |\n|--------|------|-------------|\n| `GET` | `/entity?tenant=[&model=]` | One definition, or all of tenant's |\n| `POST` | `/entity/define` | Create definition + real table |\n| `POST` | `/entity/amend` | Additive change (add/deprecate) |\n| `POST` | `/entity/retire` | SOFT hide; table + rows survive |\n\n#### Security\n\nSame constant-time gateway secret, pre-derived-claims + tenant-match check, and CRITICAL-risk synthetic action requiring **passkey attestation BOUND to the canonical operation hash** — literally the same `authorize_write` function as `/overlay/set`.\n\n#### Entity Define Request\n\n```json\n{\n \"uid\": 1,\n \"harakat_startup_id\": 123,\n \"def\": {\n \"model\": \"x_awqaf.waqf\",\n \"label\": \"Waqf\",\n \"fields\": [\n {\n \"name\": \"deed_no\",\n \"label\": \"Deed\",\n \"type\": \"text\",\n \"unique\": true\n }\n ]\n },\n \"approval_grant\": {\n \"kind\": \"passkey_attestation\",\n \"payload_hash\": \"sha256-hash\"\n }\n}\n```\n\n**Important:** The `def_hash` field is IGNORED on the wire — the kernel stamps the authoritative hash over the canonical shape.\n\n#### Entity Amend\n\n```json\n{\n \"uid\": 1,\n \"harakat_startup_id\": 123,\n \"model\": \"x_awqaf.waqf\",\n \"expected_schema_version\": 1,\n \"add_fields\": [\n {\n \"name\": \"gps\",\n \"label\": \"GPS\",\n \"type\": \"text\"\n }\n ],\n \"deprecate_fields\": [\"old_field\"],\n \"approval_grant\": {\n \"kind\": \"passkey_attestation\",\n \"payload_hash\": \"sha256-hash\"\n }\n}\n```\n\n**The MERGE lives here:** The caller sends a delta, the kernel owns the resulting definition (so a client can never hand-craft a shape that skips the additive-only law).\n\n### 4. Execute Endpoint (`execute.rs`)\n\nThe gateway-compatible endpoint (`POST /execute`).\n\n#### Processing Order\n\n```mermaid\ngraph LR\n A[Request] --> B[Trace ID]\n B --> C[Gateway Secret]\n C --> D[SVID]\n D --> E[Tenant Identity]\n E --> F[Claims]\n F --> G[Model/Entitlement]\n G --> H[Delegation]\n H --> I[Dispatch]\n```\n\n1. **Trace ID:** `x-trace-id` header → body `trace_id` → fresh UUIDv7\n2. **Gateway Secret:** Body `secret` or `x-gateway-secret` header, constant-time comparison\n3. **SVID:** Verified when required OR whenever presented\n4. **Tenant Identity:** `harakat_startup_id` MANDATORY; authority version must match\n5. **Claims:** `(uid, tenant)` → pre-derived `AccessClaims`; unknown uid or mismatch → denied\n6. **Model + Entitlement:** Unknown model → `not_found`; DATA-AUTHORED entity without active definition → `not_found`\n7. **Delegation:** Token must verify for `tool = \"model.method\"`; outbound = (mutation && action risk >= High)\n8. **Dispatch:** Reads via `ReaderPort`, mutations via mutation path with API-level `check_approval` gate run BEFORE the mutator\n\n#### Request Structure\n\n```json\n{\n \"model\": \"account.move\",\n \"method\": \"write\",\n \"args\": [\"uuid-123\", {\"amount\": 100.00, \"partner_id\": \"uuid-456\"}],\n \"kwargs\": {\n \"query\": {\"filters\": [{\"field\": \"state\", \"op\": \"=\", \"value\": \"posted\"}]}\n },\n \"secret\": \"gateway-secret\",\n \"harakat_startup_id\": \"123\",\n \"harakat_authority_version\": \"v1.0\",\n \"uid\": 1,\n \"delegation_token\": \"eyJ0eXAiOiJKV1QiLCJhbGc...\"\n}\n```\n\n### 5. Dispatch Resolution (`execute/dispatch.rs`)\n\nSplits incoming `(model, method)` into read class or declared mutation action.\n\n#### Dispatch Types\n\n```rust\npub(crate) enum Dispatch<'a> {\n Read(ReadKind),\n Capability(&'static CapabilityMeta),\n Mutation(&'a ActionMeta),\n Copy(&'a ActionMeta),\n}\n```\n\n**Read Kinds:**\n- `Read` — standard read\n- `SearchRead` — search with domain\n- `SearchCount` — count with domain\n- `NameSearch` — Odoo typeahead\n- `ReadGroup` — grouped aggregates\n- `Report` — model-declared multi-model aggregate\n\n**Capabilities:** Reserved, all-model capabilities (chatter: thread/followers/activities) resolve BEFORE per-model action lookup.\n\n**Copy:** Borrows the model's `create` action for risk/approval gating.\n\n### 6. Chatter Handler (`execute/chatter.rs`)\n\nAll-model dispatch class for thread-related operations.\n\n#### Capabilities\n\n| Capability | Description |\n|------------|-------------|\n| `thread_read` | Read thread messages |\n| `list_followers` | Get followers of a record |\n| `activity_list` | List activities for a record |\n| `activity_types` | Get available activity types |\n| `audit_read` | Read audit log |\n| `audit_period_read` | Audit period export |\n| `message_post` | Post a message |\n| `subscribe` | Subscribe to thread |\n| `unsubscribe` | Unsubscribe from thread |\n| `activity_schedule` | Schedule an activity |\n| `activity_update` | Update an activity |\n| `activity_delete` | Delete an activity |\n| `activity_done` | Mark activity complete |\n\n**Security:** Writes run the SAME approval + attestation-binding gates as `run_mutation` (declared requirement ⊕ tenant policy override, widen-only) before the port; READS skip them.\n\n### 7. Mutation Handler (`execute/mutations.rs`)\n\nHandles create/write/unlink and custom actions.\n\n#### Processing Flow\n\n```mermaid\ngraph TD\n A[Request] --> B[Strip Server-Minted Args]\n B --> C[Approval Gate]\n C --> D[Attestation Binding]\n D --> E[Inject Verified Approval]\n E --> F[P3.5 Fund Required]\n F --> G[Mutator Execute]\n```\n\n1. **Strip Server-Minted Args:** `VERIFIED_APPROVAL_ARG` and `TENANT_FUND_REQUIRED_ARG` are removed before canonical hash computation\n2. **Approval Gate:** Declared requirement ⊕ tenant policy override (overrides only WIDEN)\n3. **Attestation Binding:** Recompute canonical hash and compare with grant's `payload_hash`\n4. **Inject Verified Approval:** Write `VERIFIED_APPROVAL_ARG` only when grant is BOUND to this operation\n5. **P3.5 Fund Required:** Tell app logic which ledger standard this tenant keeps\n\n#### Mutation Operations\n\n```rust\npub enum MutationOp {\n Create { values: ValueMap },\n Update { id: Uuid, values: ValueMap },\n Delete { id: Uuid },\n Action { id: Uuid, name: String, args: ValueMap },\n}\n```\n\n### 8. Read Handler (`execute/reads.rs`)\n\nRead-side capability handlers.\n\n#### Query Parsing\n\nSupports two dialects:\n1. **Kernel Query:** `kwargs.query` with `Filter` objects\n2. **Odoo Domain:** `args[0]` as `[field, op, value]` terms\n\n**Supported Operators:**\n- `=`, `!=` (also `<>`), `<`, `<=`, `>`, `>=`\n- `in`, `not in`\n- `like`, `ilike` (both map to case-insensitive `Contains`)\n\n**Domain + kwargs.query together is ambiguous → invalid.**\n\n#### Read Group\n\nGroups by the FIRST groupby field over the reader's already-visible rows.\n\n**Aggregate Types:**\n- `sum` — sum of numeric fields\n- `avg` — average of numeric fields\n- `min` — minimum value\n- `max` — maximum value\n- `count` — row count\n\n**Important:** Decimal money stays exact (summed/averaged as `Decimal`, emitted as string — floats corrupt money).\n\n#### Name Search\n\nOdoo shape: `kwargs = {name: q, limit}`, returns `[[id, label], …]`.\n\nFilters the display (`name`) field with a case-insensitive substring when `q` is non-empty.\n\n### 9. System Writer (`execute/system.rs`)\n\nLeast-privilege System-writer endpoint (`POST /system/execute`).\n\n**Purpose:** The wave-3 unblock for the platform employee-provisioning writer. Makes the employee identity spine (`hr.employee.create`, the identity-core `write`, `apply_platform_org`) **System-only**.\n\n**Security:**\n1. **DEDICATED secret** (`KERNEL_SYSTEM_SECRET`) — NOT the gateway secret\n2. **System actor carries NO uid, NO claims resolution, NO entitlement or client tier gate**\n3. **Compile-time allowlist** — only employee/department/job identity spine\n\n## Security Model\n\n### Gateway Secret\n\n```rust\npub(crate) fn check_secret(provided: Option<&str>, expected: &str) -> Result<(), KernelError>\n```\n\n- Empty configured secret fails CLOSED (gateway parity: `if not expected or not secret or not compare`)\n- Constant-time comparison via `subtle::ConstantTimeEq`\n- Missing/wrong → denied\n\n### SVID Verification\n\n```rust\nstate.svid.verify(token).await?;\n```\n\n- Verified when required OR whenever one is presented\n- Foreign domain/invalid → denied\n- Required and absent → denied\n\n### Claims Resolution\n\n```rust\nlet claims = (state.config.claims_resolver)(req.uid, tenant_id)\n .ok_or_else(|| KernelError::denied(format!(\"unknown user {}\", req.uid)))?;\n```\n\n- Unknown uid → denied\n- Claims/tenant mismatch → denied\n- Actor kind = Assistant when delegation token is presented, else User\n\n### Delegation Verification\n\n```rust\nlet check = DelegationCheck {\n tool: format!(\"{}.{}\", req.model, req.method),\n outbound,\n uid: req.uid,\n};\nlet verdict = state.delegation.verify(&ctx, &token, &check).await?;\n```\n\n- Must verify for `tool = \"model.method\"`\n- Outbound = (mutation && action risk >= High)\n- Tier ceiling narrows the claims the read filter sees\n\n### Attestation Binding\n\n```rust\nlet expected = crate::binding::canonical_op_hash(\n ctx.tenant_id.0,\n &req.model,\n &req.method,\n &req.args,\n &kwargs,\n);\ncheck_attestation_binding(effective, req.approval_grant.as_ref(), &expected)?;\n```\n\n- A passkey-attested grant must be BOUND to this exact operation\n- The kernel recomputes the canonical-op hash over what it actually received and compares\n- A grant minted for op A can never authorize op B\n\n## Integration Points\n\n### With kernel-core\n\n- `kernel_core::entity::EntityDef` — entity definitions\n- `kernel_core::mutation::Mutation` — mutation operations\n- `kernel_core::actor::ActorCtx` — actor context\n- `kernel_core::claims::AccessClaims` — access claims\n- `kernel_core::descriptor::ModelDescriptor` — model descriptors\n- `kernel_core::query::Query` — query objects\n\n### With kernel-overlay\n\n- `state.overlay.tenant_policy(ctx)` — tenant policy overrides\n- `state.overlay.effective_descriptor(ctx, &req.model)` — effective descriptor\n- `state.overlay.validate_custom_values(ctx, &req.model, &values)` — custom value validation\n\n### With kernel-storage\n\n- `state.reader` — read operations\n- `state.mutator` — mutation execution\n- `state.thread` — thread operations\n\n### With kernel-authz\n\n- `state.delegation` — delegation verification\n- `state.svid` — SVID verification\n\n## Testing\n\nThe crate includes comprehensive tests:\n\n- `binding.rs` — canonical hash determinism and component sensitivity\n- `execute.rs` — secret check fail-closed behavior\n- `execute/dispatch.rs` — chatter methods resolve as capabilities\n- `execute/reads.rs` — UUID parse is invalid not denied\n\n## Configuration\n\nKey environment variables:\n\n- `KERNEL_GATEWAY_SECRET` — gateway secret for `/execute` and `/entity`\n- `KERNEL_SYSTEM_SECRET` — dedicated secret for `/system/execute`\n- `KERNEL_SVID_REQUIRED` — whether SVID is required\n- `KERNEL_CUSTOM_ENTITIES` — enable custom entity authoring\n- `KERNEL_COMPUTED_FIELDS` — enable computed fields\n\n## Error Handling\n\nAll errors include an `x-trace-id` response header. Error types:\n\n- `KernelError::Invalid` — malformed request\n- `KernelError::Denied` — authorization failure\n- `KernelError::NotFound` — model/record not found\n- `KernelError::Conflict` — optimistic concurrency failure\n- `KernelError::Internal` — server-side error\n\n## RFC References\n\n- **RFC 001** — Attestation binding for approval grants\n- **RFC 005** — Ownership manager-of-owner read branch\n- **RFC 010** — Custom entity authoring\n- **RFC 011** — Grant visibility and fund requirements\n- **RFC 012** — Per-tenant gate for computed fields\n\n## Contributing\n\nWhen modifying this module:\n\n1. **Maintain canonical hash parity** — any change to serialization must update platform tests\n2. **Keep fail-closed** — missing configuration should deny, not accept\n3. **Preserve constant-time comparisons** — never use `==` for secrets\n4. **Document security implications** — especially for new endpoints\n5. **Test cross-language parity** — ensure platform TS side can reproduce hashes","harakat-kernel-dev":"# Harakat_Kernel — dev\n\n\n\n# Harakat_Kernel — dev\n\nThis module provides the development environment infrastructure and launcher scripts for the `kernel-server` binary. It is designed to run an isolated, non-production stack that mirrors the production architecture without sharing resources or ports with the live Harakat stack.\n\n## Overview\n\nThe `dev` module serves two primary functions:\n1. **Infrastructure Provisioning:** Manages Docker containers for Postgres and OpenBao required by the kernel server during development.\n2. **Process Launching:** Provides cross-platform scripts (`run-kernel.ps1`, `run-kernel.sh`) to load environment variables and execute the `kernel-server` binary.\n\nUnlike the production stack, this environment is deliberately isolated. Starting or stopping these services does not affect the live stack and requires no stack lock.\n\n## Architecture\n\nThe module consists of four key files:\n* `docker-compose.dev.yml`: Defines the containerized services.\n* `kernel.env.example`: Template for local environment configuration.\n* `run-kernel.ps1`: Windows launcher script.\n* `run-kernel.sh`: Linux launcher script.\n\n### Infrastructure Isolation\n\nThe dev stack uses distinct ports and container names to prevent conflicts with the live stack:\n\n| Component | Dev Port | Live Port | Container Name |\n| :--- | :--- | :--- | :--- |\n| Postgres | 5442 | 5432 | `harakat-kernel-dev-pg` |\n| OpenBao | 8210 | 8200 | `harakat-kernel-dev-openbao` |\n\n**OpenBao Configuration:**\nThe OpenBao service runs in dev mode (`server -dev`) with an in-memory backend and auto-unsealed state. The root token is `root`. **Never** place real tenant keys in this environment.\n\n**Database Persistence:**\nThe Postgres volume `kernel_dev_pgdata` is marked as `external: true`. This allows the volume to be restored onto a new machine by matching mount paths, a requirement following the 2026-07-27 move.\n\n## Configuration\n\n### Environment Variables\n\nDevelopers must create a local file named `.env.kernel.local` (gitignored) based on `kernel.env.example`. This file is sourced by the launcher scripts.\n\n**Critical Security Constraints:**\n* **Secret Split:** `KERNEL_GATEWAY_SECRET` and `KERNEL_SYSTEM_SECRET` must be **distinct** random values. The kernel refuses to boot if they are identical.\n* **System Secret:** If `KERNEL_SYSTEM_SECRET` is unset or empty, the `/system/execute` endpoint remains dark (fail-closed).\n* **Delegation:** If `KERNEL_DELEGATION_URL` is set, `KERNEL_INTERNAL_AUTH` must be at least 32 characters and match the server's value. A mismatch causes the kernel to refuse booting to prevent opaque 401 errors during delegated calls.\n\n**Key Variables:**\n* `KERNEL_BIND`: Address for the kernel server (default `127.0.0.1:8300`).\n* `DATABASE_URL`: Connection string for the dev Postgres instance.\n* `KERNEL_TENANTS`: Tenant ID (e.g., `19`).\n* `KERNEL_ENTITLEMENTS`: Semicolon-separated list of tenant:key pairs.\n* `KERNEL_AUTHORITY`: Authority assertion version (leave empty until server proxy threading is complete).\n\n### Launcher Scripts\n\nBoth scripts perform the following steps:\n1. Validate the existence of `.env.kernel.local`.\n2. Export all `KEY=VALUE` pairs from the file into the process environment.\n3. Determine the binary path based on the `--release` flag (or default to debug).\n4. Verify the binary exists.\n5. Execute the binary in the foreground.\n\n**Windows (`run-kernel.ps1`):**\n* Requires PowerShell.\n* Supports `-Release` switch.\n* Checks for `target\\release\\kernel-server.exe` or `target\\debug\\kernel-server.exe`.\n\n**Linux (`run-kernel.sh`):**\n* Requires Bash.\n* Supports `--release` flag.\n* Checks for `target/$profile/kernel-server`.\n\n## Execution Flow\n\nThe following diagram illustrates the startup sequence when running the kernel in development.\n\n```mermaid\nsequenceDiagram\n participant Dev as Developer\n participant Script as run-kernel.sh/.ps1\n participant Env as .env.kernel.local\n participant Bin as kernel-server\n participant Docker as Docker Compose\n\n Dev->>Script: Run script\n Script->>Env: Load variables\n Script->>Script: Validate Binary Exists\n Script->>Docker: Ensure Stack Running (5442/8210)\n Script->>Bin: Execute Binary\n Bin->>Docker: Connect to Postgres (5442)\n Bin->>Docker: Connect to OpenBao (8210)\n Bin-->>Dev: Kernel Running\n```\n\n## Usage\n\n### Prerequisites\n1. Ensure Docker is running.\n2. Ensure the dev stack is up:\n ```bash\n docker compose -f dev/docker-compose.dev.yml up -d\n ```\n3. Build the kernel binary:\n ```bash\n cargo build -p kernel-server\n # or\n cargo build --release -p kernel-server\n ```\n\n### Running the Kernel\n**Windows:**\n```powershell\npowershell -File dev\\run-kernel.ps1\n# For release binary:\npowershell -File dev\\run-kernel.ps1 -Release\n```\n\n**Linux:**\n```bash\nbash dev/run-kernel.sh\n# For release binary:\nbash dev/run-kernel.sh --release\n```\n\n## Troubleshooting & Notes\n\n### Stopping the Kernel\nDo **not** stop the kernel by killing the port owner (e.g., `kill -9` on port 8300). This is a Docker landmine that can leave containers in a broken state. Instead, stop the process by PID or use `Ctrl+C` in the terminal running the script.\n\n### Database Recovery\nIf the `kernel_dev_pgdata` volume is lost, it can be recovered by matching the mount path on a new machine. The volume is named `harakat_kernel_dev_pgdata` and is external.\n\n### Delegation Failures\nIf delegation tokens are refused:\n1. Check `KERNEL_DELEGATION_URL` is set.\n2. Verify `KERNEL_INTERNAL_AUTH` is at least 32 characters.\n3. Ensure `KERNEL_INTERNAL_AUTH` matches the server's configuration exactly.\n4. If `KERNEL_DELEGATION_URL` is set but `KERNEL_INTERNAL_AUTH` is missing/short, the kernel will refuse to boot to prevent misconfiguration from appearing as an outage.\n\n### Authority Version\nThe `KERNEL_AUTHORITY` variable is currently empty. The server proxy does not yet thread `harakat_authority_version` for `/execute` and `/system/execute` mutations. Leave this empty until that threading logic is implemented.","harakat-kernel-docs":"# Harakat_Kernel — docs\n\n\n\n# Harakat_Kernel — docs Module\n\n## Overview\n\nThe `Harakat_Kernel/docs` module is the central documentation repository for the Harakat Kernel. It contains the written specification for the kernel's contracts, development procedures, and RFCs that drive the kernel's evolution. This module serves as the authoritative source for understanding the kernel's architecture, change protocol, and implementation details.\n\n## Purpose\n\nThis documentation module provides:\n\n1. **Contract specifications** — The frozen contract definitions that govern kernel behavior\n2. **Development procedures** — Toolchain requirements, test setup, and contribution guidelines\n3. **RFC documentation** — Detailed specifications for each kernel feature addition\n4. **Change protocol** — The process for evolving the kernel after the contract freeze\n\n## Structure\n\n```\ndocs/\n├── contracts.md # Main contract freeze documentation\n├── dev-setup.md # Development environment setup\n└── rfcs/\n ├── 001-app-logic-hooks-and-grant-binding.md\n ├── 002-write-hooks-ondelete-idmap.md\n ├── 003-idmap-bind.md\n ├── 004-shared-x2many-write-validation.md\n ├── 005-record-ownership-mixin.md\n ├── 006-unique-field-index.md\n ├── 007-cross-app-posting-effects.md\n ├── 008-delete-guard-hook.md\n ├── 009-child-writes-dynamic-lines.md\n ├── 010-custom-entities.md\n ├── 011-allocation-grant-visibility.md\n └── 012-computed-fields-and-rollups.md\n```\n\n## Contract Freeze Mechanism\n\nThe kernel operates under a **contract freeze** at `contracts-v1`. After the `contracts-v1` tag, changes to either the documentation or the code (`crates/kernel-core`) happen ONLY through the RFC process:\n\n```mermaid\nflowchart LR\n A[New Feature Needed] --> B[Write RFC in docs/rfcs/NNN-<slug>.md]\n B --> C[WS-0 Approves]\n C --> D[Implement kernel-core change]\n D --> E[Bump minor version]\n E --> F[Re-tag contracts-v1.N]\n F --> G[Affected workstreams rebase]\n```\n\n### Key Principles\n\n- **Additive only** — All changes are additive; existing APIs remain byte-identical\n- **Fail-safe defaults** — New features default to no-op behavior\n- **Crypto out of contract** — SHA-256 hashing happens in kernel-api, not kernel-core\n- **Tenant-scoped** — All changes respect tenant isolation\n\n## Key Components\n\n### 1. contracts.md\n\nThe main contract documentation that tracks the frozen contract version and its evolution.\n\n**Current State:** `contracts-v1.10` (RFC 010, 2026-07-24)\n\n**Contract Layers:**\n- **E1** — Contract layer only (kernel-core)\n- **E2** — Storage + registration + endpoints (kernel-storage + kernel-api)\n- **E5** — Live-walk finding (per-tenant catalog)\n\n**Key Types:**\n- `EntityDef` — Serializable authoring spec for custom entities\n- `EntityAuthoringPort` — Definition-level twin of `OverlayAuthoringPort`\n- `DynamicModelHandle` — Generic `ModelHandle` over runtime descriptor\n\n### 2. dev-setup.md\n\nDevelopment environment configuration and toolchain requirements.\n\n**Toolchain:**\n```bash\n# Rust stable via rustup (MSVC on Windows)\nrustls everywhere — NO OpenSSL (cargo-deny enforces this)\nSQLX_OFFLINE=true for DB-less builds\n```\n\n**Test Services:**\n```bash\ndocker compose -f dev/docker-compose.dev.yml up -d\n# Postgres 16: 127.0.0.1:5442 (kernel / kernel, db kernel_test)\n# OpenBao: 127.0.0.1:8210 (token root)\n```\n\n**Commands:**\n```bash\ncargo test --workspace # unit + integration\ncargo fmt --all -- --check\ncargo clippy --workspace --all-targets -- -D warnings\ncargo deny check bans licenses sources\n```\n\n### 3. RFC Documents\n\nEach RFC follows a consistent structure:\n\n```markdown\n# RFC NNN — <title>\n\nStatus: <STATE> (date, wave). Bumps contracts-v1.N → contracts-v1.N+1\n\n## Why\n- Problem statement\n- Gap analysis\n- Consumer needs\n\n## What changes in kernel-core (all ADDITIVE, default-implemented)\n- New types, traits, hooks\n- Execution order\n- Authorization posture\n\n## What changes elsewhere (non-frozen crates)\n- kernel-storage, kernel-testkit, kernel-api, kernel-server\n\n## Blast radius\n- Behavior changes\n- Narrowing points\n- Existing model impact\n\n## Proven\n- Unit tests\n- Integration tests\n- Live smoke tests\n\n## Migration note\n- Data migration requirements\n- DDL changes\n- Backward compatibility\n```\n\n## Connection to Codebase\n\n### kernel-core\n\nThe documentation references actual kernel-core types and traits:\n\n| Documentation Reference | Code Location |\n|------------------------|---------------|\n| `EntityDef` | `crates/kernel-core/src/entity.rs` |\n| `ModelHandle` | `crates/kernel-core/src/model.rs` |\n| `ActionEffects` | `crates/kernel-core/src/model.rs` |\n| `IdMapPort` | `crates/kernel-core/src/idmap.rs` |\n| `OverlayAuthoringPort` | `crates/kernel-core/src/overlay.rs` |\n\n### kernel-storage\n\nStorage implementations referenced in docs:\n\n| Documentation Reference | Code Location |\n|------------------------|---------------|\n| `PgEntityStore` | `crates/kernel-storage/src/entity_store.rs` |\n| `PgIdMap` | `crates/kernel-storage/src/idmap.rs` |\n| `PgKernel` | `crates/kernel-storage/src/kernel.rs` |\n| `create_table_sql` | `crates/kernel-storage/src/ddl.rs` |\n\n### kernel-api\n\nAPI routes and governance:\n\n| Documentation Reference | Code Location |\n|------------------------|---------------|\n| `/entity/*` routes | `crates/kernel-api/src/entity_routes.rs` |\n| `/overlay/*` routes | `crates/kernel-api/src/overlay_routes.rs` |\n| `/idmap/*` routes | `crates/kernel-api/src/idmap_routes.rs` |\n| `check_attestation_binding` | `crates/kernel-api/src/mutation.rs` |\n\n### kernel-server\n\nServer wiring and configuration:\n\n| Documentation Reference | Code Location |\n|------------------------|---------------|\n| `real_registry_with_entities` | `crates/kernel-server/src/real.rs` |\n| `ALLOCATION_ENGINE` | `crates/kernel-server/src/config.rs` |\n| `KERNEL_CUSTOM_ENTITIES` | `crates/kernel-server/src/config.rs` |\n\n## Development Workflow\n\n### Adding a New RFC\n\n1. **Write the RFC** in `docs/rfcs/NNN-<slug>.md`\n - Follow the RFC template structure\n - Include blast radius analysis\n - Specify test plan\n\n2. **Implement in kernel-core**\n - All changes must be additive\n - Default implementations must be no-ops\n - Update `contracts.md` with new version\n\n3. **Implement in kernel-storage**\n - Storage layer parity\n - DDL generation\n - Testkit implementations\n\n4. **Implement in kernel-api**\n - Route definitions\n - Governance checks\n - Attestation binding\n\n5. **Implement in kernel-server**\n - Wiring and configuration\n - Environment variables\n\n6. **Update contracts.md**\n - Bump version number\n - Document the change\n\n### Contributing to Documentation\n\n**Rules:**\n- Write RFCs before implementation\n- Keep RFCs self-contained (no external dependencies)\n- Include test plans in every RFC\n- Update `contracts.md` after each RFC merge\n\n**Style:**\n- Use code blocks for Rust types and function signatures\n- Include Mermaid diagrams only when they clarify architecture\n- Reference actual function names and types\n- Keep diagrams small (5-10 nodes max)\n\n## Security Model\n\nThe documentation describes a multi-layer security model:\n\n```mermaid\nflowchart TD\n A[Client Request] --> B{Gateway Secret}\n B -->|Valid| C{SVID Verification}\n C -->|Valid| D{Tenant Assertion}\n D -->|Valid| E{Approval Grant}\n E -->|Valid| F{Attestation Binding}\n F -->|Valid| G[Kernel Execution]\n G --> H{Tier Check}\n H -->|Pass| I[Entitlement Check]\n I -->|Pass| J[Model Guards]\n J -->|Pass| K[Execute]\n```\n\n**Key Security Points:**\n- Gateway secret: constant-time comparison\n- SVID: SPIFFE trust bundle verification\n- Attestation binding: SHA-256 over canonical op hash\n- Tier/entitlement: default-deny policy\n- Model guards: per-record validation\n\n## Change Protocol\n\nAfter the contract freeze, all changes follow this protocol:\n\n1. **RFC Note** — Write in `docs/rfcs/NNN-<slug>.md`\n2. **WS-0 Approval** — Integrator approves + implements\n3. **Version Bump** — Bump minor version + re-tag\n4. **Workstream Rebase** — Affected crates rebase on new tag\n5. **No Direct Changes** — Nobody else touches kernel-core\n\n## Testing Strategy\n\n### Unit Tests\n- `kernel-core` — Type validation, descriptor compilation\n- `kernel-storage` — DDL generation, index creation\n- `kernel-api` — Route governance, attestation binding\n\n### Integration Tests\n- `kernel-testkit` — InMemoryKernel parity\n- `kernel-storage` — Real Postgres tests\n- `kernel-api` — Live `/execute` smoke tests\n\n### End-to-End Tests\n- `apps/accounts` — Invoice posting, fund accounting\n- `apps/helpdesk` — Action effects, lifecycle\n- `apps/crm` — Write hooks, id-mapping\n\n## Known Limitations\n\n- **P2 Concerns** — Computed fields, entity lifecycle hooks\n- **Aggregate Pushdown** — Declared debt for K2+\n- **Multi-hop Foreign Posting** — One-hop bound only\n- **Per-currency Rounding** — Deferred to later RFC\n\n## Maintenance\n\n**Documentation Ownership:**\n- `contracts.md` — Kernel team\n- RFCs — Feature owners\n- `dev-setup.md` — Platform team\n\n**Review Process:**\n- RFCs require owner + reviewer sign-off\n- Contract bumps require WS-0 approval\n- All changes tracked in `contracts.md`\n\n**Version Tracking:**\n- `contracts-v1` — Initial freeze\n- `contracts-v1.1` — RFC 001\n- `contracts-v1.2` — RFC 002\n- ...\n- `contracts-v1.10` — RFC 010\n\n## Related Documentation\n\n- `docs/rfcs/` — Individual RFC specifications\n- `docs/specs/` — Behavior specifications\n- `docs/architecture/` — Architecture decisions\n- `docs/git-rules.md` — Git workflow rules\n- `docs/plans/` — Platform evolution plans","harakat-kernel-harakat-kernel":"# Harakat_Kernel — Harakat_Kernel\n\n\n\n# Harakat Kernel Workspace\n\n## Overview\n\nThe Harakat Kernel is a Rust-based application framework designed to serve as the backend for an ERP system, replacing Odoo. It operates on a \"kernel-first\" architecture where business logic is compiled into Rust crates (Layer 1) while tenant-specific customizations are handled via interpreted data overlays (Layer 2).\n\nThe workspace is structured as a monorepo containing internal crates, business applications, and ETL tools. All components share a common dependency tree and security policy enforced via `deny.toml`.\n\n## Architecture\n\nThe system is divided into two distinct layers that interact at specific chokepoints.\n\n### Layer 1: Compiled App Framework\nThis layer consists of Rust crates implementing the `App` trait. Models and behaviors are declared using attribute macros (`#[model]`, `#[field]`, `#[computed]`, `#[state_machine]`, `#[sealed]`, `#[tier]`). These macros emit both the runtime code and the UI manifest/agent-tool metadata, ensuring parity by construction.\n\n### Layer 2: Interpreted Tenant Customization\nThis layer governs everything a tenant or agent authors. It is data-driven, not code-driven. Custom fields, relabels, expressions, and automations are stored and resolved against the base schema. The \"Base Schema ⊕ Tenant Overlay\" acts as the wall between the two layers.\n\n### The Four Planes\nFour security and operational planes cut through both layers at chokepoints. Apps cannot opt out of these planes:\n\n1. **Security:** P1 spine adapters (`canSee` port), OpenFGA, delegation, SPIFFE, OpenBao sealing.\n2. **Commercial:** Entitlements and metering.\n3. **Monitoring:** Tracing and watchdogs.\n4. **Scalability:** `tenant_id`-first keys, no cross-tenant transactions, stateless processes.\n\n## Workspace Structure\n\nThe workspace is organized into three primary directories:\n\n```text\nHarakat_Kernel/\n├── crates/ # Internal libraries\n│ ├── kernel-core # Frozen contract layer\n│ ├── kernel-macros # Proc-macros\n│ ├── kernel-storage # SQLX Postgres executor\n│ ├── kernel-seal # OpenBao crypto\n│ ├── kernel-events # Outbox dispatcher\n│ ├── kernel-authz # Security adapters\n│ ├── kernel-expr # Expression language\n│ ├── kernel-overlay # Schema resolver\n│ ├── kernel-api # Axum gateway\n│ ├── kernel-server # Bootstrapping binary\n│ └── kernel-testkit # Fakes and fixtures\n├── apps/ # Business applications\n│ ├── helpdesk # K1 pilot\n│ ├── crm # Customer relationship management\n│ ├── finance # Accounting\n│ └── ... # 30+ other apps\n└── tools/ # ETL and testing harnesses\n ├── diff-harness # Record/replay vs live\n └── etl-* # Data migration tools\n```\n\n## Core Crates\n\n### `kernel-core`\nThe frozen contract layer. Defines traits, ports, `TenantCtx`, `Value`, and `EventEnvelope`. Changes are only permitted via WS-0 RFC and version bumps.\n\n### `kernel-macros`\nProvides proc-macros that emit descriptors and manifests. Used by apps to declare models and fields.\n\n### `kernel-storage`\nHandles SQLX Postgres interactions. Implements a tenant-sealed executor with a single audited mutation path.\n\n### `kernel-seal`\nImplements OpenBao Transit envelope crypto. Bit-compatible with the TypeScript `kms:v1` implementation.\n\n### `kernel-events`\nManages the transactional outbox (row + audit + outbox in one tx), consumer cursors, watchdogs, and metering.\n\n### `kernel-authz`\nImplements spine adapters for authorization, including `canSee`, OpenFGA integration, and SPIFFE verification.\n\n### `kernel-overlay`\nResolves the base schema against tenant overlays. Handles custom fields and automation interpretation.\n\n### `kernel-api`\nThe Axum-based gateway. Exposes `/execute` endpoints, middleware planes, and manifest serving.\n\n### `kernel-server`\nThe binary entry point. Handles configuration, wiring, and bootstrapping.\n\n## Security & Compliance\n\n### Cryptography Policy\nThe workspace enforces a strict **rustls-only** policy. No OpenSSL, native-tls, or schannel dependencies are permitted.\n\n* **Enforcement:** `deny.toml` explicitly bans `openssl`, `openssl-sys`, and `native-tls`.\n* **Dependencies:** `sqlx` and `reqwest` are configured with `tls-rustls-ring-webpki` and `rustls-tls` features respectively.\n* **Verification:** Gate proofs run against real Postgres and real OpenBao (dev) instances; no mocks are allowed in gate proofs.\n\n### Registry Hot-Swap\nThe system uses `arc-swap` for lock-free registry hot-swapping. This allows entities to join the live registry without a redeploy, ensuring readers always load a consistent snapshot.\n\n## Data & Events\n\n### Transactional Outbox\nA single event stream is maintained via a Postgres transactional outbox. Each transaction includes the data row, audit log, and outbox message in a single atomic operation.\n\n### Tenant Isolation\nAll database keys are `tenant_id`-first. Cross-tenant transactions are prohibited to ensure isolation.\n\n## Development Workflow\n\n### Branching Rules\n* `main`: Contains merged known-good code only.\n* `rust-kernel`: Program work happens on this branch.\n* **Commit Discipline:** Commit at every green checkpoint. Never end a session uncommitted. No destructive git commands.\n\n### Building\n* **Bare-metal Windows:** Requires stable MSVC toolchain.\n* **DB-less Builds:** Use `SQLX_OFFLINE=true` to utilize the committed `.sqlx` query cache.\n* **Integration Tests:** Require a running Postgres instance and an OpenBao dev server.\n\n### Testing\n* **Unit:** Standard Rust unit tests.\n* **Integration:** Requires Postgres + OpenBao.\n* **Diff Harness:** `tools/diff-harness` records/replays state against live Odoo to verify parity.\n\n## Architecture Diagram\n\nThe following diagram illustrates the relationship between the Kernel Layers, the Four Planes, and the Workspace components.\n\n```mermaid\ngraph TD\n subgraph \"Layer 1: Compiled Framework\"\n App[App Trait]\n Macros[Proc-Macros]\n Core[kernel-core]\n end\n\n subgraph \"Layer 2: Interpreted Customization\"\n Overlay[kernel-overlay]\n Data[Tenant Data]\n end\n\n subgraph \"Four Planes\"\n Sec[Security: OpenFGA, OpenBao]\n Comm[Commercial: Metering]\n Mon[Monitoring: Tracing]\n Scal[Scalability: tenant_id keys]\n end\n\n subgraph \"Workspace\"\n Crates[crates/]\n Apps[apps/]\n Tools[tools/]\n end\n\n App --> Core\n Macros --> App\n Core --> Sec\n Core --> Comm\n Core --> Mon\n Core --> Scal\n Overlay --> Data\n Apps --> Core\n Apps --> Overlay\n Tools --> Apps\n```\n\n## Git Rules\n\nThe meta-repo's `docs/git-rules.md` binds this repository. Adherence is mandatory:\n1. Commit at every green checkpoint.\n2. Never end a session uncommitted.\n3. No destructive git commands ever.","harakat-kernel-scripts":"# Harakat_Kernel — scripts\n\n\n\n# Harakat_Kernel/scripts\n\nThis module contains shell scripts that enforce architectural boundaries and data integrity gates within the Harakat Kernel repository. These scripts operate as static analysis tools and runtime verification gates, ensuring that `sqlx` dependencies remain confined to kernel-owned system-table crates and that database migrations function correctly for both fresh and existing databases.\n\n## Architectural Context\n\nThe scripts enforce two critical laws defined in `docs/contracts.md`:\n\n1. **WS-B (sqlx Ban):** `sqlx` is banned from tenant application data paths. Tenant data must flow through `kernel-storage`'s `TenantExecutor`. Only specific kernel crates are allowed to use `sqlx` directly.\n2. **Boot Gate:** The kernel database must be able to boot from a fresh state and maintain integrity for existing states. This guards against defects in migration ordering and schema generation (specifically regarding `allocation_rule_line` and descriptor indexes).\n\n## Script: check-sqlx-boundary.sh\n\n`check-sqlx-boundary.sh` is a static analysis gate that scans the repository for unauthorized usage of the `sqlx` crate.\n\n### Purpose\nEnsures `sqlx` is not used in `apps/`, `tools/`, or non-allowlisted `crates/`. It prevents tenant applications from bypassing `kernel-storage`'s `TenantExecutor`.\n\n### Allowlist\nThe script defines a hardcoded allowlist of crates permitted to use `sqlx`. Any crate outside this list is scanned for violations.\n\n```bash\nALLOWLIST=(\n \"crates/kernel-storage\"\n \"crates/kernel-events\"\n \"crates/kernel-overlay\"\n \"crates/kernel-testkit\"\n \"crates/kernel-server\"\n \"crates/kernel-integration\"\n)\n```\n\n*Note: `kernel-integration` is included as it contains the integrator's evidence crate (G2+ probes) that inspect kernel columns directly.*\n\n### Scanning Logic\nThe script performs the following steps:\n\n1. **Root Resolution:** Resolves the repository root relative to the script's location.\n2. **Manifest Scan:** Iterates through `Cargo.toml` files in `crates/`, `apps/`, and `tools/`.\n3. **Source Scan:** Iterates through `src/`, `tests/`, `examples/`, and `benches/` directories within each crate.\n4. **Filtering:**\n * Ignores comment lines in TOML (`#`).\n * Ignores comment lines in Rust (`//`).\n * Matches `sqlx` in dependency lines or feature references.\n5. **Violation Handling:** Offenders are collected in the `offenders` array.\n\n### Execution Flow\nThe script uses a linear scan with conditional branching based on the `is_allowed` function.\n\n```mermaid\nflowchart TD\n Start[Start Script] --> Resolve[Resolve Repo Root]\n Resolve --> Loop[Iterate Manifests]\n Loop --> CheckAllowed{Is Crate Allowed?}\n CheckAllowed -->|No| ScanFiles[Scan Cargo.toml & src/**/*.rs]\n ScanFiles --> FoundSqlx{Found sqlx?}\n FoundSqlx -->|Yes| AddOffender[Add to offenders array]\n FoundSqlx -->|No| Loop\n CheckAllowed -->|Yes| Loop\n AddOffender --> Loop\n Loop --> EndScan[End Scan]\n EndScan --> CheckOffenders{Offenders > 0?}\n CheckOffenders -->|Yes| Fail[Exit 1: Print Offenders]\n CheckOffenders -->|No| Pass[Exit 0: OK]\n```\n\n### Exit Codes\n* **0:** Success. `sqlx` is confined to the allowlist.\n* **1:** Failure. `sqlx` found in a non-allowlisted crate. Prints offending file paths and line numbers to stderr.\n\n## Script: migrate-gate.sh\n\n`migrate-gate.sh` is a runtime gate that verifies the kernel's database migration logic. It ensures that a fresh database can be created and migrated, and that an existing database can boot without data loss.\n\n### Purpose\nThis script addresses historical defects (as of 2026-08-03) where:\n* `accounts` V5 was a bare `ALTER TABLE allocation_rule_line` that failed to create the table when `ALLOCATION_ENGINE` was off.\n* Descriptor indexes were emitted before app migrations, causing boot failures (42703) on older schemas.\n\n### Test Requirements\nThe script enforces that specific tests must pass. Renaming or ignoring these tests causes the gate to fail.\n\n```bash\nEXPECTED=(\n \"fresh_db_migrates_with_the_allocation_engine_off\"\n \"fresh_db_migrates_with_the_allocation_engine_on\"\n \"engine_switched_on_after_an_engine_off_database_is_complete\"\n \"existing_pre_p3_database_boots_and_keeps_its_rows\"\n \"migrate_is_idempotent_across_boots\"\n \"fresh_db_boot_is_green\"\n \"upgrade_from_old_layout_is_seamless\"\n \"the_two_halves_compose_to_the_whole_table_ddl\"\n \"the_columns_half_emits_no_index_and_the_index_half_no_table\"\n)\n```\n\n### Execution Logic\n1. **Environment:** Requires `Postgres` at `TEST_DATABASE_URL` (default: `postgres://kernel:kernel@127.0.0.1:5442/kernel_test`).\n2. **Live Requirement:** Sets `G_REQUIRE_LIVE=1` to ensure tests fail if Postgres is unavailable, rather than skipping silently.\n3. **Test Execution:**\n * Runs `kernel-integration` tests: `migrate_ordering_pg`, `employees_split_pg`.\n * Runs `kernel-storage` library tests: `ddl::`.\n4. **Verification:**\n * Checks exit codes of `cargo test` commands.\n * Parses output to verify every test in `EXPECTED` reported `ok`.\n * Fails if any test is missing, renamed, or ignored.\n\n### Usage\nRun from the repository root or any directory:\n\n```bash\nbash scripts/migrate-gate.sh\n```\n\n### Exit Codes\n* **0:** All expected tests passed.\n* **1:** `cargo test` failed, or expected tests were not found in the output.\n\n## Integration\n\nThese scripts are designed to be run independently but are typically integrated into CI pipelines or local pre-commit hooks.\n\n* **check-sqlx-boundary.sh:** Should be run before committing changes to `Cargo.toml` or `src/**/*.rs` files to prevent architectural drift.\n* **migrate-gate.sh:** Should be run during the CI build phase to ensure database compatibility before merging changes to the schema.\n\nBoth scripts resolve the repository root dynamically, allowing them to be executed from any working directory.","harakat-kernel-tools":"# Harakat_Kernel — tools\n\n\n\n# diff-harness — Record/Replay + Projected-State Differ\n\n## Overview\n\n`diff-harness` is a verification tool that drives the same operation script against two backends — the Rust kernel (`POST /execute`) and live Odoo (JSON-RPC → `erp.gateway.execute_as`) — then projects the resulting state, normalizes it, and diffs the projections. **Empty diff == parity** (the G5 gate).\n\nThe harness enables:\n- **Record/Replay**: Execute identical operations on both backends\n- **State Projection**: Extract and normalize record data for comparison\n- **Parity Verification**: Detect structural or value differences between backends\n- **Negative Assertions**: Expect errors on specific operations (posting immutability, etc.)\n- **Self-Diff**: Verify determinism within a single backend\n\n## Architecture\n\n```mermaid\ngraph TB\n CLI[CLI: run/diff/selfdiff] --> Dispatch[dispatch]\n Dispatch --> LoadScript[load_script]\n Dispatch --> LoadConfig[load_config]\n LoadScript --> BuildBackend[build_backend]\n LoadConfig --> BuildBackend\n BuildBackend --> KernelBackend[KernelBackend]\n BuildBackend --> OdooBackend[OdooBackend]\n KernelBackend --> Runner[run_script]\n OdooBackend --> Runner\n Runner --> Normalize[normalize_projection]\n Normalize --> Differ[diff_records]\n Differ --> Report[DiffReport]\n Report --> Output[output/markdown]\n```\n\n## Core Components\n\n### Backend Abstraction\n\nThe `Backend` trait defines the contract for both kernel and Odoo implementations:\n\n```rust\npub trait Backend: Send + Sync {\n fn label(&self) -> &'static str;\n async fn execute(&self, op: &ResolvedOp) -> Result<Value, HarnessError>;\n async fn project(\n &self,\n model: &str,\n fields: &[String],\n ids: &[Value],\n include_archived: bool,\n ) -> Result<Vec<Map<String, Value>>, HarnessError>;\n async fn search_ids(&self, model: &str, domain: &Value) -> Result<Vec<Value>, HarnessError>;\n async fn delete(&self, model: &str, id: &Value) -> Result<(), HarnessError>;\n async fn idmap_value(&self, family: &str, odoo_id: i64) -> Result<Value, HarnessError>;\n async fn idmap_reverse(\n &self,\n family: &str,\n values: &[Value],\n ) -> Result<std::collections::BTreeMap<String, Value>, HarnessError>;\n}\n```\n\n**Key methods:**\n- `execute`: Runs a single operation (create/write/action/unlink/count/wizard)\n- `project`: Fetches record data for comparison\n- `search_ids`: Finds records matching a domain (cleanup discovery)\n- `idmap_value`: Resolves external IDs to backend-specific references (K2 topology 3.1d)\n- `idmap_reverse`: Maps backend references back to external IDs for projection normalization\n\n### Operation Types\n\nThe `ResolvedOp` enum represents backend-agnostic operations:\n\n```rust\npub enum ResolvedOp {\n Create { model: String, values: Map<String, Value> },\n Write { model: String, id: Value, values: Map<String, Value> },\n Action { model: String, id: Value, method: String, kwargs: Map<String, Value> },\n Unlink { model: String, id: Value },\n Count { model: String, domain: Value },\n Wizard { model: String, values: Map<String, Value>, action: String },\n}\n```\n\n**Backend-specific conventions:**\n- `create` → `args = [values]`\n- `write` → `args = [[id], values]`\n- `unlink` → `args = [[id]]`\n- `action` → `args = [[id]]`, `kwargs` passed as-is\n- `count` → `args = [domain]`\n- `wizard` → Odoo-leg only (create-then-call TransientModel)\n\n### Script Format\n\nScripts are JSON files defining operations:\n\n```json\n{\n \"name\": \"notes-smoke\",\n \"model\": \"notes.note\",\n \"projection_fields\": [\"title\", \"body\", \"stage\"],\n \"ops\": [\n { \"op\": \"create\", \"values\": { \"title\": \"first note\" } },\n { \"op\": \"write\", \"target\": \"@0\", \"values\": { \"body\": \"edited\" } },\n { \"op\": \"action\", \"target\": \"@0\", \"method\": \"advance\" },\n { \"op\": \"unlink\", \"target\": \"@0\" }\n ]\n}\n```\n\n**Key fields:**\n- `model`: Primary model for the script\n- `projection_fields`: Fields to include in the projection\n- `ops`: Array of operations with `@i` aliases for cross-reference\n- `expect_error`: Negative assertion (operation must fail with matching error)\n- `per_backend`: Backend-specific operation variants\n- `odoo_values`/`kernel_values`: Backend-specific value overrides\n\n### Configuration\n\nConfig files define backend connection details:\n\n```json\n{\n \"kernel\": {\n \"base_url\": \"http://127.0.0.1:7000\",\n \"secret\": \"<gateway shared secret>\",\n \"uid\": 7,\n \"startup_id\": 19,\n \"authority_version\": \"3\",\n \"selfdiff_startup_id_b\": 1900\n },\n \"odoo\": {\n \"base_url\": \"http://127.0.0.1:8069\",\n \"db\": \"harakat_t19\",\n \"login_uid\": 2,\n \"password\": \"<odoo api key>\",\n \"secret\": \"<erp_gateway.shared_secret>\",\n \"uid\": 7,\n \"startup_id\": 19,\n \"authority_version\": \"3\"\n }\n}\n```\n\n**Important distinctions:**\n- `login_uid`/`password`: JSON-RPC transport authentication (Odoo)\n- `uid`: Employee impersonated by `execute_as` (gateway `with_user`)\n- `secret`/`harakat_startup_id`/`harakat_authority_version`: Gateway keyword args\n- `selfdiff_startup_id_b`: Second tenant scope for kernel self-diff\n\n## Execution Flow\n\n### 1. Script Resolution\n\nThe runner resolves `@i` aliases to actual record IDs:\n\n```rust\n// In runner.rs\npub async fn run_script(backend: &dyn Backend, script: &OpScript) -> RunRecord {\n // 1. Execute ops in order, collecting created record IDs\n // 2. Resolve @i references in subsequent ops\n // 3. Execute projection queries\n // 4. Record outcomes and checks\n}\n```\n\n### 2. Projection Normalization\n\nRaw backend rows are normalized for comparison:\n\n```rust\n// In projection.rs\npub fn normalize_projection(\n model: &str,\n fields: &[String],\n raw_rows: Vec<Map<String, Value>>,\n scope: &AliasScope<'_>,\n extra_volatile: &[String],\n) -> Projection {\n // 1. Strip volatile fields (timestamps, create_uid, etc.)\n // 2. Normalize values (false→null, empty string→null, decimal strings→numbers)\n // 3. Map IDs to @i aliases\n // 4. Sort rows by alias index\n}\n```\n\n**Normalization rules:**\n| Input | Output |\n|-------|--------|\n| `false` (Odoo empty) | `null` |\n| `\"\"` (empty string) | `null` |\n| `\"12.30\"` (decimal string) | `12.3` |\n| `\"007\"` (text) | `\"007\"` |\n| `[id, \"display name\"]` (m2o) | `id` |\n| `\"2026-07-17 10:00:00\"` (datetime) | `\"<datetime>\"` |\n| `<br/>` (HTML) | `<br>` |\n\n### 3. State Comparison\n\nThe differ compares two `RunRecord` objects:\n\n```rust\n// In differ.rs\npub fn diff_records(\n left: &RunRecord,\n right: &RunRecord,\n left_label: &str,\n right_label: &str,\n) -> DiffReport {\n // 1. Compare primary projection\n // 2. Compare extra projections (by model)\n // 3. Compare named checks (count ops)\n // 4. Route declared divergences to separate section\n}\n```\n\n**Report structure:**\n- `notes`: Structural mismatches (model/field-set differences)\n- `missing_rows`: Rows in left but not right\n- `extra_rows`: Rows in right but not left\n- `field_mismatches`: Value differences\n- `declared_mismatches`: Owner-approved expected differences\n- `check_mismatches`: Count op mismatches\n\n## CLI Commands\n\n### run\n\nExecute a script against a backend and save the run record:\n\n```bash\ndiff-harness run \\\n --backend kernel|odoo \\\n --config g5.local.json \\\n --script tools/diff-harness/scripts/notes-smoke.json \\\n --out kernel-run.json\n```\n\n### diff\n\nCompare two run records and generate a report:\n\n```bash\ndiff-harness diff \\\n kernel-run.json odoo-run.json \\\n --report g5-report.md\n```\n\nExit codes: `0` = parity, `1` = divergence, `2` = error\n\n### selfdiff\n\nVerify determinism within a single backend:\n\n```bash\n# Kernel: two different tenant scopes\ndiff-harness selfdiff \\\n --backend kernel \\\n --config g5.local.json \\\n --script tools/diff-harness/scripts/notes-smoke.json\n\n# Odoo: two runs of throwaway records\ndiff-harness selfdiff \\\n --backend odoo \\\n --config g5.local.json \\\n --script tools/diff-harness/scripts/notes-smoke.json\n```\n\n## Key Patterns\n\n### Negative Assertions\n\nOperations can declare expected errors:\n\n```json\n{\n \"op\": \"write\",\n \"target\": \"@13\",\n \"values\": { \"reference\": \"TAMPER\" },\n \"expect_error\": \"immutable\"\n}\n```\n\nThe harness records the refusal and continues iff the backend errors with a `\"kind: message\"` containing the substring.\n\n### Per-Backend Operations\n\nDifferent backends may implement the same semantic action differently:\n\n```json\n{\n \"op\": \"per_backend\",\n \"kernel\": {\n \"op\": \"action\",\n \"target\": \"@9\",\n \"method\": \"set_phase\",\n \"kwargs\": { \"phase\": \"prospect\" }\n },\n \"odoo\": {\n \"op\": \"write\",\n \"target\": \"@9\",\n \"values\": { \"harakat_phase\": \"prospect\" }\n }\n}\n```\n\n### Wizard Operations\n\nOdoo-specific wizard pattern (create-then-call):\n\n```json\n{\n \"op\": \"per_backend\",\n \"odoo\": {\n \"op\": \"action\",\n \"target\": \"@6\",\n \"method\": \"merge\",\n \"odoo_wizard\": {\n \"model\": \"helpdesk.ticket.merge\",\n \"action\": \"action_merge\",\n \"values\": {\n \"ticket_ids\": [[6, 0, [\"@6\", \"@4\"]]],\n \"target_ticket_id\": \"@6\",\n \"create_new\": false\n }\n }\n }\n}\n```\n\n## Integration Points\n\n### kernel-core\n\nThe kernel backend uses `kernel-core::wire::ExecuteRequest` for contract-checked payloads:\n\n```rust\nuse kernel_core::wire::{ExecuteRequest, StringOrInt};\n\npub struct ExecuteRequest {\n uid: i64,\n model: String,\n method: String,\n args: Vec<Value>,\n kwargs: Map<String, Value>,\n secret: Option<String>,\n svid: Option<String>,\n harakat_startup_id: Option<StringOrInt>,\n harakat_authority_version: Option<StringOrInt>,\n delegation_token: Option<String>,\n approval_grant: Option<String>,\n trace_id: Option<String>,\n}\n```\n\n### Odoo Gateway\n\nThe Odoo backend calls `erp.gateway.execute_as` via JSON-RPC:\n\n```json\n{\n \"jsonrpc\": \"2.0\",\n \"method\": \"call\",\n \"params\": {\n \"service\": \"object\",\n \"method\": \"execute_kw\",\n \"args\": [\n \"<db>\", <login_uid>, \"<password>\",\n \"erp.gateway\", \"execute_as\",\n [<uid>, \"<model>\", \"<method>\", <args>, <kwargs>],\n { \"secret\": \"<gateway secret>\", \"harakat_startup_id\": <id> }\n ]\n }\n}\n```\n\n### accounts Crate\n\nThe finance books-reconcile gate drives the real `accounts` crate through the harness machinery. The kernel selfdiff is proven offline by `tests/finance_books_selfdiff.rs` using `InMemoryKernel`.\n\n## Testing\n\n### Unit Tests\n\n```rust\n// In differ.rs\n#[test]\nfn identical_projections_are_parity() {\n let a = projection(vec![(\"@0\", json!({\"title\":\"a\",\"stage\":\"draft\"}))]);\n let b = a.clone();\n let report = diff_projections(&a, &b, \"kernel\", \"odoo\");\n assert!(report.is_empty());\n}\n```\n\n### Integration Tests\n\n```rust\n// In tests/stub_backends.rs\n#[tokio::test]\nasync fn kernel_and_odoo_stub_runs_reach_parity() {\n let kernel = KernelBackend::new(cfg);\n let odoo = OdooBackend::new(cfg);\n let kernel_run = run_script(&kernel, &script).await;\n let odoo_run = run_script(&odoo, &script).await;\n let report = diff_records(&kernel_run, &odoo_run, \"kernel\", \"odoo\");\n assert!(report.is_empty());\n}\n```\n\n## Exit Codes\n\n| Code | Meaning |\n|------|---------|\n| 0 | Parity/success |\n| 1 | Divergence |\n| 2 | Usage/config/runtime error |\n\n## Common Pitfalls\n\n1. **Archived rows**: Odoo auto-filters archived rows unless `include_archived: true` is set in the projection\n2. **ID collisions**: Odoo int IDs share a namespace; throwaway-record IDs are large to avoid collisions\n3. **Datetime comparison**: Wall clocks differ; timestamps compare by presence, not value\n4. **HTML normalization**: Odoo sanitizes HTML; void tags lose their slash\n5. **Decimal strings**: Only strings WITH a decimal point convert to numbers\n\n## Contributing\n\nWhen adding new features:\n\n1. **Backend trait**: Add methods to `Backend` trait, implement in both backends\n2. **Script format**: Update `OpScript::from_json` and runner alias resolution\n3. **Normalization**: Add rules to `normalize_value` with tests\n4. **Differ**: Extend `DiffReport` and `diff_records` for new comparison surfaces\n5. **Tests**: Add unit tests for new logic, integration tests for end-to-end\n\nSee existing scripts in `scripts/` for examples:\n- `notes-smoke.json`: Basic CRUD operations\n- `crm-parity.json`: Complex multi-model scenario with declared divergences\n- `finance-books-parity.json`: Finance gate with posting immutability tests\n- `helpdesk-parity.json`: Helpdesk ticket lifecycle\n- `wave2-ownership-parity.json`: Ownership m2m relationship tests","harakat-kernel":"# Harakat_Kernel\n\n\n\n# Harakat_Kernel\n\n## Overview\n\nThe Harakat Kernel is a Rust-based application framework designed to serve as the backend for an ERP system, replacing Odoo. It operates on a \"kernel-first\" architecture where business logic is compiled into Rust crates (Layer 1) while tenant-specific customizations are handled via interpreted data overlays (Layer 2). The workspace is structured as a monorepo containing internal crates, business applications, and ETL tools, all sharing a common dependency tree and security policy enforced via `deny.toml`.\n\n## Sub-Module Integration\n\nThe module group coordinates the lifecycle of the kernel through specialized sub-modules that enforce boundaries and provide infrastructure:\n\n* **Infrastructure & Execution:** The [Harakat_Kernel — dev](dev.md) module provisions isolated Docker containers for Postgres and OpenBao and provides cross-platform launcher scripts to execute the `kernel-server` binary. This ensures the development stack mirrors production architecture without sharing resources.\n* **Architectural Enforcement:** The [Harakat_Kernel — scripts](scripts.md) module acts as a verification gate, enforcing architectural laws such as the `sqlx` ban on tenant application data paths and validating database migrations for both fresh and existing databases.\n* **Business Logic & Crates:** The [Harakat_Kernel — apps](apps.md) and [Harakat_Kernel — crates](crates.md) sub-modules contain the compiled business logic. These components interact with core kernel state management via `kernel-core`, `kernel-storage`, and `kernel-api`.\n* **Support & Documentation:** The [Harakat_Kernel — tools](tools.md) and [Harakat_Kernel — docs](docs.md) sub-modules support the build pipeline and provide reference material for the framework.\n\n## Data Flow and Dependencies\n\nBusiness logic modules (`accounts`, `payroll`, `purchase`, `crm`) rely heavily on core kernel components for state management and access control. Key interactions include:\n\n* **State Management:** Functions such as `apply_child_writes` and `run_foreign_lifecycle` in `kernel-storage` interface with `LogicCtx` and `Mutation` models defined in `kernel-core`.\n* **Access Control:** Authorization flows derive `AccessClaims` from `kernel-authz` and `kernel-core`, ensuring tenant isolation (e.g., `one_tenant_s_rollup_never_sees_another_tenant_s_children`).\n* **API State:** Application logic queries `ApiConfig` and `ApiState` from `kernel-api` to manage configuration and runtime state.\n\n## Architecture\n\n```mermaid\ngraph TD\n subgraph Development\n A[Harakat_Kernel — dev] -->|Provisioning| B_kernel-server[\"B(kernel-server)\"]\n end\n\n subgraph Enforcement\n C[Harakat_Kernel — scripts] -->|Validation| D[Apps & Crates]\n end\n\n subgraph Core Logic\n D -->|Business Logic| E[Harakat_Kernel — apps]\n D -->|Internal Crates| F[Harakat_Kernel — crates]\n end\n\n subgraph Kernel Core\n E -->|State & Mutations| G[kernel-core]\n E -->|Storage| H[kernel-storage]\n E -->|API| I[kernel-api]\n end\n\n B -.->|Runs| D\n C -.->|Enforces| D\n```","harakat-landing-app":"# Harakat_Landing — app\n\n\n\n# Harakat_Landing Module Documentation\n\n## Overview\n\nThe `Harakat_Landing` module is a Next.js application that serves as the marketing and information hub for Harakat — an AI-powered business platform for SMEs. It provides a comprehensive landing experience with multiple pages, a custom design system, and an email submission API.\n\n### Core Responsibilities\n\n- **Marketing & Information**: Presents Harakat's value proposition, features, pricing, and company information\n- **Lead Generation**: Contact form with rate-limited email submission to Zoho\n- **Brand Consistency**: Implements a unified design system across all pages\n- **SEO & Accessibility**: Structured data, metadata, and semantic HTML\n\n---\n\n## Architecture\n\n```mermaid\ngraph TD\n A[ClientLayout] --> B[PageShell]\n A --> C[PageBlocks]\n A --> D[SmoothScroll]\n B --> E[Page Components]\n E --> F[Home Pages]\n E --> G[Company Pages]\n E --> H[Utility Pages]\n I[API Route] --> J[Email Service]\n J --> K[Zoho SMTP]\n```\n\n### Layout Hierarchy\n\n```\nClientLayout (Root)\n├── SmoothScroll\n├── PageBlocks (Transition overlay)\n└── Children (Page content)\n └── PageShell (Page wrapper)\n └── Page-specific components\n```\n\n---\n\n## Key Components\n\n### ClientLayout (`app/ClientLayout.tsx`)\n\nThe root layout that sets up global styling and shared utilities.\n\n**Responsibilities:**\n- Injects Google Fonts (Instrument Sans, Instrument Serif, Amiri, JetBrains Mono)\n- Applies CSS variables for font families\n- Mounts `SmoothScroll` and `PageBlocks` components\n- Wraps all page content\n\n**Font Variables:**\n```css\n--font-sans: Instrument Sans\n--font-display: Instrument Serif\n--font-arabic: Amiri\n--font-mono: JetBrains Mono\n```\n\n### PageShell (`components/home/page-shell.tsx`)\n\nA reusable page wrapper that provides consistent structure across all pages.\n\n**Usage Pattern:**\n```tsx\n<PageShell>\n <section className=\"lp-section\">\n <h1>Title<\/h1>\n <p>Content<\/p>\n <\/section>\n<\/PageShell>\n```\n\n### PageBlocks (`components/home/page-blocks.tsx`)\n\nA 9x9 grid overlay that animates during page transitions.\n\n**Behavior:**\n- State: `hidden` → `covering` → `revealing`\n- Uses inline animation delays for staggered cell reveal\n- Respects `prefers-reduced-motion`\n\n---\n\n## Pages\n\n### Home Page (`app/page.tsx`)\n\nThe main landing page with hero, features, outcomes, and CTA sections.\n\n**Key Sections:**\n- Hero with floating pills and dashboard mock\n- \"What we do\" cards grid\n- Outcomes carousel with animated bars\n- \"How it works\" 3D wireframe animation\n- Suite grid linking to sub-apps\n- Pricing plans\n- FAQ section\n\n### Company Pages\n\n#### About (`app/company/about/page.tsx`)\n- Mission statement and story\n- Values grid (Structure, Assistants, Plain language, Data ownership)\n- Founder profiles\n- World map visualization\n- Mural of pixel glyphs\n\n#### Careers (`app/company/careers/page.tsx`)\n- Hiring process (4-step journey)\n- Culture cards\n- \"No open roles\" honest empty state\n- Staircase hero visual\n\n#### Security (`app/company/security/page.tsx`)\n- Encryption at rest explanation\n- Human approval workflow\n- Tenant isolation diagram\n- Audit trail examples\n\n### Utility Pages\n\n| Page | Route | Purpose |\n|------|-------|---------|\n| Calculator | `/calculator` | ROI calculator for time savings |\n| Contact | `/contact` | Lead form with schema.org markup |\n| FAQ | `/faq` | Common questions with structured data |\n| Login | `/login` | Authentication entry point |\n| Pricing | `/pricing` | Tiered pricing plans |\n| Platform | `/platform` | Product demo and features |\n\n---\n\n## API Routes\n\n### POST `/api/send-email`\n\nHandles contact form submissions with rate limiting and sanitization.\n\n**Request Body:**\n```json\n{\n \"name\": \"string\",\n \"email\": \"string\",\n \"message\": \"string\",\n \"purpose\": \"string\",\n \"company\": \"string\",\n \"solution\": \"string\",\n \"teamSize\": \"string\",\n \"type\": \"contact\" | \"demo\"\n}\n```\n\n**Validation Flow:**\n```mermaid\ngraph TD\n A[POST Request] --> B{Rate Limit Check}\n B -->|Exceeded| C[429 Too Many Requests]\n B -->|OK| D{Required Fields}\n D -->|Missing| E[400 Name/Email Required]\n D -->|OK| F{Email Format}\n F -->|Invalid| G[400 Invalid Email]\n F -->|OK| H{Length Limits}\n H -->|Exceeded| I[400 Input Too Long]\n H -->|OK| J[Sanitize HTML]\n J --> K{Form Type}\n K -->|contact| L[Contact Email Template]\n K -->|demo| M[Demo Request Template]\n K -->|Other| N[400 Invalid Type]\n L --> O[Send to Zoho]\n M --> O\n O --> P[200 Success]\n```\n\n**Rate Limiting:**\n- Per-IP tracking using `x-forwarded-for` header\n- 5 requests per 15-minute window\n- In-memory Map (resets on deploy)\n\n**Email Templates:**\n- Contact form: Name, email, purpose, message\n- Demo request: Adds company, solution interest, team size\n\n**Environment Variables Required:**\n```bash\nZOHO_EMAIL\nZOHO_PASSWORD\nZOHO_RECIPIENT_EMAIL\n```\n\n---\n\n## Design System\n\n### Color Palette\n\n```css\n--lp-paper: #f4f1e8\n--lp-ink: #0a2e1e\n--lp-jade: #359167\n--lp-oasis: #5dad89\n--lp-saffron: #d2c49e\n--lp-line: #d7ddcb\n```\n\n### Typography\n\n| Class | Font | Use Case |\n|-------|------|----------|\n| `lp-display` | Instrument Serif | Headings, display text |\n| `lp-sans` | Instrument Sans | Body, UI, labels |\n| `lp-arabic` | Amiri | Arabic text, tashkeel |\n| `lp-mono` | JetBrains Mono | Code, data displays |\n\n### Component Classes\n\n**Buttons:**\n- `.lp-btn` — Primary (ink background)\n- `.lp-btn--outline` — Secondary (border only)\n- `.lp-btn--ghost-dark` — Dark mode ghost\n\n**Sections:**\n- `.lp-section` — Standard section with padding\n- `.lp-phero` — Split page hero (text + visual)\n- `.lp-chero` — Centered hero on tinted blocks\n- `.lp-statement` — Full-width serif statement\n\n**Cards:**\n- `.lp-card` — Feature cards with icons\n- `.lp-values__card` — Value proposition cards\n- `.lp-plan` — Pricing plan cards\n\n---\n\n## Styling Approach\n\n### CSS Organization\n\n1. **`app/globals.css`** — Tailwind imports, CSS variables, base animations\n2. **`app/home.css`** — Scoped `.lp` design system styles\n3. **Inline styles** — Used for pixel-perfect positioning (hero visuals, pixel glyphs)\n\n### Animation System\n\n**Keyframes:**\n- `lp-rise` — Fade up entrance\n- `lp-dropin` — Header drop-in\n- `lp-float` — Floating pill animation\n- `lp-marquee` — Infinite scrolling text\n- `lp-fill` — Progress bar fill\n\n**Reduced Motion:**\nAll animations respect `prefers-reduced-motion` and disable gracefully.\n\n---\n\n## Data & Schema\n\n### Structured Data\n\nPages inject JSON-LD for SEO:\n\n**Contact Page:**\n```json\n{\n \"@type\": \"ContactPage\",\n \"mainEntity\": {\n \"@type\": \"Organization\",\n \"contactPoint\": {\n \"@type\": \"ContactPoint\",\n \"email\": \"talktous@harakat.tech\"\n }\n }\n}\n```\n\n**FAQ Page:**\n```json\n{\n \"@type\": \"FAQPage\",\n \"mainEntity\": [\n {\n \"@type\": \"Question\",\n \"name\": \"Question\",\n \"acceptedAnswer\": {\n \"@type\": \"Answer\",\n \"text\": \"Answer\"\n }\n }\n ]\n}\n```\n\n### Metadata\n\nEach page defines:\n- `title` — Page title\n- `description` — SEO description\n- `canonical` — Canonical URL\n- `openGraph` — Social sharing metadata\n- `keywords` — Search keywords\n\n---\n\n## Environment Variables\n\n```bash\n# Email Service\nZOHO_EMAIL\nZOHO_PASSWORD\nZOHO_RECIPIENT_EMAIL\n\n# Next.js\nNEXT_PUBLIC_APP_URL\n```\n\n---\n\n## Contributing Guidelines\n\n### Adding New Pages\n\n1. Create `app/[route]/page.tsx`\n2. Wrap content in `<PageShell>`\n3. Define metadata at top of file\n4. Add to navigation in `HomeHeader` if needed\n\n### Adding New Components\n\n1. Place in `components/home/`\n2. Use `.lp` prefix for design system classes\n3. Respect responsive breakpoints (900px, 1024px)\n4. Test with `prefers-reduced-motion`\n\n### API Changes\n\n1. Update rate limit constants in `api/send-email/route.ts`\n2. Test with Postman or curl\n3. Verify email delivery to Zoho\n4. Check error responses (400, 429, 500)\n\n### Design System Updates\n\n1. Modify `app/home.css` for new colors/typography\n2. Update `app/globals.css` for CSS variables\n3. Ensure dark mode compatibility\n4. Test across all page templates\n\n---\n\n## Performance Considerations\n\n- **Font Loading**: Uses `next/font/google` for optimized font delivery\n- **Image Optimization**: Next.js Image component for all images\n- **Edge Runtime**: OG icons run on edge for faster generation\n- **Code Splitting**: Pages are individually rendered\n- **Lazy Loading**: Components like `Reveal` animate on scroll\n\n---\n\n## Testing Checklist\n\n- [ ] All pages render without console errors\n- [ ] Contact form submits successfully\n- [ ] Rate limiting triggers at 6th request\n- [ ] Animations respect reduced motion preference\n- [ ] Mobile menu works on all pages\n- [ ] SEO metadata appears in page source\n- [ ] Structured data validates in Rich Results Test\n- [ ] Dark mode toggles correctly\n- [ ] All links navigate to correct routes\n\n---\n\n## Related Modules\n\n- `components/home/` — Reusable page components\n- `components/home/pixel/` — Pixel glyph icons and animations\n- `components/home/calculator/` — ROI calculator logic\n- `components/home/contact-form-lp/` — Form validation and submission\n\n---\n\n## Known Limitations\n\n1. **Rate Limiting**: In-memory only, resets on deploy (not production-ready for high traffic)\n2. **Email Service**: Zoho SMTP requires valid credentials\n3. **Pixel Positioning**: Some hero visuals use inline styles for precise positioning\n4. **Animation Complexity**: 3D wireframe animations may impact performance on low-end devices\n\n---\n\n## Quick Reference\n\n| Task | File |\n|------|------|\n| Add page | `app/[route]/page.tsx` |\n| Add component | `components/home/[name].tsx` |\n| Change colors | `app/home.css` |\n| Update fonts | `app/ClientLayout.tsx` |\n| Email config | `app/api/send-email/route.ts` |\n| Global styles | `app/globals.css` |\n| Navigation | `components/home/home-header.tsx` |","harakat-landing-components":"# Harakat_Landing — components\n\n\n\n# Harakat_Landing — Components Module\n\n## Overview\n\nThe `Harakat_Landing/components` module contains all React components for Harakat's marketing and landing pages. It implements a pixel-inspired design system with heavy use of scroll-driven animations, custom SVG glyphs, and Next.js routing patterns.\n\n## Purpose\n\nThis module provides:\n- **Marketing page components** (Hero, Calculator, HowItWorks, PlatformStory, etc.)\n- **Shared UI components** (PixelGlyph, Arrow, Reveal, etc.)\n- **Page wrappers** (PageShell, PageBlocks)\n- **Animation utilities** (SmoothScroll, useDimensions)\n- **Radix UI wrappers** (Accordion, AlertDialog, Badge, etc.)\n\n## Architecture\n\n```mermaid\ngraph TD\n A[PageShell] --> B[HomeHeader]\n A --> C[HomeFooter]\n A --> D[Main Content]\n D --> E[Hero]\n D --> F[HowItWorks]\n D --> G[PlatformStory]\n D --> H[Calculator]\n D --> I[Outcomes]\n D --> J[Security]\n E --> K[HeroPills]\n E --> L[DashboardMock]\n F --> M[Panel Components]\n G --> N[PillarVisual]\n H --> O[Calculator Fields]\n K --> P[PixelGlyph]\n M --> P\n N --> P\n O --> P\n B --> Q[Dropdown]\n Q --> P\n D --> R[PageBlocks]\n R --> S[9x9 Grid]\n```\n\n## Key Components\n\n### Hero & HeroPills (`hero.tsx`, `hero-pills.tsx`, `hero-data.ts`)\n\nThe hero section features an animated \"pills\" layer that floats organically and converges into the dashboard mock on scroll.\n\n**Key behaviors:**\n- Idle state: CSS float + mouse parallax\n- Scroll state: Pills accelerate into the dashboard mock\n- Reduced motion: Animations disabled via `prefers-reduced-motion`\n\n```typescript\n// HeroPills animation loop\nconst loop = (now: number) => {\n raf = requestAnimationFrame(loop);\n // Smooth mouse tracking\n mx += (mxT - mx) * 0.06;\n my += (myT - my) * 0.06;\n // Scroll progress\n const pT = Math.min(Math.max(window.scrollY / FEED_DISTANCE, 0), 1);\n // Accelerate into mock\n const pe = pS * pS;\n // Transform each pill\n el.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${s})`;\n};\n```\n\n**Constants:**\n- `FEED_DISTANCE = 520px` — scroll distance for pill convergence\n- `PARALLAX = [9, 6, 12, 7, 10, 5, 4, 8, 6, 5]` — per-element depth\n- `INTRO_DELAY = 380ms` — pills join after headline rises\n- `INTRO_STAGGER = 90ms` — stagger between pills\n\n### Calculator (`calculator.tsx`)\n\nLive ROI calculator with sliders for team size, costs, hours, and automation assumptions.\n\n**State:**\n```typescript\nconst [team, setTeam] = useState(12);\nconst [cost, setCost] = useState(1500);\nconst [tools, setTools] = useState(250);\nconst [hours, setHours] = useState<Record<string, number>>(...);\nconst [auto, setAuto] = useState<Record<string, number>>(...);\n```\n\n**Calculations:**\n```typescript\nconst hourly = cost / WORK_HOURS_PER_MONTH; // 1500 / 173\nconst saved = team * hours[c.key] * (auto[c.key] / 100) * WEEKS_PER_MONTH;\nconst moneyYear = hoursMonthly * hourly * 12 + toolSavingsYear;\nconst fte = hoursMonthly / WORK_HOURS_PER_MONTH;\n```\n\n**Constants:**\n- `WEEKS_PER_MONTH = 4.33`\n- `WORK_HOURS_PER_MONTH = 173`\n\n### HowItWorks (`how-it-works.tsx`)\n\nPinned scroll sequence with four panels (Data, Knowledge, Assistants, Governance).\n\n**Animation flow:**\n1. Intro title fades in\n2. Each panel slides in with staggered blocks\n3. Illustrations fly past the camera\n4. Finale: \"The harakat platform\" with CTA\n\n**Panel window calculation:**\n```typescript\nconst panelWindow = (i: number): [number, number] => {\n const s = INTRO_END + 0.02 + i * PANEL_SPAN;\n return [s, s + PANEL_SPAN];\n};\n```\n\n**Motion values:**\n- `opacity` — panel fade in/out\n- `y` — card vertical position\n- `x` — block exit animation\n- `cardX` — card lateral hand-off\n- `cardZ` — depth z-axis\n\n### PlatformStory (`platform-story.tsx`)\n\nFull-bleed pinned slider with left copy strip and right visual strip.\n\n**Animation mechanics:**\n```typescript\nconst shape = (p: number) => {\n const i = Math.floor(p);\n const f = p - i;\n const t = Math.min(Math.max((f - DWELL) / (1 - 2 * DWELL), 0), 1);\n return i + t * t * (3 - 2 * t);\n};\n```\n\n**Easing:**\n- `EASE_LEFT = 0.16` — faster left strip\n- `EASE_RIGHT = 0.075` — slower right strip (delayed arrival)\n- `DWELL = 0.15` — magnetic snap fraction\n\n**Responsive:**\n- Desktop (≥900px): Pinned strip\n- Mobile: Stacked layout\n\n### Pixel System (`pixel.tsx`)\n\nCustom 5x5 pixel SVG glyphs used throughout the site.\n\n**Glyphs:**\n- Brand: `logo` (Harakat loop mark)\n- Functional: `doc`, `folder`, `mail`, `calendar`, `search`, `key`, `shield`, etc.\n- Platform: `crm`, `project`, `payroll`, `finance`, `inventory`, etc.\n\n**Rendering:**\n```typescript\nexport function PixelGlyph({ name, size = 20, color = \"currentColor\" }) {\n if (name === \"logo\") return <HarakatLoop size={size} color={color} />;\n const m = GLYPHS[name] ?? GLYPHS.cube;\n const cell = size / 5;\n return (\n <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>\n {m.flatMap((row, y) =>\n row.map((v, x) =>\n v ? (\n <rect\n key={`${x}-${y}`}\n x={x * cell}\n y={y * cell}\n width={cell + 0.3}\n height={cell + 0.3}\n fill={color}\n />\n ) : null\n )\n )}\n <\/svg>\n );\n}\n```\n\n**Harakat Loop Mark:**\n```typescript\nconst LOOP_PATH = \"M 1.68 18.56 L 1.4 17.41 ...\"; // 24x24 viewBox\n```\n\n### PageBlocks (`page-blocks.tsx`)\n\n9x9 pixel transition effect for page navigation.\n\n**Behavior:**\n- Server renders fully covered (ink blocks + logo on center)\n- Entering: blocks vanish one by one in random order; center logo last\n- Exiting: center logo lands first, rest fill in randomly, then `router.push`\n\n**Constants:**\n- `N = 9` — grid size\n- `CELLS = 81` — total cells\n- `CENTER = 40` — center cell index\n- `SPREAD = 430ms` — flip duration\n- `LOGO_LAG = 150ms` — logo outlives rest\n\n### ContactFormLp (`contact-form-lp.tsx`)\n\nContact form with `/api/send-email` integration.\n\n**Form state:**\n```typescript\nconst [form, setForm] = useState({ name: \"\", email: \"\", purpose: \"\", message: \"\" });\nconst [busy, setBusy] = useState(false);\nconst [status, setStatus] = useState<{ ok: boolean; msg: string } | null>(null);\n```\n\n**Submit flow:**\n```typescript\nconst submit = async (e: React.FormEvent) => {\n e.preventDefault();\n setBusy(true);\n setStatus(null);\n try {\n const r = await fetch(\"/api/send-email\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ ...form, type: \"contact\" }),\n });\n if (r.ok) {\n setStatus({ ok: true, msg: \"Message sent...\" });\n setForm({ name: \"\", email: \"\", purpose: \"\", message: \"\" });\n } else {\n const d = await r.json().catch(() => ({}));\n setStatus({ ok: false, msg: d.error || \"Could not send...\" });\n }\n } catch {\n setStatus({ ok: false, msg: \"Could not send...\" });\n } finally {\n setBusy(false);\n }\n};\n```\n\n### HomeHeader (`home-header.tsx`)\n\nNavigation header with dropdown menus and mobile menu.\n\n**Scroll behavior:**\n```typescript\nconst onScroll = () => {\n const y = window.scrollY;\n const delta = y - lastY.current;\n if (y < 120 || delta < -4) setHidden(false);\n else if (delta > 4) setHidden(true);\n lastY.current = y;\n};\n```\n\n**Dropdown items:**\n- Pricing: Plans, Do the math\n- Company: About, Careers, Security, Contact us\n\n### HomeFooter (`home-footer.tsx`)\n\nFooter with six link groups: Platform, The Suite, Company, Connect, Get started, Legal.\n\n**External links:**\n- `talktous@harakat.tech` (mailto)\n- LinkedIn, Instagram (external)\n\n## Animation Patterns\n\n### useInView Hook\n\n```typescript\nexport function useInView<T extends HTMLElement>(threshold = 0.25) {\n const ref = useRef<T | null>(null);\n const [inView, setInView] = useState(false);\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n const io = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) {\n setInView(true);\n io.disconnect();\n }\n },\n { threshold }\n );\n io.observe(el);\n return () => io.disconnect();\n }, [threshold]);\n\n return { ref, inView };\n}\n```\n\n### Reveal Component\n\n```typescript\nexport function Reveal({ children, className = \"\", delay = 0 }) {\n const { ref, inView } = useInView<HTMLDivElement>(0.2);\n return (\n <div\n ref={ref}\n className={`lp-reveal ${className}`}\n style={delay ? { transitionDelay: `${delay}ms` } : undefined}\n {...(inView ? { \"data-in\": \"\" } : {})}\n >\n {children}\n <\/div>\n );\n}\n```\n\n### SmoothScroll (`smooth-scroll.tsx`)\n\nLenis-based inertial scrolling with hash navigation support.\n\n```typescript\nconst lenis = new Lenis({\n lerp: 0.11,\n wheelMultiplier: 1,\n touchMultiplier: 1.4,\n});\n\nconst onHash = () => {\n const h = window.location.hash;\n if (!h) return;\n const el = document.querySelector(h);\n if (el) lenis.scrollTo(el as HTMLElement, { offset: -90 });\n};\n```\n\n## UI Component Wrappers\n\nThe module includes Radix UI component wrappers with consistent styling:\n\n| Component | File | Usage |\n|-----------|------|-------|\n| Accordion | `ui/accordion.tsx` | Collapsible sections |\n| AlertDialog | `ui/alert-dialog.tsx` | Modal dialogs |\n| Alert | `ui/alert.tsx` | Warning/info messages |\n| Avatar | `ui/avatar.tsx` | User avatars |\n| Badge | `ui/badge.tsx` | Status labels |\n| Breadcrumb | `ui/breadcrumb.tsx` | Navigation trails |\n| Button | `ui/button.tsx` | Action buttons |\n| Card | `ui/card.tsx` | Content cards |\n| Carousel | `ui/carousel.tsx` | Image sliders |\n| Command | `ui/command.tsx` | Command palette |\n| Dialog | `ui/dialog.tsx` | Modal dialogs |\n| Drawer | `ui/drawer.tsx` | Slide-out panels |\n| Form | `ui/form.tsx` | Form validation |\n| Input | `ui/input.tsx` | Text inputs |\n| Label | `ui/label.tsx` | Form labels |\n| Separator | `ui/separator.tsx` | Visual dividers |\n| Sheet | `ui/sheet.tsx` | Mobile sheets |\n| Skeleton | `ui/skeleton.tsx` | Loading placeholders |\n| Sonner | `ui/sonner.tsx` | Toast notifications |\n| Sidebar | `ui/sidebar.tsx` | Navigation sidebar |\n| Toast | `ui/toast.tsx` | Toast messages |\n| Tooltip | `ui/tooltip.tsx` | Hover tooltips |\n\n## Hooks\n\n### useDimensions (`hooks/use-debounced-dimensions.ts`)\n\nDebounced resize hook for responsive animations.\n\n```typescript\nexport function useDimensions(ref: RefObject<HTMLElement | SVGElement>): Dimensions {\n const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 });\n\n useEffect(() => {\n let timeoutId: NodeJS.Timeout;\n\n const updateDimensions = () => {\n if (ref.current) {\n const rect = ref.current.getBoundingClientRect();\n if (rect) {\n setDimensions({ width: rect.width, height: rect.height });\n }\n }\n };\n\n const debouncedUpdateDimensions = () => {\n clearTimeout(timeoutId);\n timeoutId = setTimeout(updateDimensions, 250);\n };\n\n if (typeof window !== \"undefined\") {\n updateDimensions();\n window.addEventListener(\"resize\", debouncedUpdateDimensions);\n }\n\n return () => {\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"resize\", debouncedUpdateDimensions);\n }\n clearTimeout(timeoutId);\n };\n }, [ref]);\n\n return dimensions;\n}\n```\n\n## Integration with Codebase\n\n### Incoming Calls (Pages → Components)\n\n```\nHomePage → Hero, HowItWorks, Outcomes, HomeHeader, HomeFooter, Marquee\nCalculatorPage → Calculator, PageShell\nPlatformPage → PlatformStory, PheroLive, Marquee, PixelGlyph\nContactPage → ContactFormLp, PageShell\nPricingPage → PixelGlyph, Arrow\nLoginPage → PixelGlyph, Arrow\n```\n\n### Outgoing Calls (Components → Components)\n\n```\nHero → HeroPills, DashboardMock\nHeroPills → Pill\nHowItWorks → Panel, Arrow\nPlatformStory → PillarVisual, SlideCopy\nCalculator → Arrow, fmt\nContactFormLp → Arrow\nHomeHeader → Dropdown, PixelGlyph\nSuiteGrid → PixelGlyph, Reveal, Arrow\nWhatWeDo → PixelGlyph, Reveal\nSecurity → Reveal, PixelGlyph\nSock → Reveal, Arrow\nPageBlocks → PixelGlyph\n```\n\n### External Dependencies\n\n- **Next.js**: `useRouter`, `usePathname`, `Link`\n- **Framer Motion**: `motion`, `useScroll`, `useTransform`, `useMotionValueEvent`\n- **Lenis**: Smooth scrolling library\n- **Radix UI**: Primitives for UI components\n- **Lucide React**: Icon components (via `lucide-react`)\n\n## Design System\n\n### Color Variables\n\n```css\n--lp-jade, --lp-jade-soft, --lp-jade-deep\n--lp-saffron, --lp-saffron-soft\n--lp-coral, --lp-coral-soft\n--lp-peri, --lp-peri-soft\n--lp-ink, --lp-ink-faint, --lp-ink-soft\n```\n\n### Typography\n\n- `lp-display` — Hero headings\n- `lp-h1`, `lp-h2`, `lp-h3` — Section headings\n- `lp-sub` — Subheadings\n- `lp-label` — Small labels\n\n### Spacing\n\n- `lp-container` — Max-width wrapper\n- `lp-section` — Section wrapper\n- `lp-calc`, `lp-form`, `lp-footer` — Component-specific containers\n\n## Accessibility\n\n- **Reduced Motion**: All animations respect `prefers-reduced-motion`\n- **ARIA Labels**: Form fields, buttons, and interactive elements\n- **Keyboard Navigation**: Dropdowns, modals, and carousels\n- **Focus Management**: Dialogs and sheets manage focus traps\n\n## Performance Considerations\n\n1. **Animation Optimization**:\n - `requestAnimationFrame` for smooth loops\n - `transform3d` for GPU acceleration\n - `will-change` for animated elements\n\n2. **Lazy Loading**:\n - Client components for animations\n - Server components for static content\n\n3. **Debouncing**:\n - Resize events debounced to 250ms\n - Scroll events use `requestAnimationFrame`\n\n4. **Memoization**:\n - `useMemo` for computed values\n - `useCallback` for event handlers\n\n## Contributing\n\n### Adding New Components\n\n1. Create component in `components/home/` or `components/ui/`\n2. Use `PixelGlyph` for icons\n3. Wrap animations with `Reveal`\n4. Respect `prefers-reduced-motion`\n5. Add ARIA labels for accessibility\n\n### Animation Guidelines\n\n- Use `framer-motion` for scroll-driven animations\n- Use `requestAnimationFrame` for custom loops\n- Keep animations under 600ms\n- Stagger elements for visual interest\n\n### Testing\n\n- Test on mobile (≤900px)\n- Test with reduced motion\n- Test keyboard navigation\n- Test with slow network (lazy loading)\n\n## Known Issues\n\n- None documented\n- All animations respect reduced motion preference\n- All external links use `rel=\"noopener noreferrer\"` where appropriate","harakat-landing-harakat-landing":"# Harakat_Landing — Harakat_Landing\n\n\n\n# Harakat_Landing\n\n## Overview\nThe `Harakat_Landing` module is a Next.js 14 standalone application serving as the primary landing page for the Harakat project. It utilizes a multi-stage Docker build process to optimize production performance and includes server-side capabilities for contact form handling via Zoho SMTP.\n\n## Architecture & Build Process\nThe application is built using **Next.js 14.2.25** with **pnpm** as the package manager. The build pipeline is defined in `Dockerfile` using a multi-stage approach to minimize the final image size.\n\n### Build Stages\n1. **Base**: Installs Node.js 20 and activates `pnpm`.\n2. **Deps**: Installs production dependencies using `pnpm install --frozen-lockfile`.\n3. **Build**: Copies source code and `.env` files, then runs `pnpm build`. This generates the `.next/standalone` output.\n4. **Runner**: Copies the standalone build and static assets into a minimal Node.js image. It runs as a non-root user (`nextjs`) and exposes port 3000.\n\n### Execution Flow\nThe module does not contain internal logic execution flows detectable via static analysis; it functions primarily as a build configuration and entry point. The runtime behavior is driven by the Next.js standalone server and a custom `server.js` entry point for API/SMTP handling.\n\n```mermaid\nflowchart TD\n A[Source Code] -->|pnpm install| B[Dependencies]\n B -->|pnpm build| C[.next/standalone]\n C -->|COPY| D[Docker Runner Image]\n D -->|CMD| E[node server.js]\n E -->|Serve| F[Port 3000]\n E -->|Handle| G[Contact Form / SMTP]\n```\n\n## Configuration\n\n### Next.js Configuration (`next.config.mjs`)\nThe `next.config.mjs` file configures the application for production deployment and legacy routing.\n\n* **Output Mode**: `output: 'standalone'` enables the standalone output format required for the Docker runner.\n* **Image Optimization**: `images.unoptimized: true` disables automatic image optimization.\n* **Redirects**: Temporary redirects are configured for legacy paths:\n * `/about` → `/company/about`\n * `/experience` → `/contact`\n * `/solutions` → `/platform`\n* **Security Headers**: Configures `Content-Type` for `/.well-known/apple-app-site-association` to `application/json` to support iOS passkeys and universal links.\n\n### Environment Variables\n* **Build Time**: `NEXT_PUBLIC_*` variables are read automatically by Next.js during the build process.\n* **Runtime**: The Dockerfile comments indicate that Zoho SMTP credentials are passed as runtime environment variables (not prefixed with `NEXT_PUBLIC_`) to the container.\n\n## Dependencies\nThe module relies on a specific set of libraries for UI, animation, and backend functionality.\n\n### UI & Styling\n* **Tailwind CSS v4**: Configured via `@tailwindcss/postcss` with `new-york` style from Shadcn UI.\n* **Shadcn UI**: Uses Radix UI primitives (e.g., `@radix-ui/react-dialog`, `@radix-ui/react-select`).\n* **Icons**: `lucide-react`.\n\n### Animation & 3D\n* **GSAP**: `@gsap/react` and `gsap` for animations.\n* **Framer Motion**: `framer-motion` and `motion`.\n* **Three.js**: `@react-three/fiber` and `three` for 3D elements.\n\n### Backend & Forms\n* **Nodemailer**: `nodemailer` for email handling.\n* **Zod**: `zod` for schema validation.\n* **React Hook Form**: `react-hook-form` and `@hookform/resolvers` for form management.\n\n### Utilities\n* **Date Handling**: `date-fns`, `react-day-picker`.\n* **Charts**: `recharts`.\n* **Analytics**: `@vercel/analytics`.\n\n## Runtime Behavior\nIn production, the application runs as a non-root user (`nextjs`) within the Docker container.\n\n1. **Static Assets**: Served from `/app/.next/static`.\n2. **Standalone App**: Served from `/app/.next/standalone`.\n3. **API/SMTP**: The `server.js` entry point handles server-side requests, specifically the contact form submission which routes to Zoho SMTP.\n4. **Port**: Listens on `0.0.0.0:3000`.\n\n## Development Scripts\nThe `package.json` defines the following scripts for local development:\n\n* `pnpm dev`: Starts the development server on port 3003.\n* `pnpm build`: Builds the production standalone output.\n* `pnpm start`: Starts the production server on port 3003.\n* `pnpm lint`: Runs ESLint.\n\n## Security Considerations\n* **Apple App Site Association**: Explicitly configured to serve JSON content type to ensure iOS devices correctly associate the domain with the app.\n* **User Permissions**: The production runner uses a dedicated system user (`nodejs`) to limit file system access.\n* **Environment Isolation**: Sensitive SMTP credentials are excluded from the build output and injected at runtime via Docker environment variables.","harakat-landing-hooks":"# Harakat_Landing — hooks\n\n\n\n# Harakat_Landing — Hooks\n\nThis module provides custom React hooks for managing UI state within the `Harakat_Landing` application. It includes functionality for responsive design detection (`use-mobile.ts`) and a client-side toast notification system (`use-toast.ts`).\n\n## Overview\n\nThe module is designed to handle lightweight, client-side state management without external state libraries. It relies on React's `useState` and `useEffect` hooks, along with a custom reducer pattern for toast notifications.\n\n## Mobile Detection\n\nThe `use-mobile.ts` file provides a hook to detect if the current viewport width is below a specific breakpoint.\n\n### `useIsMobile`\n\nA custom hook that returns a boolean indicating whether the device is mobile.\n\n**Signature:**\n```typescript\nexport function useIsMobile(): boolean\n```\n\n**Behavior:**\n- **Breakpoint:** Uses `MOBILE_BREAKPOINT` set to `768`.\n- **Initial State:** Returns `false` until the `useEffect` runs and sets the initial value based on `window.innerWidth`.\n- **Updates:** Listens to `window.matchMedia` changes to update the state dynamically when the viewport resizes.\n- **Cleanup:** Removes the event listener on unmount.\n\n**Usage:**\n```typescript\nconst isMobile = useIsMobile();\n```\n\n## Toast Notification System\n\nThe `use-toast.ts` file implements a client-side toast notification system inspired by `react-hot-toast`. It manages the state of active toasts, allowing components to trigger notifications and dismiss them programmatically.\n\n### Key Components\n\n#### `toast` Function\nThe primary API for creating a toast notification.\n\n**Signature:**\n```typescript\nfunction toast(props: Toast)\n```\n\n**Returns:**\nAn object containing methods to control the toast:\n- `id`: Unique identifier for the toast.\n- `dismiss`: Function to dismiss the specific toast.\n- `update`: Function to update the toast properties.\n\n**Execution Flow:**\n1. Calls `genId()` to generate a unique ID.\n2. Dispatches an `ADD_TOAST` action via `dispatch`.\n3. Returns the control object.\n\n#### `useToast` Hook\nThe consumer hook for accessing the toast state and actions.\n\n**Signature:**\n```typescript\nfunction useToast()\n```\n\n**Returns:**\n- `toasts`: Array of active toast objects.\n- `toast`: The `toast` function described above.\n- `dismiss`: Function to dismiss a toast by ID or all toasts.\n\n**State Management:**\n- Uses a custom listener pattern (`listeners` array) to propagate state changes from the `memoryState` to the hook.\n- Updates `memoryState` via the `dispatch` function.\n\n### State Management Architecture\n\nThe toast system uses a reducer pattern to manage state transitions.\n\n**Constants:**\n- `TOAST_LIMIT`: Set to `1`. Only one toast is allowed to be visible at a time.\n- `TOAST_REMOVE_DELAY`: Set to `1000000` (1000 seconds). Defines the timeout for auto-removal after dismissal.\n\n**Actions:**\n- `ADD_TOAST`: Adds a new toast to the state, respecting the `TOAST_LIMIT`.\n- `UPDATE_TOAST`: Updates properties of an existing toast.\n- `DISMISS_TOAST`: Closes the toast and schedules removal via `addToRemoveQueue`.\n- `REMOVE_TOAST`: Removes the toast from the state array.\n\n**Reducer Logic:**\n- `reducer` handles the switch statement for actions.\n- In the `DISMISS_TOAST` case, it calls `addToRemoveQueue` to schedule the removal timeout.\n- `addToRemoveQueue` ensures only one timeout exists per toast ID to prevent duplicate removals.\n\n## Toast Lifecycle\n\nThe following diagram illustrates the internal execution flow of a toast notification from creation to removal.\n\n```mermaid\ngraph TD\n A[toast() API Call] --> B[genId]\n B --> C[dispatch ADD_TOAST]\n C --> D[reducer]\n D --> E[State Update]\n \n F[dismiss() API Call] --> G[dispatch DISMISS_TOAST]\n G --> H[addToRemoveQueue]\n H --> I[setTimeout]\n I --> J[dispatch REMOVE_TOAST]\n \n E -.-> K[Listeners Update]\n J -.-> K\n```\n\n## Integration\n\n### Dependencies\n- **React:** `react` (for `useState`, `useEffect`).\n- **UI Components:** `@/components/ui/toast` (for `ToastProps`, `ToastActionElement`).\n\n### Client-Side Requirement\nThe `use-toast.ts` file is marked with `'use client'`. This hook and its associated state cannot be used in server components. Ensure the toast provider (imported from `@/components/ui/toast`) is rendered in the root client component to render the actual toast UI.\n\n### Configuration\nTo modify the behavior of the mobile detection or toast system, update the constants at the top of their respective files:\n- `MOBILE_BREAKPOINT` in `use-mobile.ts`.\n- `TOAST_LIMIT` and `TOAST_REMOVE_DELAY` in `use-toast.ts`.","harakat-landing-lib":"# Harakat_Landing — lib\n\n\n\n# Harakat_Landing — lib\n\n## Overview\n\nThe `lib` module within `Harakat_Landing` provides a centralized utility for managing CSS class names. It abstracts the complexity of merging multiple class sources (strings, arrays, objects) into a single, conflict-free string. This is essential for maintaining clean, conditional styling in the application's React components.\n\n## API Reference\n\n### `cn`\n\nA variadic utility function for merging class names.\n\n**Signature**\n```typescript\nexport function cn(...inputs: ClassValue[]): string\n```\n\n**Parameters**\n| Name | Type | Description |\n| :--- | :--- | :--- |\n| `...inputs` | `ClassValue[]` | Variable arguments representing class values. Can be strings, arrays of strings, or objects with class keys. |\n\n**Returns**\n| Type | Description |\n| :--- | :--- |\n| `string` | A single string containing the merged, deduplicated CSS class names. |\n\n**Behavior**\nThe function accepts any number of arguments. It first processes the inputs through `clsx` to handle conditional logic (e.g., `className={cn('base', isActive && 'active')}`), then passes the result to `tailwind-merge` to resolve conflicts between Tailwind utility classes.\n\n## Implementation Details\n\nThe module consists of a single exported function, `cn`. It does not contain internal logic branches or state management. Its execution flow is a direct composition of two external library calls.\n\n**Execution Flow**\n1. **Input Collection:** The function collects all arguments passed via the spread operator.\n2. **Conditional Merging:** `clsx` processes the inputs to handle boolean logic and array flattening.\n3. **Conflict Resolution:** `tailwind-merge` processes the result to ensure Tailwind utility classes do not override each other (e.g., `p-4` vs `p-8`).\n4. **Output:** Returns the final merged string.\n\n```mermaid\nflowchart LR\n A[Inputs] --> B_clsx[\"B(clsx)\"]\n B --> C[twMerge]\n C --> D[Final String]\n```\n\n## Dependencies\n\nThis module relies on two external packages for its functionality:\n\n1. **`clsx`**: Used for merging class names with conditional logic support.\n2. **`tailwind-merge`**: Used to resolve conflicts between Tailwind CSS utility classes.\n\n## Integration\n\nThe `cn` function is designed to be imported and used throughout the `Harakat_Landing` application. It replaces standard `className` prop usage to ensure consistent styling behavior across components.\n\n**Example Usage**\n```typescript\nimport { cn } from '@/lib/utils'\n\nfunction Button({ variant = 'primary', children }) {\n return (\n <button className={cn(\n 'px-4 py-2 rounded',\n variant === 'primary' && 'bg-blue-500 text-white',\n variant === 'secondary' && 'bg-gray-500 text-gray-100'\n )}>\n {children}\n <\/button>\n )\n}\n```\n\n## Module Statistics\n\n* **Internal Calls:** None\n* **Outgoing Calls:** None (Directly calls external libraries only)\n* **Incoming Calls:** None (Static analysis indicates no internal module dependencies)\n* **Execution Flows:** Linear (Input → `clsx` → `tailwind-merge` → Output)","harakat-landing-public":"# Harakat_Landing — public\n\n\n\n# Harakat_Landing — public\n\n## Overview\nThe `Harakat_Landing — public` module serves as the static asset and configuration layer for the Harakat Solutions landing page. It contains no executable logic, server-side functions, or database interactions. Its primary responsibility is to provide structured metadata, Progressive Web App (PWA) configuration, and static content required for the frontend to render the public-facing interface.\n\nThis module is designed for high performance and SEO optimization, ensuring the browser can immediately access necessary configuration and schema data without server-side processing delays.\n\n## File Inventory\n\n### `about.txt`\n**Purpose:** Static content source for company information and product descriptions.\n**Format:** Plain text / Markdown.\n**Usage:**\n- Serves as the source of truth for company overview, leadership, and product details.\n- Typically processed by the build pipeline or CMS to generate HTML content blocks.\n- Contains key differentiators, search query optimization, and integration capabilities.\n\n**Key Content Sections:**\n- **Company Overview:** Harakat Solutions, AI Business Intelligence Platform.\n- **Core Products:**\n - `Harakat@Offices`: Operations Management, Workflow Automation.\n - `Harakat@Marketing`: Marketing Automation, Campaign Management.\n - `Harakat@Finance`: Financial Management, Forecasting.\n- **Leadership:** Ahmad Yasin (Business Development), Ahmad Tarek (Technical).\n- **Contact:** `talktous@harakat.tech`, `+962 7 7932 2179`.\n\n### `ai-context.json`\n**Purpose:** JSON-LD structured data for SEO and AI engine indexing.\n**Format:** JSON.\n**Usage:**\n- Embedded in the `<head>` of the main HTML document.\n- Defines the `Organization`, `WebSite`, `SoftwareApplication`, and `Product` entities.\n- Enables rich snippets in search results (e.g., star ratings, pricing, descriptions).\n\n**Schema Entities:**\n- **Organization:** `https://harakat.tech/#organization`\n - `name`: \"Harakat Solutions\"\n - `foundingDate`: \"2024\"\n - `foundingLocation`: Amman, Jordan\n - `serviceArea`: GeoCircle (Amman, 20,000,000m radius)\n- **WebSite:** `https://harakat.tech/#website`\n - `potentialAction`: SearchAction targeting `https://harakat.tech/?s={search_term_string}`\n- **SoftwareApplication:** `https://harakat.tech/#software`\n - `applicationCategory`: \"BusinessApplication\"\n - `offers`: AggregateOffer (Currency: USD)\n- **Products:**\n - `Harakat@Marketing`: `https://harakat.tech/solutions/marketing#product` (Rating: 4.8)\n - `Harakat@Finance`: `https://harakat.tech/solutions/finance#product` (Rating: 4.9)\n\n### `manifest.json`\n**Purpose:** PWA (Progressive Web App) configuration.\n**Format:** JSON.\n**Usage:**\n- Allows the site to be installed on mobile devices as a standalone app.\n- Defines visual identity and behavior when installed.\n\n**Configuration Details:**\n- `name`: \"Harakat - The Intelligent Workspace for Offices & Enterprises\"\n- `short_name`: \"Harakat\"\n- `start_url`: \"/\"\n- `display`: \"standalone\"\n- `theme_color`: \"#5dad89\"\n- `background_color`: \"#0a0f0a\"\n\n## Execution Flow & Architecture\n\n### Execution Model\nThis module contains **no execution flows**.\n- **Internal Calls:** None.\n- **Outgoing Calls:** None.\n- **Incoming Calls:** None.\n- **Server-Side Logic:** None.\n\nThe module functions as a static resource set. The web server (e.g., Nginx, Apache, or a static host like Vercel/Netlify) serves these files directly to the client. The frontend application consumes these files to populate the DOM and metadata.\n\n### Data Consumption\n1. **Browser Load:** The browser requests `index.html`.\n2. **Metadata Injection:** The build process or runtime script injects `ai-context.json` into the HTML `<head>`.\n3. **PWA Registration:** The browser reads `manifest.json` to register the app for installation.\n4. **Content Rendering:** The frontend fetches or renders content derived from `about.txt`.\n\n## Integration Points\n\n### Frontend Integration\nDevelopers integrating this module should ensure:\n1. **Schema Injection:** The `ai-context.json` content is serialized and placed within the `<script type=\"application/ld+json\">` tag in the main layout.\n2. **Manifest Linking:** A link to `manifest.json` must be present in the `<head>`:\n ```html\n <link rel=\"manifest\" href=\"/manifest.json\">\n ```\n3. **Content Processing:** The `about.txt` file should be processed by the build system to generate the necessary HTML sections for the landing page.\n\n### External Dependencies\n- **Schema.org:** Relies on standard JSON-LD vocabulary.\n- **PWA Standards:** Relies on the Web App Manifest specification.\n- **Static Hosting:** Requires a static file server capable of serving JSON and TXT files with correct MIME types (`application/ld+json`, `text/plain`, `application/json`).\n\n## Architecture Diagram\n\nThe following diagram illustrates the static asset structure and how the browser consumes these files.\n\n```mermaid\ngraph TD\n Browser[Browser / Client]\n Server[Static Web Server]\n \n subgraph Public_Module [Harakat_Landing — public]\n About[about.txt<br/>Static Content]\n Context[ai-context.json<br/>SEO Schema]\n Manifest[manifest.json<br/>PWA Config]\n end\n \n subgraph Frontend [Main Application]\n HTML[HTML Layout]\n DOM[DOM Renderer]\n end\n \n Server -->|Serves| Browser\n Browser -->|Fetches| About\n Browser -->|Fetches| Context\n Browser -->|Fetches| Manifest\n Context -->|Injected into| HTML\n Manifest -->|Linked to| HTML\n About -->|Processed into| HTML\n HTML -->|Populates| DOM\n```\n\n## Maintenance & Updates\n\n### Updating Content\n- **`about.txt`:** Edit the text file directly. Ensure formatting remains consistent for any build scripts that parse this file.\n- **`ai-context.json`:** Update JSON values directly. Validate against the JSON-LD Playground to ensure schema validity before deployment.\n- **`manifest.json`:** Update icon paths or colors if the design system changes. Ensure `icons` array references valid image paths.\n\n### Version Control\n- Treat these files as critical assets.\n- Changes to `ai-context.json` may affect SEO indexing; test changes in a staging environment.\n- Changes to `manifest.json` may affect PWA installation behavior on user devices.\n\n## Troubleshooting\n\n| Issue | Potential Cause | Resolution |\n| :--- | :--- | :--- |\n| **PWA not installing** | `manifest.json` not linked or invalid | Verify `<link rel=\"manifest\">` is in `<head>` and JSON is valid. |\n| **Rich Snippets missing** | `ai-context.json` not embedded | Ensure JSON-LD script is present in the HTML head. |\n| **Content not rendering** | `about.txt` not processed | Check build pipeline for static content processing steps. |\n| **404 on assets** | Incorrect MIME types | Ensure server returns `application/json` for `.json` files. |","harakat-landing-styles":"# Harakat_Landing — styles\n\n\n\n# Harakat_Landing — styles\n\n## Overview\n\nThe `globals.css` module serves as the central configuration point for the application's visual design system. It defines the global color palette, typography, and theming logic using CSS custom properties and Tailwind CSS directives. This file ensures consistent styling across the `Harakat_Landing` application, supporting both light and dark modes.\n\n## Purpose\n\nThis module establishes the foundational design tokens required by the rest of the application. It does not contain executable logic but rather configures the styling engine (Tailwind CSS) to render the UI components correctly.\n\nKey responsibilities include:\n- Defining the color system using the OKLCH color space.\n- Configuring Tailwind's design system via `@theme inline`.\n- Enabling dark mode support through CSS custom variants.\n- Applying global resets and base styles to the document body.\n\n## Theming System\n\nThe module utilizes CSS custom properties (variables) to manage colors. These variables are defined in two contexts: `:root` for the light theme and `.dark` for the dark theme.\n\n### Color Palette\nThe color system is built using `oklch` values, which provide better perceptual uniformity compared to traditional RGB or HSL. Key tokens include:\n- `--background`: Primary background color.\n- `--foreground`: Primary text color.\n- `--primary`: Main action color (buttons, links).\n- `--card`: Container background color.\n- `--sidebar`: Specific tokens for sidebar components.\n\n### Dark Mode Support\nDark mode is implemented using a custom variant selector:\n```css\n@custom-variant dark (&:is(.dark *));\n```\nWhen the `.dark` class is applied to the `<html>` or `<body>` element, the CSS variables defined within the `.dark` block override the `:root` values.\n\n## Tailwind Configuration\n\nThe `@theme inline` directive maps the CSS variables to Tailwind's utility class system. This allows developers to use utility classes like `bg-background`, `text-foreground`, or `border-border` directly in HTML components.\n\n### Mapped Tokens\nThe following Tailwind design tokens are configured:\n- **Colors:** `background`, `foreground`, `card`, `popover`, `primary`, `secondary`, `muted`, `accent`, `destructive`, `border`, `input`, `ring`, `chart-1` through `chart-5`, and `sidebar` variants.\n- **Radius:** `radius-sm`, `radius-md`, `radius-lg`, `radius-xl` derived from the `--radius` variable.\n- **Fonts:** `font-sans` and `font-mono` mapped to `--font-geist-sans` and `--font-geist-mono`.\n\n## Global Styles\n\nThe `@layer base` block applies styles to the entire document before component-specific styles are loaded.\n\n- **Borders:** All elements (`*`) receive the `border-border` color and an outline ring with 50% opacity.\n- **Body:** The `<body>` element is set to use the `background` color and `foreground` text color.\n\n## Usage Guidelines\n\nDevelopers should use the Tailwind utility classes defined in this module rather than hardcoding colors.\n\n### Example Usage\n```html\n<!-- Light Mode -->\n<div class=\"bg-background text-foreground p-4\">\n Content\n<\/div>\n\n<!-- Dark Mode -->\n<div class=\"dark bg-background text-foreground p-4\">\n Content\n<\/div>\n```\n\n### Theme Customization\nTo modify the color palette, update the `:root` or `.dark` CSS variables in `globals.css`. Changes will automatically propagate to all Tailwind utility classes referencing those tokens.\n\n## Architecture Flow\n\nThe following diagram illustrates how the configuration flows from the CSS file to the rendered DOM.\n\n```mermaid\nflowchart LR\n Root[\":root CSS Variables\"] --> Theme[\"@theme inline\"]\n Theme --> Tailwind[Tailwind Utility Classes]\n Tailwind --> DOM[DOM Elements]\n DarkMode[\".dark Class\"] -.->|Overrides| Root\n```\n\n## Dependencies\n\n- **Tailwind CSS:** Loaded via `@import 'tailwindcss';`.\n- **Tw Animate CSS:** Loaded via `@import 'tw-animate-css';` for animation utilities.\n- **Geist Fonts:** Referenced in `@theme inline` for `--font-geist-sans` and `--font-geist-mono`.","harakat-landing":"# Harakat_Landing\n\n\n\n# Harakat_Landing\n\nThe `Harakat_Landing` module is a Next.js 14 standalone application serving as the primary landing page for the Harakat project. It utilizes a multi-stage Docker build process to optimize production performance and includes server-side capabilities for contact form handling via Zoho SMTP.\n\n## Architecture\n\nThe application follows a layered architecture designed for performance, SEO, and maintainability.\n\n* **Static & Configuration:** The `public` module provides structured metadata, PWA configuration, and static content required for the frontend to render the public-facing interface without server-side processing delays.\n* **Design System:** The `styles` module establishes foundational design tokens (color palette, typography) using CSS custom properties and Tailwind CSS directives.\n* **Utilities:** The `lib` module offers centralized utilities, such as the `cn` function, to merge class names conflict-free across components.\n* **State Management:** The `hooks` module handles lightweight client-side state, including responsive design detection (`use-mobile`) and toast notifications (`use-toast`).\n* **UI & Routing:** The `components` and `app` modules implement the user interface, utilizing the design system and utilities defined in the lower layers.\n* **Server Logic:** API routes handle server-side tasks, such as email processing via Zoho SMTP.\n\n## Key Workflows\n\n### Styling & Theming\nThe `styles` module defines global CSS variables and Tailwind directives. Components in the `components` module rely on the `cn` utility from `lib` to apply these styles dynamically, ensuring consistent rendering across light and dark modes.\n\n### User Interaction & State\nUI components (e.g., `HomePage`, `Hero`, `Carousel`) interact with the `hooks` module to manage state. For instance, the `use-toast` hook manages notification queues via a custom reducer, while `use-mobile` adjusts layout behavior based on viewport width.\n\n### Form Handling\nContact form submissions flow from the `app` pages through API routes. The `api/send-email/route.ts` endpoint handles the request, utilizing utilities like `isRateLimited` and `escapeHtml` to secure the data before processing via Zoho SMTP.\n\n## Module Relationships\n\n```mermaid\nflowchart TD\n subgraph StaticLayer [\"Static & Config\"]\n Public[public]\n Styles[styles]\n Lib[lib]\n end\n\n subgraph LogicLayer [\"Logic & State\"]\n Hooks[hooks]\n API[api]\n end\n\n subgraph PresentationLayer [\"UI & Routing\"]\n App[app]\n Components[components]\n end\n\n Public -->|Metadata/SEO| App\n Styles -->|Design Tokens| Components\n Lib -->|Class Utilities| Components\n Hooks -->|State Management| Components\n Components -->|Server Actions| API\n API -->|Zoho SMTP| External[External Services]\n```","harakat-office-app":"# Harakat_Office — app\n\n\n\n# Harakat_Office App Module Documentation\n\n## Overview\n\nThe Harakat_Office app module provides a project management cockpit for coordinating work across teams. It implements a multi-tenant project workspace with board views, milestones, deliverables, asks, member management, and audit trails. The application is built with Next.js 15 App Router, Next-Intl for internationalization, and uses a 2FA authentication system.\n\n## Architecture\n\n```mermaid\ngraph TD\n A[LocaleLayout] --> B[AppLayout]\n B --> C[ProjectsPage]\n B --> D[ProjectDetailPage]\n B --> E[LoginPage]\n C --> F[vendorsApi.listProjects]\n D --> G[useProjectData]\n E --> H[useAuth]\n H --> I[login/completeTwoFactor]\n```\n\n### Layout Hierarchy\n\n```\nLocaleLayout ([locale]/layout.tsx)\n├── Providers (components/providers.tsx)\n│ ├── NextIntlClientProvider\n│ └── App Router Providers\n└── AppLayout ([locale]/(app)/layout.tsx)\n ├── TopBar (components/shell/top-bar.tsx)\n ├── main (content area)\n └── AppFooter (components/shell/app-footer.tsx)\n```\n\n## Authentication Flow\n\nThe authentication system implements two-factor authentication (2FA) with email-based OTP codes.\n\n### State Machine\n\n```mermaid\nstateDiagram-v2\n [*] --> Loading\n Loading --> Authenticated[\"Authenticated:\"] isAuthenticated\n Loading --> Redirect[\"Redirect:\"] !isAuthenticated\n Authenticated --> Loading[\"Loading:\"] router.replace\n Loading --> LoginPage[\"LoginPage:\"] /login route\n LoginPage --> Credentials[\"Credentials:\"] email + password\n Credentials --> Challenge[\"Challenge:\"] needs-2fa\n Challenge --> LoginPage[\"LoginPage:\"] backToCredentials\n Challenge --> Verified[\"Verified:\"] code verified\n Verified --> Authenticated[\"Authenticated:\"] router.replace\n```\n\n### Key Functions\n\n**`useAuth`** (`@/lib/auth-context`)\n- `login(email, password)` → Returns `needs-2fa` challenge or direct authentication\n- `completeTwoFactor(challengeToken, code)` → Validates OTP and completes login\n- `isAuthenticated` → Boolean flag for auth state\n- `isLoading` → Loading state for auth check\n\n**`LoginPage`** (`[locale]/login/page.tsx`)\n- Manages credential submission and 2FA challenge\n- Stores credentials in `credentialsRef` (never drives renders)\n- Handles resend cooldown with 30-second timer\n- Uses `react-hook-form` for form validation\n\n**`TwoFactorStep`** (component within LoginPage)\n- Renders OTP input with Latin digits (LTR pinned)\n- Manages resend cooldown countdown\n- Handles 2FA verification errors (401 covers wrong/expired code)\n\n### Authentication Guard\n\n**`AppLayout`** (`[locale]/(app)/layout.tsx`)\n```typescript\nReact.useEffect(() => {\n if (!isLoading && !isAuthenticated) {\n router.replace(\"/login\");\n }\n}, [isLoading, isAuthenticated, router]);\n```\n\nRedirects unauthenticated users to `/login` after auth state stabilizes.\n\n## Project Management\n\n### Projects List Page\n\n**`ProjectsPage`** (`[locale]/(app)/projects/page.tsx`)\n\n**State Types:**\n```typescript\ntype LoadState =\n | { kind: \"loading\" }\n | { kind: \"not-enabled\" }\n | { kind: \"error\" }\n | { kind: \"ready\"; projects: ProjectListItem[] };\n```\n\n**Execution Flow:**\n1. `load()` callback fetches projects via `vendorsApi.listProjects()`\n2. Sets loading state, then resolves to ready/error/not-enabled\n3. Renders appropriate UI based on state kind\n\n**Key Components:**\n- `PageHeader` - Title, subtitle, and create action\n- `CreateProjectDialog` - Modal for new project creation\n- `ProjectStatusBadge` - Visual status indicator\n- `EmptyState` - When no projects exist\n- `ErrorState` - For API failures with retry\n\n**Project Card Data:**\n- `name` - Project title\n- `description` - Optional description\n- `status` - ARCHIVED, ACTIVE, etc.\n- `memberCount` - Number of team members\n- `openAsks` - Count of pending requests\n- `createdAt` - ISO timestamp\n\n### Project Detail Page\n\n**`ProjectDetailPage`** (`[locale]/(app)/projects/[id]/page.tsx`)\n\n**ID Conversion Boundary:**\n```typescript\nconst id = Number(params.id); // String route param → Int entity id\n```\n\n**Data Loading:**\n- Uses `useProjectData(id)` hook from `@/lib/use-project-data`\n- Returns `board`, `detail`, `milestones`, `deliverables`, `asks`, `audit`\n- Handles loading, error, and not-enabled states\n\n**Tab Navigation:**\n```typescript\nTabs defaultValue=\"board\"\n├── board → BoardTab\n├── milestones → MilestonesTab\n├── deliverables → DeliverablesTab\n├── asks → AsksTab\n├── members → MembersTab\n└── audit → AuditTab\n```\n\n**Error States:**\n- `not-enabled` → `NotEnabledState`\n- `error` or missing data → `ErrorState` with retry\n- Invalid ID → `ErrorState` with back link\n\n**ProjectActions** (`components/project/project-actions.tsx`)\n- Action buttons for project management\n- `onDone` callback triggers data refresh\n\n## Key Components\n\n### Common UI Components\n\n| Component | Path | Purpose |\n|-----------|------|---------|\n| `Card` | `components/ui/card.tsx` | Container with border and padding |\n| `Skeleton` | `components/ui/skeleton.tsx` | Loading state placeholders |\n| `Button` | `components/ui/button.tsx` | Primary/secondary action buttons |\n| `Input` | `components/ui/input.tsx` | Form inputs with validation |\n| `Label` | `components/ui/label.tsx` | Form field labels |\n| `Tabs` | `components/ui/tabs.tsx` | Tab navigation system |\n| `ErrorState` | `components/common/states.tsx` | Error display with retry |\n| `NotEnabledState` | `components/common/states.tsx` | Feature disabled indicator |\n| `EmptyState` | `components/common/empty-state.tsx` | Empty list placeholder |\n\n### Project-Specific Components\n\n| Component | Path | Props |\n|-----------|------|-------|\n| `BoardTab` | `components/board/board-tab.tsx` | `board`, `projectId`, `members`, `onRefresh` |\n| `MilestonesTab` | `components/project/milestones-tab.tsx` | `projectId`, `milestones`, `members`, `onRefresh` |\n| `DeliverablesTab` | `components/project/deliverables-tab.tsx` | `projectId`, `deliverables`, `members`, `onRefresh` |\n| `AsksTab` | `components/project/asks-tab.tsx` | `projectId`, `asks`, `members`, `loadError`, `onRefresh` |\n| `MembersTab` | `components/project/members-tab.tsx` | `projectId`, `members`, `onRefresh` |\n| `AuditTab` | `components/project/audit-tab.tsx` | `audit`, `loadError`, `onRefresh` |\n\n### Shell Components\n\n| Component | Path | Purpose |\n|-----------|------|---------|\n| `TopBar` | `components/shell/top-bar.tsx` | Navigation header |\n| `AppFooter` | `components/shell/app-footer.tsx` | Page footer |\n| `LocaleSwitcher` | `components/shell/locale-switcher.tsx` | Language selection |\n| `ThemeToggle` | `components/shell/theme-toggle.tsx` | Light/dark mode switch |\n\n## State Management\n\n### Client-Side State\n\n**`ProjectsPage`** - Manages project list state:\n```typescript\nconst [state, setState] = React.useState<LoadState>({ kind: \"loading\" });\n```\n\n**`LoginPage`** - Manages auth flow state:\n```typescript\nconst [formError, setFormError] = React.useState<string | null>(null);\nconst [challenge, setChallenge] = React.useState<Challenge | null>(null);\nconst credentialsRef = React.useRef<LoginForm | null>(null);\n```\n\n**`ProjectDetailPage`** - Uses `useProjectData` hook for server state:\n```typescript\nconst data = useProjectData(id);\n// data.status: \"loading\" | \"error\" | \"not-enabled\" | \"ready\"\n```\n\n### Server-Side State\n\n**`LocaleLayout`** - Manages locale and direction:\n```typescript\nconst { locale } = await params;\nconst dir = localeDirection[locale as Locale];\n```\n\n## Internationalization\n\n### Next-Intl Integration\n\n**`LocaleLayout`** (`[locale]/layout.tsx`)\n- Validates locale against `routing.locales`\n- Sets `setRequestLocale(locale)` for client hydration\n- Passes `locale` and `dir` to HTML element\n- Uses `NextIntlClientProvider` for client-side translations\n\n**Translation Keys:**\n- `guard` - Auth guard messages\n- `login` - Login form and 2FA\n- `projects` - Project list and actions\n- `project` - Project detail labels\n- `app` - App metadata\n- `common` - Shared UI labels\n\n### RTL Support\n\n```typescript\n// ProjectDetailPage - BackLink\nclassName=\"rtl:rotate-180\"\n\n// LoginPage - OTP input\ndir=\"ltr\" // Always Latin digits\n```\n\n## Styling & Theme\n\n### CSS Variables\n\n**Light Theme (Default):**\n```css\n--brand-paper: #f6f5f0\n--brand-ink: #0a2118\n--brand-green: #2f8a63\n--brand-forest: #0f5537\n```\n\n**Dark Theme:**\n```css\n--background: #0b1712\n--foreground: #eef1ec\n--primary: #3ba173\n```\n\n### Signal Colors\n\nEach color has a single semantic meaning:\n- `--success` - Completion, positive states\n- `--warning` - Caution, pending items\n- `--danger` - Errors, destructive actions\n- `--info` - Informational messages\n\n### Utility Classes\n\n```css\n.panel {\n background: var(--card);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n}\n\n.tnum {\n font-variant-numeric: tabular-nums;\n}\n```\n\n## API Integration\n\n### Vendors API\n\n**`vendorsApi`** (`@/lib/api/vendors.ts`)\n- `listProjects()` → Returns `ProjectListItem[]`\n- Used by `ProjectsPage` for project enumeration\n\n### Client API\n\n**`@/lib/api/client.ts`**\n- `isNotEnabled(err)` → Detects feature disabled errors\n- `ApiError` → Error type with `status` property\n\n### Project Data Hook\n\n**`useProjectData`** (`@/lib/use-project-data.ts`)\n- Fetches project details, board, milestones, deliverables, asks, audit\n- Returns unified data structure with status tracking\n- Handles refresh via `data.refresh` callback\n\n## Error Handling\n\n### Error States\n\n| State | Trigger | Component |\n|-------|---------|-----------|\n| `loading` | Initial fetch | `Skeleton` |\n| `error` | API failure | `ErrorState` |\n| `not-enabled` | Feature disabled | `NotEnabledState` |\n| `empty` | No data | `EmptyState` |\n\n### Error Recovery\n\n- `ErrorState` provides `onRetry` callback\n- `2FA` failures trigger resend button\n- `401` errors show generic message (wrong/expired code indistinguishable)\n\n### Form Validation\n\n**`react-hook-form`** integration:\n```typescript\n{\n required: tCommon(\"requiredField\"),\n pattern: {\n value: /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/,\n message: tCommon(\"requiredField\"),\n }\n}\n```\n\n## Cross-Community Dependencies\n\n### `cn` Utility\n\nMultiple components use `cn` from `StartupsPlatform/lib/utils.ts`:\n- `ProjectsPage` → `CreateProjectDialog` → `cn`\n- `ProjectDetailPage` → `ErrorState` → `cn`\n- `LoginPage` → `LocaleSwitcher` → `cn`\n\n### Dialog Portal\n\n**`ProjectsPage`** → `CreateProjectDialog` → `DialogPortal`\n- Modal dialogs use portal mounting\n- `DialogContent` and `DialogOverlay` from `components/ui/dialog.tsx`\n\n## Contributing Guidelines\n\n### Adding New Tabs\n\n1. Create tab component in `components/project/[tab-name]-tab.tsx`\n2. Accept props: `projectId`, `data`, `members`, `onRefresh`\n3. Add tab trigger in `ProjectDetailPage`\n4. Add translation keys in `project` namespace\n\n### Adding New States\n\n1. Extend `LoadState` type in `ProjectsPage`\n2. Add conditional rendering for new state kind\n3. Create corresponding state component in `components/common/states.tsx`\n\n### Adding New API Endpoints\n\n1. Add to `vendorsApi` in `@/lib/api/vendors.ts`\n2. Handle errors with `isNotEnabled()` check\n3. Update `LoadState` types if needed\n\n### Theme Customization\n\n1. Modify CSS variables in `globals.css`\n2. Ensure signal colors maintain semantic meaning\n3. Test both light and dark modes\n\n## Performance Considerations\n\n- `useProjectData` hook manages server state efficiently\n- Skeleton loading prevents layout shift\n- `cn` utility minimizes class string concatenation\n- `credentialsRef` avoids unnecessary re-renders in login flow\n- `cooldown` timer uses `useEffect` cleanup for interval\n\n## Security Notes\n\n- Credentials stored in `credentialsRef` never drive renders\n- OTP codes are 6 numeric digits (Latin)\n- 2FA challenge tokens are single-use\n- 30-second resend cooldown prevents abuse\n- `401` errors don't distinguish between wrong code and expired challenge","harakat-office-components":"# Harakat_Office — components\n\n\n\n# Harakat_Office — Components Module\n\n## Overview\n\nThe `components` module provides the complete React UI layer for the Harakat Office project management application. It implements a shell-based architecture with reusable UI primitives, feature-specific components, and shared utilities for internationalization, theming, and user interactions.\n\nAll components are client-side (`\"use client\"`), leveraging Next.js App Router conventions with `next-intl` for i18n, Radix UI primitives for accessibility, and Sonner for toast notifications.\n\n---\n\n## Architecture\n\n```mermaid\ngraph TB\n subgraph Shell Layer\n TopBar[TopBar]\n UserMenu[UserMenu]\n ThemeToggle[ThemeToggle]\n LocaleSwitcher[LocaleSwitcher]\n AppFooter[AppFooter]\n end\n\n subgraph Common UI\n Badge[Badge]\n Button[Button]\n Card[Card]\n Dialog[Dialog]\n Input[Input]\n Select[Select]\n Table[Table]\n EmptyState[EmptyState]\n StatusBadges[StatusBadges]\n PlainText[PlainText]\n end\n\n subgraph Feature Components\n BoardTab[BoardTab]\n AsksTab[AsksTab]\n MembersTab[MembersTab]\n MilestonesTab[MilestonesTab]\n DeliverablesTab[DeliverablesTab]\n AuditTab[AuditTab]\n ProjectActions[ProjectActions]\n end\n\n subgraph Providers\n Providers[Providers]\n AuthProvider[AuthProvider]\n ThemeProvider[ThemeProvider]\n end\n\n Providers --> TopBar\n Providers --> UserMenu\n Providers --> ThemeToggle\n Providers --> LocaleSwitcher\n TopBar --> UserMenu\n TopBar --> ThemeToggle\n TopBar --> LocaleSwitcher\n TopBar --> BoardTab\n TopBar --> AsksTab\n TopBar --> MembersTab\n TopBar --> MilestonesTab\n TopBar --> DeliverablesTab\n TopBar --> AuditTab\n BoardTab --> Common UI\n AsksTab --> Common UI\n MembersTab --> Common UI\n MilestonesTab --> Common UI\n DeliverablesTab --> Common UI\n AuditTab --> Common UI\n ProjectActions --> Common UI\n```\n\n---\n\n## Shell Components\n\n### TopBar\nThe application header containing navigation branding and user controls.\n\n```tsx\nexport function TopBar() {\n return (\n <header className=\"sticky top-0 z-30 border-b backdrop-blur\">\n <div className=\"flex h-14 max-w-6xl items-center justify-between\">\n <Link href=\"/projects\">\n <span className=\"bg-primary text-primary-foreground flex size-7 items-center justify-center rounded-md text-sm font-bold\">\n ح\n <\/span>\n <span className=\"flex flex-col leading-none\">\n <span className=\"text-sm font-semibold\">Harakat Office<\/span>\n <span className=\"text-muted-foreground text-[11px]\">\n Project Management Platform\n <\/span>\n <\/span>\n <\/Link>\n <div className=\"flex items-center gap-1\">\n <LocaleSwitcher />\n <ThemeToggle />\n <UserMenu />\n <\/div>\n <\/div>\n <\/header>\n );\n}\n```\n\n**Key Responsibilities:**\n- Sticky positioning with backdrop blur for readability\n- Branding with Arabic \"ح\" logo\n- User controls: locale switching, theme toggling, account menu\n\n### UserMenu\nUser account dropdown with authentication context.\n\n```tsx\nexport function UserMenu() {\n const { user, logout } = useAuth();\n const router = useRouter();\n\n function handleLogout() {\n logout();\n router.replace(\"/login\");\n }\n\n const initial = (user?.name || user?.email || \"?\").trim().charAt(0).toUpperCase();\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"sm\" className=\"gap-2\">\n <span className=\"bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-full text-xs font-semibold\">\n {initial}\n <\/span>\n <span className=\"hidden max-w-[10rem] truncate text-sm sm:inline\">\n {user?.name || user?.email}\n <\/span>\n <\/Button>\n <\/DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"min-w-56\">\n <DropdownMenuLabel className=\"flex flex-col gap-1\">\n <span className=\"text-xs text-muted-foreground\">Signed in as<\/span>\n <span className=\"truncate font-medium\">{user?.name}<\/span>\n {user?.startup?.name ? (\n <span className=\"text-muted-foreground truncate text-xs font-normal\">\n {user.startup.name}\n <\/span>\n ) : null}\n <\/DropdownMenuLabel>\n <DropdownMenuSeparator />\n <DropdownMenuItem variant=\"destructive\" onClick={handleLogout}>\n <LogOut className=\"size-4\" />\n Logout\n <\/DropdownMenuItem>\n <\/DropdownMenuContent>\n <\/DropdownMenu>\n );\n}\n```\n\n### ThemeToggle\nLight/dark theme switcher with SSR-safe mounting.\n\n```tsx\nexport function ThemeToggle() {\n const { resolvedTheme, setTheme } = useTheme();\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n\n const isDark = resolvedTheme === \"dark\";\n\n return (\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Toggle theme\"\n onClick={() => setTheme(isDark ? \"light\" : \"dark\")}\n >\n {mounted && isDark ? (\n <Sun className=\"size-4\" />\n ) : (\n <Moon className=\"size-4\" />\n )}\n <\/Button>\n );\n}\n```\n\n**SSR Safety:** Uses `mounted` state to prevent hydration mismatch between server and client theme rendering.\n\n### LocaleSwitcher\nLanguage switcher supporting English and Arabic.\n\n```tsx\nexport function LocaleSwitcher() {\n const locale = useLocale();\n const pathname = usePathname();\n const router = useRouter();\n\n const labels: Record<Locale, string> = {\n en: \"English\",\n ar: \"العربية\",\n };\n\n function switchTo(next: Locale) {\n if (next === locale) return;\n router.replace(pathname, { locale: next });\n }\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"sm\" aria-label=\"Switch language\">\n <Globe className=\"size-4\" />\n <span className=\"text-sm\">{labels[locale as Locale]}<\/span>\n <\/Button>\n <\/DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"min-w-40\">\n {routing.locales.map((l) => (\n <DropdownMenuItem\n key={l}\n onClick={() => switchTo(l)}\n className=\"justify-between\"\n >\n <span>{labels[l]}<\/span>\n {l === locale ? <Check className=\"size-4\" /> : null}\n <\/DropdownMenuItem>\n ))}\n <\/DropdownMenuContent>\n <\/DropdownMenu>\n );\n}\n```\n\n---\n\n## Common UI Components\n\n### Status Badges\nCentralized status rendering for various entity types.\n\n```tsx\nexport function MemberStatusBadge({ status }: { status: MemberStatus }) {\n const map: Record<MemberStatus, { variant: Variant; key: string }> = {\n INVITED: { variant: \"info\", key: \"statusInvited\" },\n ACTIVE: { variant: \"success\", key: \"statusActive\" },\n REVOKED: { variant: \"secondary\", key: \"statusRevoked\" },\n };\n const { variant, key } = map[status];\n return <Badge variant={variant}>{t(key)}<\/Badge>;\n}\n\nexport function AskStatusBadge({ status }: { status: AskStatus }) {\n const map: Record<AskStatus, { variant: Variant; key: string }> = {\n PENDING: { variant: \"info\", key: \"statusPending\" },\n ANSWERED: { variant: \"success\", key: \"statusAnswered\" },\n SUPPRESSED: { variant: \"danger\", key: \"statusSuppressed\" },\n UNANSWERED: { variant: \"warning\", key: \"statusUnanswered\" },\n FAILED: { variant: \"danger\", key: \"statusFailed\" },\n };\n const { variant, key } = map[status];\n return <Badge variant={variant}>{t(key)}<\/Badge>;\n}\n```\n\n**Usage Pattern:** All status badges follow the same pattern:\n1. Map status enum to `{ variant, key }` tuple\n2. Use `Badge` component with variant\n3. Translate key via `useTranslations`\n\n### PlainText\nRenders untrusted cross-company text with strict security constraints.\n\n```tsx\nexport function PlainText({\n children,\n className,\n}: {\n children: string;\n className?: string;\n}) {\n return (\n <p\n dir=\"auto\"\n className={cn(\n \"whitespace-pre-wrap break-words text-sm leading-relaxed\",\n className,\n )}\n >\n {children}\n <\/p>\n );\n}\n```\n\n**Security Doctrine:**\n- No markdown parsing\n- No HTML injection\n- No URL auto-linking\n- No `dangerouslySetInnerHTML`\n- `dir=\"auto\"` for mixed Arabic/English content\n\n**Use Cases:**\n- Vendor narrated blocks (untrusted data)\n- Suppression reasons (third-party text)\n- Audit action labels (raw codes)\n\n### EmptyState\nReusable empty state component for list views.\n\n```tsx\nexport function EmptyState({\n icon: Icon,\n title,\n body,\n action,\n className,\n}: {\n icon?: React.ComponentType<{ className?: string }>;\n title: string;\n body?: string;\n action?: React.ReactNode;\n className?: string;\n}) {\n return (\n <div\n className={cn(\n \"border-border flex flex-col items-center justify-center gap-3 rounded-xl border border-dashed px-6 py-14 text-center\",\n className,\n )}\n >\n {Icon ? (\n <div className=\"bg-muted text-muted-foreground flex size-11 items-center justify-center rounded-full\">\n <Icon className=\"size-5\" />\n <\/div>\n ) : null}\n <div className=\"space-y-1\">\n <h3 className=\"text-base font-semibold\">{title}<\/h3>\n {body ? (\n <p className=\"text-muted-foreground mx-auto max-w-sm text-sm leading-relaxed\">\n {body}\n <\/p>\n ) : null}\n <\/div>\n {action ? <div className=\"mt-1\">{action}<\/div> : null}\n <\/div>\n );\n}\n```\n\n---\n\n## Feature Components\n\n### BoardTab\nDisplays project board with vendor members and chain status.\n\n```tsx\nexport function BoardTab({\n board,\n projectId,\n members,\n onRefresh,\n}: {\n board: BoardResponse;\n projectId: number;\n members: Member[];\n onRefresh: () => Promise<void>;\n}) {\n const boardMembers = board.members;\n\n return (\n <div className=\"space-y-4\">\n <SectionHeader\n title={t(\"title\")}\n subtitle={t(\"subtitle\")}\n actions={\n <>\n <NoteComposer\n projectId={projectId}\n members={members}\n onDone={onRefresh}\n />\n <ChainPill ok={board.chain.ok} length={board.chain.length} />\n <\/>\n }\n />\n\n {boardMembers.length === 0 ? (\n <EmptyState icon={Users} title={t(\"emptyTitle\")} body={t(\"emptyBody\")} />\n ) : (\n <div className=\"grid gap-4 xl:grid-cols-2\">\n {boardMembers.map((m) => (\n <MemberCard key={m.id} member={m} />\n ))}\n <\/div>\n )}\n <\/div>\n );\n}\n```\n\n**Key Components:**\n- `ChainPill`: Visual indicator of hash chain integrity\n- `MemberCard`: Detailed vendor member status card\n- `NoteComposer`: Coordinator's status note dialog\n\n### MemberCard\nComprehensive vendor member status display.\n\n```tsx\nexport function MemberCard({ member }: { member: BoardMember }) {\n const s = member.signals;\n\n const milestonesComplete = s.milestonesTotal > 0 && s.milestonesDone === s.milestonesTotal;\n const deliverablesTotal = s.deliverablesProvided + s.deliverablesDue;\n const deliverablesComplete = deliverablesTotal > 0 && s.deliverablesDue === 0;\n\n return (\n <Card className=\"gap-4 py-5\">\n <div className=\"flex items-start justify-between gap-3 px-6\">\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold\">{displayName(member.startupName)}<\/h3>\n <div className=\"mt-1.5 flex flex-wrap items-center gap-1.5\">\n <Badge variant=\"outline\">{roleLabel}<\/Badge>\n <MemberStatusBadge status={member.status} />\n <\/div>\n <\/div>\n <\/div>\n\n <div className=\"grid gap-4 px-6 lg:grid-cols-2\">\n {/* Narrated — the vendor's own account */}\n <NarratedBlock narrated={member.narratedLatest} />\n\n {/* Objective signals — computed by the cockpit */}\n <div className=\"space-y-2\">\n <div className=\"text-muted-foreground text-xs font-semibold uppercase tracking-wide\">\n {t(\"signals\")}\n <\/div>\n <div className=\"grid grid-cols-2 gap-2 sm:grid-cols-3\">\n <SignalTile\n label={t(\"milestones\")}\n value={`${num(s.milestonesDone)}/${num(s.milestonesTotal)}`}\n tone={milestonesComplete ? \"success\" : \"neutral\"}\n icon={CheckCircle2}\n />\n {/* ... 5 more signal tiles */}\n <\/div>\n <\/div>\n <\/div>\n <\/Card>\n );\n}\n```\n\n**Signal Tiles:**\n- Milestones: `done/total` with success/neutral tone\n- Overdue: count with danger tone if > 0\n- Deliverables: `provided/total` with success/neutral tone\n- Last Update: relative time with clock icon\n- Unanswered Asks: count with warning tone if > 0\n- Suppressed Asks: count with danger tone if > 0\n\n### NoteComposer\nCoordinator's status note dialog (not vendor narrated).\n\n```tsx\nexport function NoteComposer({\n projectId,\n members,\n onDone,\n}: {\n projectId: number;\n members: Member[];\n onDone: () => Promise<void>;\n}) {\n const attachable = members.filter(\n (m) => m.role === \"VENDOR\" && m.status !== \"REVOKED\",\n );\n\n async function submit() {\n setTouched(true);\n if (!content.trim() || saving) return;\n setSaving(true);\n try {\n await vendorsApi.addNote(projectId, {\n content: content.trim(),\n memberId: about,\n });\n toast.success(t(\"added\"));\n setOpen(false);\n resetAll();\n await onDone();\n } catch {\n toast.error(t(\"addError\"));\n } finally {\n setSaving(false);\n }\n }\n}\n```\n\n**Key Distinction:** Unlike vendor narrated blocks, coordinator notes:\n- Are written by the tenant (trusted)\n- Do NOT become vendor narrated blocks\n- Land on the project's hash-chained audit trail\n- Use plain text (no markdown)\n\n### AsksTab\nProject asks (questions to vendors) with status tracking.\n\n```tsx\nexport function AsksTab({\n projectId,\n asks,\n members,\n loadError,\n onRefresh,\n}: {\n projectId: number;\n asks: Ask[];\n members: Member[];\n loadError: boolean;\n onRefresh: () => Promise<void>;\n}) {\n const activeVendors = members.filter(\n (m) => m.role === \"VENDOR\" && m.status === \"ACTIVE\",\n );\n\n return (\n <div className=\"space-y-4\">\n <SectionHeader\n title={t(\"title\")}\n subtitle={t(\"subtitle\")}\n actions={<AskComposer projectId={projectId} members={activeVendors} onDone={onRefresh} />}\n />\n\n <div className=\"border-info/25 bg-info/5 text-muted-foreground flex items-start gap-2 rounded-lg border px-3 py-2 text-xs\">\n <Info className=\"text-info mt-0.5 size-4 shrink-0\" />\n <span>{t(\"silenceCaption\")}<\/span>\n <\/div>\n\n {loadError ? (\n <ErrorState message={t(\"sendError\")} onRetry={onRefresh} />\n ) : asks.length === 0 ? (\n <EmptyState\n icon={MessageSquareWarning}\n title={t(\"emptyTitle\")}\n body={t(\"emptyBody\")}\n />\n ) : (\n <div className=\"space-y-2\">\n {asks.map((a) => (\n <AskRow key={a.id} ask={a} members={members} />\n ))}\n <\/div>\n )}\n <\/div>\n );\n}\n```\n\n**Ask Status Flow:**\n```\nPENDING → UNANSWERED → ANSWERED\n ↓\n SUPPRESSED (withheld/declined)\n ↓\n FAILED (rail delivery error)\n```\n\n### MembersTab\nProject member management with invite/revoke dialogs.\n\n```tsx\nexport function MembersTab({\n projectId,\n members,\n onRefresh,\n}: {\n projectId: number;\n members: Member[];\n onRefresh: () => Promise<void>;\n}) {\n return (\n <div className=\"space-y-4\">\n <SectionHeader\n title={t(\"title\")}\n subtitle={t(\"subtitle\")}\n actions={<InviteDialog projectId={projectId} onDone={onRefresh} />}\n />\n\n {members.length === 0 ? (\n <EmptyState icon={Users} title={t(\"emptyTitle\")} body={t(\"emptyBody\")} />\n ) : (\n <div className=\"panel overflow-hidden\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{t(\"columnVendor\")}<\/TableHead>\n <TableHead>{t(\"columnRole\")}<\/TableHead>\n <TableHead>{t(\"columnStatus\")}<\/TableHead>\n <TableHead>{t(\"columnLink\")}<\/TableHead>\n <TableHead className=\"text-end\">{t(\"columnActions\")}<\/TableHead>\n <\/TableRow>\n <\/TableHeader>\n <TableBody>\n {members.map((m) => (\n <TableRow key={m.id}>\n <TableCell>\n <div className=\"font-medium\">{displayName(m.startupName)}<\/div>\n {m.invitedAt ? (\n <div className=\"text-muted-foreground text-xs\">\n {t(\"invitedOn\", { date: formatDate(m.invitedAt, locale) })}\n <\/div>\n ) : null}\n <\/TableCell>\n <TableCell>\n {m.role === \"COORDINATOR\" ? t(\"roleCoordinator\") : t(\"roleVendor\")}\n <\/TableCell>\n <TableCell>\n <MemberStatusBadge status={m.status} />\n <\/TableCell>\n <TableCell>\n <LinkStateBadge status={m.status} />\n <\/TableCell>\n <TableCell className=\"text-end\">\n {m.role === \"VENDOR\" && m.status !== \"REVOKED\" ? (\n <RevokeDialog\n projectId={projectId}\n member={m}\n onDone={onRefresh}\n />\n ) : (\n <span className=\"text-muted-foreground text-sm\">—<\/span>\n )}\n <\/TableCell>\n <\/TableRow>\n ))}\n <\/TableBody>\n <\/Table>\n <\/div>\n )}\n <\/div>\n );\n}\n```\n\n### MilestonesTab\nProject milestone management with status updates.\n\n```tsx\nexport function MilestonesTab({\n projectId,\n milestones,\n members,\n onRefresh,\n}: {\n projectId: number;\n milestones: Milestone[];\n members: Member[];\n onRefresh: () => Promise<void>;\n}) {\n const STATUSES: MilestoneStatus[] = [\"PENDING\", \"IN_PROGRESS\", \"BLOCKED\", \"DONE\"];\n\n return (\n <div className=\"space-y-4\">\n <SectionHeader\n title={t(\"title\")}\n subtitle={t(\"subtitle\")}\n actions={<AddMilestoneDialog projectId={projectId} members={members} onDone={onRefresh} />}\n />\n\n {milestones.length === 0 ? (\n <EmptyState icon={Flag} title={t(\"emptyTitle\")} body={t(\"emptyBody\")} />\n ) : (\n <div className=\"space-y-2\">\n {milestones.map((m) => (\n <MilestoneRow\n key={m.id}\n projectId={projectId}\n milestone={m}\n members={members}\n onDone={onRefresh}\n />\n ))}\n <\/div>\n )}\n <\/div>\n );\n}\n```\n\n**Status Change Flow:**\n```mermaid\ngraph LR\n A[PENDING] --> B[IN_PROGRESS]\n B --> C[BLOCKED]\n B --> D[DONE]\n C --> B\n C --> D\n```\n\n### DeliverablesTab\nProject deliverable tracking with provided status.\n\n```tsx\nexport function DeliverablesTab({\n projectId,\n deliverables,\n members,\n onRefresh,\n}: {\n projectId: number;\n deliverables: Deliverable[];\n members: Member[];\n onRefresh: () => Promise<void>;\n}) {\n return (\n <div className=\"space-y-4\">\n <SectionHeader\n title={t(\"title\")}\n subtitle={t(\"subtitle\")}\n actions={<AddDeliverableDialog projectId={projectId} members={members} onDone={onRefresh} />}\n />\n\n {deliverables.length === 0 ? (\n <EmptyState icon={Package} title={t(\"emptyTitle\")} body={t(\"emptyBody\")} />\n ) : (\n <div className=\"space-y-2\">\n {deliverables.map((d) => (\n <DeliverableRow\n key={d.id}\n projectId={projectId}\n deliverable={d}\n members={members}\n onDone={onRefresh}\n />\n ))}\n <\/div>\n )}\n <\/div>\n );\n}\n```\n\n### AuditTab\nHash-chained audit trail visualization.\n\n```tsx\nexport function AuditTab({\n audit,\n loadError,\n onRefresh,\n}: {\n audit: AuditResponse | null;\n loadError: boolean;\n onRefresh: () => Promise<void>;\n}) {\n const chainOk = audit.chainOk;\n\n return (\n <div className=\"space-y-4\">\n <SectionHeader title={t(\"title\")} subtitle={t(\"subtitle\")} />\n\n <div\n className={cn(\n \"flex items-start gap-2.5 rounded-lg border px-4 py-3 text-sm\",\n chainOk\n ? \"border-success/30 bg-success/8 text-success\"\n : \"border-danger/30 bg-danger/8 text-danger\",\n )}\n >\n {chainOk ? (\n <ShieldCheck className=\"mt-0.5 size-5 shrink-0\" />\n ) : (\n <ShieldX className=\"mt-0.5 size-5 shrink-0\" />\n )}\n <div>\n <p className=\"font-semibold\">\n {chainOk ? t(\"chainVerified\") : t(\"chainBroken\")}\n <\/p>\n <p className=\"text-foreground/80 text-xs\">\n {chainOk ? t(\"chainVerifiedBody\") : t(\"chainBrokenBody\")}\n <\/p>\n <\/div>\n <\/div>\n\n {audit.events.length === 0 ? (\n <EmptyState\n icon={ScrollText}\n title={t(\"emptyTitle\")}\n body={t(\"emptyBody\")}\n />\n ) : (\n <div className=\"panel overflow-hidden\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"w-12\">{t(\"columnSeq\")}<\/TableHead>\n <TableHead>{t(\"columnActor\")}<\/TableHead>\n <TableHead>{t(\"columnAction\")}<\/TableHead>\n <TableHead>{t(\"columnDetails\")}<\/TableHead>\n <TableHead>{t(\"columnTime\")}<\/TableHead>\n <\/TableRow>\n <\/TableHeader>\n <TableBody>\n {audit.events.map((e) => {\n const details = JSON.stringify(e.payload ?? {});\n return (\n <TableRow key={e.seq}>\n <TableCell className=\"tnum text-muted-foreground\">{e.seq}<\/TableCell>\n <TableCell>\n <Badge variant=\"outline\">{t(ACTOR_KEY[e.actorKind])}<\/Badge>\n <\/TableCell>\n <TableCell className=\"font-medium\">\n <AuditActionLabel action={e.action} />\n <\/TableCell>\n <TableCell className=\"max-w-[22rem]\">\n <code\n dir=\"ltr\"\n title={details}\n className=\"text-muted-foreground block truncate font-mono text-xs\"\n >\n {details}\n <\/code>\n <\/TableCell>\n <TableCell className=\"text-muted-foreground whitespace-nowrap text-xs\">\n {formatDateTime(e.createdAt, locale)}\n <\/TableCell>\n <\/TableRow>\n );\n })}\n <\/TableBody>\n <\/Table>\n <\/div>\n )}\n <\/div>\n );\n}\n```\n\n**Audit Action Label:**\n```tsx\nexport function AuditActionLabel({ action }: { action: string }) {\n const t = useTranslations(\"vendorCodes.auditAction\");\n const key = auditActionKey(action);\n\n if (!key) {\n return <span><bdi>{action}<\/bdi><\/span>;\n }\n return (\n <>\n <span className=\"block\">{t(key)}<\/span>\n <span className=\"text-muted-foreground/80 mt-0.5 block font-mono text-[11px] font-normal\">\n <bdi>{action}<\/bdi>\n <\/span>\n <\/>\n );\n}\n```\n\n**Forensic Display:** Shows both translated label and raw machine code for audit trail integrity.\n\n---\n\n## UI Primitives\n\n### Badge\nVariant-based status indicator with color-coded backgrounds.\n\n```tsx\nconst badgeVariants = cva(\n \"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none transition-colors overflow-hidden\",\n {\n variants: {\n variant: {\n default: \"border-transparent bg-primary text-primary-foreground\",\n secondary: \"border-border bg-muted text-foreground\",\n outline: \"border-border bg-transparent text-foreground\",\n success: \"border-transparent bg-success/12 text-success dark:bg-success/18\",\n warning: \"border-transparent bg-warning/14 text-warning dark:bg-warning/20\",\n danger: \"border-transparent bg-danger/12 text-danger dark:bg-danger/20\",\n info: \"border-transparent bg-info/12 text-info dark:bg-info/20\",\n },\n },\n defaultVariants: { variant: \"default\" },\n },\n);\n```\n\n### Dialog\nRadix-based dialog with optional close button.\n\n```tsx\nexport function Dialog({\n children,\n showCloseButton = true,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & {\n showCloseButton?: boolean;\n}) {\n return (\n <DialogPortal data-slot=\"dialog-portal\">\n <DialogOverlay />\n <DialogPrimitive.Content\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border p-6 shadow-2xl duration-200 sm:max-w-lg\",\n className,\n )}\n {...props}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close className=\"ring-offset-background focus:ring-ring absolute top-4 end-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\">\n <XIcon />\n <span className=\"sr-only\">Close<\/span>\n <\/DialogPrimitive.Close>\n )}\n <\/DialogPrimitive.Content>\n <\/DialogPortal>\n );\n}\n```\n\n### Card\nContainer component with header/footer support.\n\n```tsx\nexport function Card({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card\"\n className={cn(\n \"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm\",\n className,\n )}\n {...props}\n />\n );\n}\n```\n\n---\n\n## Providers\n\n### Providers\nRoot provider component for theme, auth, and toast context.\n\n```tsx\nexport function Providers({ children }: { children: React.ReactNode }) {\n return (\n <ThemeProvider\n attribute=\"class\"\n defaultTheme=\"system\"\n enableSystem\n disableTransitionOnChange\n >\n <AuthProvider>\n {children}\n <Toaster position=\"top-center\" richColors closeButton />\n <\/AuthProvider>\n <\/ThemeProvider>\n );\n}\n```\n\n**Usage:** Wrap in `app/[locale]/layout.tsx` or `app/layout.tsx`\n\n---\n\n## API Integration Pattern\n\nAll feature components follow a consistent mutation pattern:\n\n```tsx\nasync function submit() {\n setSaving(true);\n try {\n await vendorsApi.mutation(projectId, { ...payload });\n toast.success(t(\"success\"));\n setOpen(false);\n resetAll();\n await onDone();\n } catch {\n toast.error(t(\"error\"));\n } finally {\n setSaving(false);\n }\n}\n```\n\n**Key Points:**\n1. Set loading state before API call\n2. Show toast on success/error\n3. Reset form state\n4. Trigger parent refresh callback\n5. Always clear loading state in finally block\n\n---\n\n## Internationalization\n\nAll components use `useTranslations` from `next-intl`:\n\n```tsx\nconst t = useTranslations(\"board\");\nconst tCommon = useTranslations(\"common\");\n\n// Usage\n<h3>{t(\"title\")}<\/h3>\n<p>{tCommon(\"requiredField\")}<\/p>\n```\n\n**Translation Keys:**\n- `board`: Board-specific strings\n- `asks`: Ask-related strings\n- `members`: Member management strings\n- `milestones`: Milestone strings\n- `deliverables`: Deliverable strings\n- `audit`: Audit trail strings\n- `common`: Shared UI strings\n- `vendorCodes`: Machine code translations\n\n---\n\n## Error Handling\n\n### ErrorState\nReusable error display with retry option.\n\n```tsx\nexport function ErrorState({\n message,\n onRetry,\n}: {\n message: string;\n onRetry?: () => void;\n}) {\n return (\n <div className=\"border-border flex flex-col items-center justify-center gap-3 rounded-xl border border-dashed px-6 py-14 text-center\">\n <div className=\"bg-danger/10 text-danger flex size-11 items-center justify-center rounded-full\">\n <AlertTriangle className=\"size-5\" />\n <\/div>\n <p className=\"text-muted-foreground max-w-sm text-sm\">{message}<\/p>\n {onRetry ? (\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry}>\n {t(\"retry\")}\n <\/Button>\n ) : null}\n <\/div>\n );\n}\n```\n\n### NotEnabledState\nFeature disabled state (e.g., feature flags).\n\n```tsx\nexport function NotEnabledState() {\n return (\n <div className=\"flex min-h-[60vh] items-center justify-center px-6\">\n <div className=\"max-w-md space-y-3 text-center\">\n <div className=\"bg-muted text-muted-foreground mx-auto flex size-12 items-center justify-center rounded-full\">\n <ShieldOff className=\"size-6\" />\n <\/div>\n <h1 className=\"text-xl font-semibold\">{t(\"title\")}<\/h1>\n <p className=\"text-muted-foreground text-sm leading-relaxed\">{t(\"body\")}<\/p>\n <\/div>\n <\/div>\n );\n}\n```\n\n---\n\n## Component Dependencies\n\n```mermaid\ngraph LR\n A[Feature Components] --> B[Common UI]\n B --> C[UI Primitives]\n D[Shell Components] --> B\n E[Providers] --> D\n F[API Layer] -.-> A\n G[Translations] -.-> A\n H[Format Library] -.-> A\n```\n\n**Key Dependencies:**\n- `vendorsApi`: All data mutations\n- `useTranslations`: All text content\n- `format`: Date/time/number formatting\n- `cn`: Utility class merger\n- `sonner`: Toast notifications\n\n---\n\n## Best Practices\n\n1. **Always use `useTranslations`** for all user-facing text\n2. **Keep forms in Dialogs** for mutations\n3. **Use `PlainText`** for untrusted cross-company text\n4. **Set loading states** before API calls\n5. **Reset form state** after submission\n6. **Call `onDone()`** after successful mutations\n7. **Use `EmptyState`** for empty lists\n8. **Use `ErrorState`** for load errors\n9. **Use `Badge` variants** consistently for status\n10. **Use `dir=\"auto\"`** for mixed-language content\n\n---\n\n## File Structure\n\n```\nHarakat_Office/components/\n├── board/\n│ ├── board-tab.tsx\n│ ├── chain-pill.tsx\n│ ├── member-card.tsx\n│ ├── narrated-block.tsx\n│ ├── note-composer.tsx\n│ └── signal-tile.tsx\n├── project/\n│ ├── asks-tab.tsx\n│ ├── audit-tab.tsx\n│ ├── deliverables-tab.tsx\n│ ├── member-select.tsx\n│ ├── members-tab.tsx\n│ ├── milestones-tab.tsx\n│ └── project-actions.tsx\n├── common/\n│ ├── empty-state.tsx\n│ ├── page-header.tsx\n│ ├── plain-text.tsx\n│ ├── states.tsx\n│ ├── status-badges.tsx\n│ └── vendor-code.tsx\n├── shell/\n│ ├── app-footer.tsx\n│ ├── locale-switcher.tsx\n│ ├── theme-toggle.tsx\n│ ├── top-bar.tsx\n│ └── user-menu.tsx\n├── ui/\n│ ├── badge.tsx\n│ ├── button.tsx\n│ ├── card.tsx\n│ ├── dialog.tsx\n│ ├── dropdown-menu.tsx\n│ ├── input.tsx\n│ ├── label.tsx\n│ ├── select.tsx\n│ ├── separator.tsx\n│ ├── skeleton.tsx\n│ ├── sonner.tsx\n│ ├── table.tsx\n│ ├── tabs.tsx\n│ └── textarea.tsx\n├── providers.tsx\n├── theme-provider.tsx\n└── projects/\n └── create-project-dialog.tsx\n```","harakat-office-harakat-office":"# Harakat_Office — Harakat_Office\n\n\n\n# Harakat_Office Configuration Module\n\n## Overview\nThe `Harakat_Office` module serves as the foundational configuration layer for the Next.js application. It does not contain business logic or API endpoints; instead, it defines the runtime environment, internationalization (i18n) strategy, security policies, and build tooling required for the application to function.\n\nThis module consists primarily of configuration files (`next.config.mjs`, `middleware.ts`, `tsconfig.json`) and dependency definitions (`package.json`).\n\n## Architecture\n\n### Internationalization (i18n)\nThe application uses `next-intl` to manage localization. The setup is split between the middleware and the Next.js configuration.\n\n1. **Middleware (`middleware.ts`)**:\n * Imports `createMiddleware` from `next-intl/middleware`.\n * Passes the routing configuration from `./i18n/routing` to the middleware.\n * **Matcher**: The `config` object defines the `matcher` array. It intercepts all pathnames except:\n * API routes (`/api`)\n * Next.js internals (`/_next`, `/_vercel`)\n * Static files (files containing a dot, e.g., `favicon.ico`)\n\n2. **Next.js Configuration (`next.config.mjs`)**:\n * Imports `createNextIntlPlugin` from `next-intl/plugin`.\n * Initializes the plugin with the request handler from `./i18n/request.ts`.\n * Wraps the default `nextConfig` with `withNextIntl`.\n\n### Security Headers\nThe `next.config.mjs` file applies security headers globally to all requests via the `headers` async function.\n\n```javascript\nheaders: async () => [\n {\n source: \"/(.*)\",\n headers: [\n { key: \"X-Content-Type-Options\", value: \"nosniff\" },\n { key: \"X-Frame-Options\", value: \"DENY\" },\n { key: \"X-XSS-Protection\", value: \"1; mode=block\" },\n { key: \"Referrer-Policy\", value: \"strict-origin-when-cross-origin\" },\n { key: \"Permissions-Policy\", value: \"geolocation=(), microphone=(), camera=()\" },\n ],\n },\n]\n```\n\n### Build & Tooling\n* **TypeScript (`tsconfig.json`)**:\n * Configures `paths` to map `@/*` to `./*`.\n * Enables `strict` mode and `isolatedModules`.\n * Includes the Next.js plugin for type checking.\n* **Styling (`postcss.config.mjs`)**:\n * Configures Tailwind CSS v4 via `@tailwindcss/postcss`.\n* **Dependencies (`package.json`)**:\n * Defines scripts for development (`dev`), building (`build`), and linting (`lint`).\n * Includes a custom script `check:i18n` to validate internationalization files.\n\n## Request Lifecycle\n\nWhen a request is made to the application, the following flow occurs regarding the configuration module:\n\n1. **Routing**: The `next-intl` middleware intercepts the request based on the `matcher` defined in `middleware.ts`.\n2. **Localization**: The middleware determines the locale and redirects or rewrites the URL if necessary.\n3. **Rendering**: The request proceeds to the Next.js router, which applies the security headers defined in `next.config.mjs`.\n\n```mermaid\nsequenceDiagram\n participant Client\n participant Middleware\n participant NextJS\n participant App\n\n Client->>Middleware: Request\n Middleware->>Middleware: Check Matcher\n alt Excluded Path\n Middleware-->>Client: Pass Through\n else Included Path\n Middleware->>Middleware: Apply Locale\n Middleware-->>NextJS: Rewritten URL\n end\n NextJS->>App: Render Page\n NextJS-->>Client: Response + Security Headers\n```\n\n## Key Components\n\n### `middleware.ts`\n* **Function**: `createMiddleware(routing)`\n* **Purpose**: Handles URL rewriting and locale detection.\n* **Config**: `matcher` excludes API and static assets to prevent i18n interference.\n\n### `next.config.mjs`\n* **Function**: `withNextIntl(nextConfig)`\n* **Purpose**: Integrates `next-intl` into the Next.js build process.\n* **Settings**:\n * `outputFileTracingRoot`: Set to `__dirname` to isolate the tracing root for monorepo compatibility.\n * `images.unoptimized`: Set to `true` to disable Next.js image optimization.\n * `eslint.ignoreDuringBuilds`: Set to `true` to allow builds to proceed despite linting errors.\n\n### `package.json`\n* **Scripts**:\n * `dev`: Runs `next dev` on port 3005.\n * `build`: Runs `next build`.\n * `start`: Runs `next start` on port 3005.\n * `check:i18n`: Executes `node scripts/check-i18n.mjs`.\n\n## Execution Flow\nThe module itself contains no runtime execution logic. Its \"execution\" is static configuration applied at build time or request interception time.\n\n* **Build Time**: `next.config.mjs` and `tsconfig.json` are read by the Next.js compiler and TypeScript compiler respectively.\n* **Runtime**: `middleware.ts` is executed by the Next.js server for every incoming HTTP request matching the `matcher`.\n\n## Contributing Guidelines\n1. **i18n**: Any changes to routing or locale handling must be reflected in `middleware.ts` and `./i18n/routing`.\n2. **Security**: Do not remove security headers defined in `next.config.mjs` without a security review.\n3. **Paths**: Use the `@/*` alias defined in `tsconfig.json` for imports.\n4. **Linting**: ESLint is ignored during builds (`ignoreDuringBuilds: true`), but `npm run lint` should be run before committing.","harakat-office-i18n":"# Harakat_Office — i18n\n\n\n\n# Harakat_Office — i18n\n\nThis module configures internationalization (i18n) for the Harakat_Office application using the `next-intl` library. It centralizes locale definitions, handles server-side request configuration for locale detection, and provides locale-aware navigation utilities for the client.\n\n## Overview\n\nThe module consists of three core files:\n1. **`routing.ts`**: Defines supported locales, default behavior, and text direction.\n2. **`request.ts`**: Configures server-side locale resolution and message loading.\n3. **`navigation.ts`**: Wraps Next.js navigation APIs to ensure locale prefixes are preserved.\n\nTogether, these files ensure that all user interactions and server responses respect the selected language (English or Arabic) and maintain correct URL structures.\n\n## Routing Configuration\n\nThe routing configuration is defined in `routing.ts`. This file serves as the single source of truth for supported languages and URL structure.\n\n```typescript\nexport const routing = defineRouting({\n locales: [\"en\", \"ar\"],\n defaultLocale: \"en\",\n localePrefix: \"always\",\n});\n```\n\n### Key Settings\n* **`locales`**: An array of supported language codes. Currently supports `en` (English) and `ar` (Arabic).\n* **`defaultLocale`**: The fallback locale if no specific locale is detected in the request. Defaults to `en`.\n* **`localePrefix`**: Set to `\"always\"`. This enforces the `/{locale}` prefix on all URLs (e.g., `/en/about`, `/ar/about`).\n\n### Type Definitions\n* **`Locale`**: A type alias derived from `routing.locales`. This ensures type safety when referencing language codes throughout the application.\n* **`localeDirection`**: A record mapping locales to their text direction (`\"ltr\"` or `\"rtl\"`). This is essential for rendering Arabic content correctly.\n\n## Request Configuration\n\nServer-side locale detection is handled in `request.ts`. This file implements `getRequestConfig` from `next-intl/server`.\n\n### Execution Flow\nWhen a request is made to the server, the following logic determines the active locale:\n\n1. **Request Locale**: The `requestLocale` parameter is extracted from the incoming request headers.\n2. **Validation**: `hasLocale(routing.locales, requested)` checks if the requested locale is valid.\n3. **Fallback**: If the requested locale is invalid, `routing.defaultLocale` is used.\n4. **Message Loading**: The corresponding translation file (`../messages/${locale}.json`) is dynamically imported.\n\n```typescript\nexport default getRequestConfig(async ({ requestLocale }) => {\n const requested = await requestLocale;\n const locale = hasLocale(routing.locales, requested)\n ? requested\n : routing.defaultLocale;\n\n return {\n locale,\n messages: (await import(`../messages/${locale}.json`)).default,\n };\n});\n```\n\n### Architecture Diagram\nThe following diagram illustrates how a request is processed to determine the locale and load translations.\n\n```mermaid\nsequenceDiagram\n participant Client\n participant Server\n participant RequestConfig\n participant Routing\n participant Messages\n\n Client->>Server: HTTP Request\n Server->>RequestConfig: getRequestConfig(requestLocale)\n RequestConfig->>Routing: hasLocale(requested, locales)\n alt Valid Locale\n RequestConfig->>RequestConfig: Use requested locale\n else Invalid Locale\n RequestConfig->>Routing: Get defaultLocale\n RequestConfig->>RequestConfig: Use default locale\n end\n RequestConfig->>Messages: Import messages/${locale}.json\n RequestConfig-->>Server: { locale, messages }\n Server-->>Client: Rendered Response\n```\n\n## Navigation Utilities\n\nClient-side navigation is handled in `navigation.ts`. It uses `createNavigation` from `next-intl/navigation` to wrap standard Next.js navigation functions.\n\n### Why Use Wrappers?\nUsing raw `next/navigation` exports (e.g., `next/link`) can result in broken links or incorrect redirects when the locale prefix (`/{locale}`) is missing. The wrappers in this module ensure that all navigation actions respect the `localePrefix: \"always\"` setting defined in `routing.ts`.\n\n### Exported Functions\n* **`Link`**: A wrapper around `next/link`. Use this for all internal links to ensure the locale prefix is included.\n* **`redirect`**: A wrapper around `next/redirect`. Ensures redirects preserve the locale.\n* **`usePathname`**: Hook to retrieve the current pathname including the locale prefix.\n* **`useRouter`**: Hook to access the router instance with locale-aware methods.\n* **`getPathname`**: Utility function to get the current pathname.\n\n### Usage Example\n```typescript\nimport { Link, useRouter } from \"@/i18n/navigation\";\n\nexport default function Page() {\n const router = useRouter();\n\n // Correct: Preserves /en or /ar prefix\n return (\n <Link href=\"/about\">About<\/Link>\n );\n}\n```\n\n## Integration with the Codebase\n\n### Message Files\nThe `request.ts` module expects translation files located in `../messages/`. The filename must match the locale code (e.g., `en.json`, `ar.json`).\n\n### Component Usage\nWhen building components that require locale-specific behavior:\n1. Import `localeDirection` from `routing` to apply CSS classes for RTL support.\n2. Import `Link` from `navigation` instead of `next/link`.\n3. Use `useRouter` or `usePathname` to handle programmatic navigation.\n\n### Type Safety\nImport the `Locale` type from `routing` to ensure consistency across the application:\n\n```typescript\nimport type { Locale } from \"@/i18n/routing\";\n```","harakat-office-lib":"# Harakat_Office — lib\n\n\n\n# Harakat_Office — lib\n\nThis module encapsulates the client-side logic for the Harakat Office application. It provides a typed REST client, authentication management, project data fetching, and utility functions for formatting and localization. The module is designed to mirror the backend's `StartupsPlatform` pattern while maintaining its own token keys and login routes.\n\n## Architecture\n\nThe module follows a layered architecture:\n1. **Network Layer (`client.ts`)**: Handles HTTP requests, token injection, and error handling.\n2. **API Layer (`auth.ts`, `vendors.ts`)**: Typed wrappers for specific endpoints.\n3. **State Layer (`auth-context.tsx`)**: Manages global authentication state and session persistence.\n4. **Data Layer (`use-project-data.ts`)**: Custom hooks for fetching project-specific data.\n5. **Utilities (`format.ts`, `vendor-codes.ts`, `utils.ts`)**: Helpers for formatting, code mapping, and class merging.\n\n```mermaid\nsequenceDiagram\n participant UI\n participant Context as AuthContext\n participant Hook as useProjectData\n participant API as vendorsApi/authApi\n participant Client as apiClient\n participant Server\n\n UI->>Context: useAuth()\n Context->>Client: request()\n Client->>Server: HTTP Request (Bearer Token)\n Server-->>Client: Response\n Client-->>Context: Update Session/User\n Context-->>UI: User State\n UI->>Hook: useProjectData(id)\n Hook->>API: listAsks, getBoard, etc.\n API->>Client: apiClient.get/post\n Client->>Server: HTTP Request\n Server-->>Client: Response\n Client-->>Hook: Data\n Hook-->>UI: ProjectData\n```\n\n## API Client & Network Layer\n\nThe core of the module is `client.ts`, which defines the `apiClient` singleton and the `request` function.\n\n### Configuration\n* **API URL**: Determined by `process.env.NEXT_PUBLIC_API_URL` or defaults to `http://localhost:5000` in development.\n* **Token Storage**: Uses `sessionStorage` with keys `officeAccessToken` and `officeUser`.\n* **Locale**: Detects the active locale (`en` or `ar`) from the URL pathname to ensure redirects and formatting remain consistent.\n\n### Request Handling\nThe `request` function handles all HTTP interactions:\n1. **Authentication**: Automatically attaches the `Authorization: Bearer <token>` header if a token exists in `sessionStorage`.\n2. **Error Handling**:\n * **401 Unauthorized**: If the request is not to a login endpoint and the status is 401, the client clears the session and redirects the user to the login page (`/${currentLocale()}/login`).\n * **404 Not Found**: Checks for the specific error string `\"not enabled\"` to determine if the vendors platform is disabled.\n * **Other Errors**: Extracts error messages from the response body (`message`, `error`, or `msg`) and throws an `ApiError`.\n3. **Response Parsing**: Handles 204 No Content responses by returning an empty object.\n\n### API Methods\nThe `apiClient` object exposes standard HTTP methods:\n* `get<T>(path)`: Fetches data.\n* `post<T>(path, body)`: Sends POST requests with JSON stringified bodies.\n* `patch<T>(path, body)`: Sends PATCH requests.\n* `delete<T>(path)`: Sends DELETE requests.\n\n## Authentication\n\nAuthentication is split between the raw API calls (`auth.ts`) and the React Context (`auth-context.tsx`).\n\n### API Layer (`auth.ts`)\n* `login(email, password)`: Initiates the login process. Returns a `LoginResponse`.\n* `complete2FA(challengeToken, code)`: Completes the 2FA challenge using the token from the initial login response and the OTP code.\n\n### Context Layer (`auth-context.tsx`)\nThe `AuthProvider` wraps the application to provide the `useAuth` hook.\n* **State Management**: Tracks `user` and `isLoading` state.\n* **Session Persistence**:\n * On mount, it checks `sessionStorage` for existing tokens and user data.\n * The `storeSession` function is the single source of truth for writing a session (both initial login and 2FA completion).\n* **Login Flow**:\n 1. Calls `authApi.login`.\n 2. If `response.token` and `response.user` exist, it stores the session and returns `{ kind: \"signed-in\" }`.\n 3. If `response.requires2FA` is true, it returns `{ kind: \"needs-2fa\", challengeToken: ... }` without persisting a session.\n 4. Throws an error on failure.\n* **2FA Completion**: Calls `authApi.complete2FA`. If successful, it calls `storeSession` to finalize the login.\n\n## Vendor Data & Hooks\n\n### API Layer (`vendors.ts`)\nThis module exposes the `vendorsApi` object, wrapping endpoints under `/api/vendors`.\n* **Projects**: `listProjects`, `createProject`, `getProject`, `updateProject`.\n* **Members**: `inviteMember`, `removeMember`.\n* **Milestones**: `createMilestone`, `updateMilestone`.\n* **Deliverables**: `createDeliverable`, `updateDeliverable`.\n* **Notes**: `addNote` (Coordinator-authored only).\n* **Asks**: `listAsks`, `createAsk`.\n* **Board & Audit**: `getBoard`, `getAudit`.\n\n### Data Hook (`use-project-data.ts`)\nThe `useProjectData` hook is the primary entry point for project details in the UI.\n* **Parallel Fetching**: Uses `Promise.allSettled` to fetch `board`, `detail`, `asks`, and `audit` simultaneously.\n* **Error Tolerance**:\n * **Critical Errors**: If `board` or `detail` fails (and is not \"not enabled\"), the status becomes `error`.\n * **Non-Critical Errors**: If `asks` or `audit` fail, the status remains `ready`, but specific error flags (`asksError`, `auditError`) are set to `true`.\n * **Platform Disabled**: If any request returns a 404 with the `\"not enabled\"` error, the status becomes `not-enabled`.\n* **Refresh**: The `refresh` callback re-fetches all data, resetting the status to `loading` first.\n\n## Utilities & Localization\n\n### Formatting (`format.ts`)\nProvides locale-aware formatting functions.\n* **Numbering**: Uses Latin digits (`nu-latn`) for consistency across Arabic and English locales.\n* **Date/Time**:\n * `formatDate`: Formats ISO strings to `YYYY MMM DD`.\n * `formatDateTime`: Includes hours and minutes.\n * `formatRelative`: Returns relative time (e.g., \"2 days ago\") using `Intl.RelativeTimeFormat`.\n * `daysUntil`: Returns the number of days remaining until a date.\n * `isOverdue`: Checks if a date has passed.\n* **Display Name**: `displayName` handles null/undefined values by returning a neutral dash (`—`).\n\n### Vendor Codes (`vendor-codes.ts`)\nMaps server-generated frozen strings to i18n message keys.\n* **Suppression Reasons**: Maps `Ask.suppressionReason` values (e.g., `\"Withheld by the vendor's policy\"`) to keys like `\"withheldByPolicy\"`. Returns `null` for unknown values to allow raw rendering.\n* **Audit Actions**: Maps `AuditEvent.action` values (e.g., `\"project.created\"`) to keys like `\"projectCreated\"`.\n* **Lookup**: Uses `Map` objects for O(1) lookup performance.\n\n### Class Merging (`utils.ts`)\n* `cn(...inputs)`: A wrapper around `clsx` and `tailwind-merge` for combining class names.\n\n## Error Handling & Security\n\n### Session Security\n* **Token Expiry**: The `request` function in `client.ts` automatically clears `sessionStorage` and redirects to the login page upon receiving a 401 response (excluding login endpoints).\n* **2FA Flow**: The `auth-context` ensures that a session is never partially written. If the first login leg returns a challenge, no token is stored until `completeTwoFactor` succeeds.\n\n### Error Classification\n* `ApiError`: Custom error class extending `Error` with `status` and `data` properties.\n* `isNotEnabled`: Helper function to detect if the vendors platform is disabled (404 with specific error string).\n\n### Data Integrity\n* **Partial Updates**: The `updateProject` and `updateMilestone` functions rely on the server's behavior where only present keys are updated, preventing unintended side effects (e.g., renaming a project does not change its status).\n* **Free-Text Passthrough**: In `vendor-codes.ts`, unknown server strings are returned as `null` from lookup functions, allowing the UI to render the raw string in a quarantine state rather than mislabeling it.","harakat-office-messages":"# Harakat_Office — messages\n\n\n\n# Harakat_Office — messages\n\n## Overview\n\nThe `Harakat_Office — messages` module serves as the localization (i18n) layer for the Harakat Office application. It provides all user-facing text strings in two supported languages: English (`en.json`) and Arabic (`ar.json`).\n\nThis module is a static data resource. It contains no internal logic, functions, or execution flows. Its sole purpose is to store translatable keys and values that are consumed by the application's UI components to render text dynamically based on the user's locale.\n\n## File Structure\n\nThe module consists of two JSON files located in the `messages` directory:\n\n* `messages/en.json`: English localization strings.\n* `messages/ar.json`: Arabic localization strings.\n\nBoth files share an identical key structure to ensure consistency across languages.\n\n## Data Schema & Patterns\n\nThe JSON structure is organized into nested objects representing specific UI contexts. Each key maps to a string value.\n\n### Key Naming Convention\nKeys follow a hierarchical naming convention: `context.subcontext.key`.\n* **Context:** The top-level domain (e.g., `login`, `projects`, `common`).\n* **Subcontext:** A specific section within the domain (e.g., `login.twoFactorTitle`).\n* **Key:** The specific UI element text (e.g., `login.submit`).\n\n### Interpolation\nStrings may contain placeholders for dynamic data. These are denoted by curly braces `{}`.\n* **Examples:** `{email}`, `{name}`, `{date}`, `{time}`, `{count}`.\n* **Usage:** The application's i18n engine replaces these placeholders with actual values at runtime.\n\n### Pluralization\nThe module supports ICU-style pluralization for dynamic counts. This is critical for accurate localization of numbers.\n* **Syntax:** `{count, plural, =0 {zero} one {one} other {other}}`\n* **Examples:**\n * `projects.memberCount`\n * `projects.openAsks`\n * `board.chainEvents`\n * `milestones.milestonesDone`\n\n### Error Codes & Audit Actions\nSpecific sections handle system-level feedback and audit logging:\n* `vendorCodes.suppression`: Defines reasons why a vendor's response might be suppressed (e.g., `withheldByPolicy`, `linkRevoked`).\n* `vendorCodes.auditAction`: Defines actions recorded in the audit trail (e.g., `projectCreated`, `askFired`).\n\n## Domain Breakdown\n\nThe following sections define the scope of the localization data:\n\n### Application Core\n* `app`: Application name and tagline.\n* `common`: Generic UI actions (Save, Cancel, Loading, Error).\n* `locale`: Language switching options.\n* `footer`: Legal disclaimers and posture statements.\n\n### Authentication\n* `login`: Fields for email, password, and two-factor verification.\n* `notEnabled`: Messages when the platform is disabled for an organization.\n* `guard`: Session checking status.\n\n### Project Management\n* `projects`: List view of coordinated projects.\n* `project`: Single project view, including editing and archiving.\n* `board`: The \"Holdup board\" displaying vendor status and signals.\n* `members`: Vendor invitation and revocation management.\n* `audit`: The immutable audit trail log.\n\n### Coordination Tools\n* `milestones`: Project timeline tracking.\n* `deliverables`: Expected outputs and status.\n* `asks`: Questions sent to vendor assistants.\n* `notes`: Internal coordinator notes attached to projects or vendors.\n\n## Integration & Usage\n\nThis module is consumed by the application's UI layer. While the module itself contains no logic, the application uses the keys defined here to render text.\n\n### Execution Flow\nBased on the call graph analysis, this module has **no internal execution flows**. It does not make outgoing calls, nor does it receive incoming calls. It is a passive data store.\n\n1. **Initialization:** The application loads `messages/en.json` or `messages/ar.json` based on the user's browser locale.\n2. **Rendering:** UI components request strings using the key paths (e.g., `t('login.submit')`).\n3. **Substitution:** The i18n engine resolves placeholders (e.g., `{email}`) and plural forms (e.g., `{count, plural...}`).\n\n### Connection to Codebase\nThe module connects to the rest of the codebase via the **UI Layer**. Any component requiring text must reference a key from this module. There are no direct dependencies on other logic modules within this file.\n\n## Maintenance Guidelines\n\nWhen adding new strings to this module, adhere to the following rules:\n\n1. **Consistency:** Ensure the key path exists in both `en.json` and `ar.json`.\n2. **Placeholders:** Use `{key}` for dynamic values. Do not hardcode values.\n3. **Pluralization:** If a string depends on a count, use the ICU plural format.\n4. **Context:** Group related strings under the same context (e.g., all login strings under `login`).\n5. **Audit Actions:** New actions that modify the system state must be added to `vendorCodes.auditAction` to ensure they are logged correctly.\n\n## Localization Architecture\n\nThe following diagram illustrates how this module fits into the broader application architecture.\n\n```mermaid\nflowchart TD\n App[Application UI] -->|Requests Text| I18n[Translation Engine]\n I18n -->|Loads| EN[en.json]\n I18n -->|Loads| AR[ar.json]\n EN -->|Provides| App\n AR -->|Provides| App\n App -->|Logs| Audit[Audit Trail]\n Audit -->|Reads| EN\n Audit -->|Reads| AR\n```\n\n* **Application UI:** Components that render text.\n* **Translation Engine:** Resolves keys to values and handles interpolation/pluralization.\n* **en.json / ar.json:** The static data sources in this module.\n* **Audit Trail:** Reads from this module to display localized audit messages.","harakat-office-scripts":"# Harakat_Office — scripts\n\n\n\n# Harakat_Office/scripts/check-i18n.mjs\n\n## Overview\n`check-i18n.mjs` is a standalone validation script located within the `Harakat_Office/scripts` directory. Its primary responsibility is to enforce consistency between the English (`en`) and Arabic (`ar`) message catalogs. It ensures that every user-facing string key exists in both languages with an identical hierarchical path, preventing translation drift.\n\n## Purpose\nThis script is critical for maintaining localization integrity. It verifies that:\n1. **Key Parity:** The set of keys in `en.json` matches the set of keys in `ar.json`.\n2. **Structural Integrity:** Nested objects are flattened into consistent dot-notation paths.\n3. **Count Consistency:** The total number of keys in both catalogs is identical.\n\nIf discrepancies are found, the script exits with a non-zero status code to signal a build or deployment failure.\n\n## Dependencies\nThe script relies exclusively on Node.js built-in modules:\n* `node:fs` (via `readFileSync`)\n* `node:url` (via `fileURLToPath`)\n* `node:path` (via `dirname`, `join`)\n\nIt does not import any external packages or other project modules.\n\n## Core Components\n\n### `load(locale)`\nReads and parses the JSON message catalog for a specific locale.\n* **Input:** `locale` (string, e.g., `\"en\"` or `\"ar\"`).\n* **Path:** Constructs the path `messages/{locale}.json` relative to the script's directory.\n* **Output:** A parsed JavaScript object representing the message catalog.\n\n### `flatten(obj, prefix = \"\")`\nA recursive helper function that converts a nested object structure into a flat array of string paths.\n* **Logic:** Iterates through object entries. If a value is a non-array object, it recurses with the current key appended to the `prefix`. If the value is a primitive, it pushes the full path to the output array.\n* **Output:** A sorted array of dot-notation strings (e.g., `[\"app.title\", \"app.settings.language\"]`).\n\n### Main Execution Logic\nThe script orchestrates the validation process:\n1. Loads both `en` and `ar` catalogs.\n2. Flattens both into sorted arrays.\n3. Converts arrays to Sets for efficient lookup.\n4. Identifies keys missing in either language.\n5. Compares the total key counts.\n6. Logs results and exits with code `1` on failure or `0` on success.\n\n## Execution Flow\n\nThe script follows a linear validation pipeline. It loads data, normalizes the structure, compares the sets, and reports the outcome.\n\n```mermaid\nflowchart TD\n Start[Start Script] --> LoadEN[Load messages/en.json]\n LoadEN --> FlattenEN[Flatten EN Keys]\n LoadEN --> LoadAR[Load messages/ar.json]\n LoadAR --> FlattenAR[Flatten AR Keys]\n \n FlattenEN --> SortEN[Sort EN Array]\n FlattenAR --> SortAR[Sort AR Array]\n \n SortEN --> CreateSetEN[Create EN Set]\n SortAR --> CreateSetAR[Create AR Set]\n \n CreateSetEN --> Compare[Compare Sets]\n CreateSetAR --> Compare\n \n Compare --> CheckMissing{Missing Keys?}\n CheckMissing -- Yes --> LogError[Log Missing Keys]\n CheckMissing -- No --> CheckCount{Counts Match?}\n \n LogError --> ExitFail[Exit Code 1]\n CheckCount -- No --> LogCountError[Log Count Mismatch]\n CheckCount -- Yes --> LogSuccess[Log Parity OK]\n LogCountError --> ExitFail\n LogSuccess --> ExitSuccess[Exit Code 0]\n```\n\n## Usage\n\nTo run the validation script, execute it via Node.js from the project root or the scripts directory:\n\n```bash\nnode scripts/check-i18n.mjs\n```\n\n### Output Behavior\n* **Success:** Prints `i18n parity OK — en and ar match.` and exits with code `0`.\n* **Failure:** Prints specific error messages (e.g., `Missing in ar: [...]`) or `Key counts differ.` and exits with code `1`.\n\n## Integration with Codebase\n* **Incoming Calls:** None. This script is not imported or called by other modules.\n* **Outgoing Calls:** None. It does not trigger other scripts or external services.\n* **Data Flow:** Reads from the `messages` directory (relative to the script location) and writes to `stdout`.\n\n## Maintenance Notes\n* **File Paths:** The script assumes the message files are located at `messages/en.json` and `messages/ar.json` relative to the `scripts` directory.\n* **Recursive Flattening:** When adding new nested message keys, ensure the JSON structure is preserved. The `flatten` function handles arbitrary nesting depth automatically.\n* **Sorting:** Both key lists are sorted before comparison to ensure deterministic output during debugging.","harakat-office-types":"# Harakat_Office — types\n\n\n\n# Harakat_Office — types\n\n## Overview\n\nThis module (`vendors.ts`) defines the shared TypeScript type contracts for the **Coordinator Cockpit** API (`/api/vendors/*`). It serves as the single source of truth for the shapes the server returns to the client.\n\n**Critical Constraints:**\n* **Server Sync:** The server module is built in parallel. This file must be kept in lockstep with the server implementation.\n* **ID Handling:** All IDs are `number` (Prisma `Int` autoincrement primary keys). Route parameters arriving from the URL are strings and **must** be parsed with `Number()` at the boundary before being passed into these shapes or the API client.\n* **No Execution:** This module contains no logic, only type definitions. It is imported by other modules to validate data structures.\n\n## Core Constants\n\nThe following types define the allowed values for status, roles, and actor kinds across the system.\n\n```typescript\nexport type ProjectStatus = \"ACTIVE\" | \"ARCHIVED\";\nexport type MemberRole = \"COORDINATOR\" | \"VENDOR\";\nexport type MemberStatus = \"INVITED\" | \"ACTIVE\" | \"REVOKED\";\nexport type MilestoneStatus = \"PENDING\" | \"IN_PROGRESS\" | \"BLOCKED\" | \"DONE\";\nexport type AskStatus =\n | \"PENDING\"\n | \"ANSWERED\"\n | \"SUPPRESSED\"\n | \"UNANSWERED\"\n | \"FAILED\";\nexport type ActorKind = \"HUMAN\" | \"AGENT\" | \"SYSTEM\";\nexport type NoteSource = \"COORDINATOR\" | \"VENDOR_ASSISTANT\" | \"VENDOR_HUMAN\";\n```\n\n**Note on `NoteSource`:**\n* `COORDINATOR`: Notes written by the platform owner.\n* `VENDOR_ASSISTANT`: The vendor's assistant answered over the link.\n* `VENDOR_HUMAN`: A person at the vendor answered it themselves (v1 answer leg).\n* *Quarantine:* Both vendor sources are cross-company text and stay quarantined; the cockpit labels which one you are reading.\n\n## Authentication & Session\n\nThis section defines the shapes for user authentication, including standard login and 2FA flows.\n\n### OfficeUser\nRepresents an authenticated user. Includes a nested `startup` object for context.\n\n```typescript\nexport interface OfficeUser {\n id: number;\n name: string;\n email: string;\n role: string;\n level: number;\n startupId: number;\n startup: {\n id: number;\n name: string;\n [key: string]: unknown;\n };\n}\n```\n\n### Login Flows\nThe login process is conditional based on 2FA configuration.\n\n1. **Initial Request (`POST /api/startup-auth/login`):**\n * Returns `LoginResponse`.\n * **Success:** Contains `token` and `user`.\n * **2FA Challenge:** If 2FA is enabled, returns `requires2FA: true` and `challengeToken`. The `token` and `user` are **absent** in this case.\n * **Failure:** Returns `ok: false` with an `error` string.\n\n2. **2FA Completion (`POST /api/startup-auth/login/2fa`):**\n * Requires `{ challengeToken, code }`.\n * Returns `TwoFactorLoginResponse`.\n * **Success:** Mirrors the non-2FA branch (`{ ok, token, user }`).\n * **Failure:** Returns 401 with an `error` string.\n * **Timing:** Challenge JWT lives 15 minutes; emailed 6-digit code lives 10 minutes.\n\n3. **Client-Side Result (`AuthContext.login()`):**\n * Resolves to `LoginResult`.\n * `signed-in`: Session written to `sessionStorage`.\n * `needs-2fa`: Returns `challengeToken` for the user to complete.\n\n```typescript\nexport interface LoginResponse {\n ok: boolean;\n token?: string;\n user?: OfficeUser;\n requires2FA?: boolean;\n challengeToken?: string;\n message?: string;\n error?: string;\n}\n\nexport interface TwoFactorLoginResponse {\n ok: boolean;\n token?: string;\n user?: OfficeUser;\n message?: string;\n error?: string;\n}\n\nexport type LoginResult =\n | { kind: \"signed-in\" }\n | { kind: \"needs-2fa\"; challengeToken: string; message?: string };\n```\n\n## Project Management\n\nDefines structures for projects, members, milestones, and deliverables.\n\n### Project Structures\n* `ProjectListItem`: Summary view (includes `memberCount`, `openAsks`).\n* `ProjectDetail`: Full view (includes `description`, `status`).\n* `ProjectListResponse` / `ProjectResponse`: Wrapper interfaces for API endpoints.\n\n```typescript\nexport interface ProjectListItem {\n id: number;\n name: string;\n description: string | null;\n status: ProjectStatus;\n memberCount: number;\n openAsks: number;\n createdAt: string;\n}\n\nexport interface ProjectDetail {\n id: number;\n name: string;\n description: string | null;\n status: ProjectStatus;\n createdAt: string;\n}\n```\n\n### Members\nRepresents a user's relationship to a project.\n\n* **`startupName`**: Nullable. Derived from the joined `Startup` row (`?? null`).\n* **`invitedEmail`**: Nullable. Server field name is `invitedEmail` (request body field is `inviteEmail`).\n* **`linkTokenId`**: Nullable. Link to `DelegationToken`.\n\n```typescript\nexport interface Member {\n id: number;\n startupId: number;\n startupName: string | null;\n invitedEmail?: string | null;\n role: MemberRole;\n status: MemberStatus;\n linkTokenId?: number | null;\n invitedAt?: string | null;\n joinedAt?: string | null;\n revokedAt?: string | null;\n}\n```\n\n### Milestones & Deliverables\n* `Milestone`: Tracks progress (`status`, `dueAt`, `completedAt`). `responsibleMemberId` is nullable.\n* `Deliverable`: Tracks provided items (`providedAt`, `note`).\n\n```typescript\nexport interface Milestone {\n id: number;\n title: string;\n description: string | null;\n status: MilestoneStatus;\n dueAt: string | null;\n completedAt: string | null;\n responsibleMemberId: number | null;\n updatedAt: string;\n}\n\nexport interface Deliverable {\n id: number;\n name: string;\n dueAt: string | null;\n providedAt: string | null;\n note: string | null;\n responsibleMemberId: number | null;\n}\n```\n\n## Communication & Status\n\n### Asks\nQuestions sent to members.\n\n* `targetMemberId`: The recipient.\n* `status`: Tracks lifecycle (`PENDING`, `ANSWERED`, `SUPPRESSED`, etc.).\n* `suppressionReason`: Why an ask was suppressed.\n\n```typescript\nexport interface Ask {\n id: number;\n targetMemberId: number;\n question: string;\n status: AskStatus;\n answerText: string | null;\n suppressionReason: string | null;\n createdAt: string;\n respondedAt: string | null;\n expiresAt: string | null;\n}\n```\n\n### Status Notes\nRows from `CoordinationStatusNote`.\n\n* **`source`**: Hardcoded to `\"COORDINATOR\"` on the server side when created by the coordinator.\n* **`memberId`**: `null` indicates the note is about the project as a whole.\n\n```typescript\nexport interface StatusNote {\n id: number;\n projectId: number;\n memberId: number | null;\n source: NoteSource;\n content: string;\n askId: number | null;\n createdAt: string;\n}\n```\n\n## Board & Signals\n\nThe \"Holdup Board\" aggregates member status and signals.\n\n### Member Signals\nAggregated metrics for a member.\n* **Deliverables:** `deliverablesDue` (outstanding only, `providedAt == null`) is **DISJOINT** from `deliverablesProvided`.\n\n```typescript\nexport interface MemberSignals {\n milestonesTotal: number;\n milestonesDone: number;\n milestonesOverdue: number;\n deliverablesDue: number;\n deliverablesProvided: number;\n lastUpdateAt: string | null;\n unansweredAsks: number;\n suppressedAsks: number;\n}\n```\n\n### Board Member\nCombines member identity with their current signals and latest narration.\n\n```typescript\nexport interface BoardMember {\n id: number;\n startupId: number;\n startupName: string | null;\n role: MemberRole;\n status: MemberStatus;\n narratedLatest: NarratedLatest | null;\n signals: MemberSignals;\n}\n```\n\n## Audit & Errors\n\n### Audit Events\nImmutable log of actions.\n\n* `actorKind`: `HUMAN`, `AGENT`, or `SYSTEM`.\n* `hash`: Used for chain verification.\n\n```typescript\nexport interface AuditEvent {\n seq: number;\n actorKind: ActorKind;\n action: string;\n payload: Record<string, unknown>;\n createdAt: string;\n hash: string;\n}\n```\n\n### Errors\n* `NotEnabledError`: Returned when a platform flag is off.\n\n```typescript\nexport interface NotEnabledError {\n error: \"not enabled\";\n}\n```\n\n## Integration & Usage Guidelines\n\n### 1. Boundary Parsing\nWhen receiving data from the URL (e.g., `/api/vendors/:id`), the parameter `id` arrives as a string.\n* **Action:** Parse immediately with `Number(id)`.\n* **Reason:** All internal IDs are `number` (Prisma `Int`). Passing strings to these types will cause type errors.\n\n### 2. Server-Client Sync\nThis file is the contract. If the server adds a field to `ProjectDetail`, this file must be updated.\n* **Warning:** Do not assume optional fields are always present. Check for `null` or `undefined` (e.g., `startupName`, `invitedEmail`).\n\n### 3. Authentication Logic\nWhen handling `LoginResponse`:\n* Do not assume `ok: true` means the user is signed in.\n* Check for `token` and `user` existence.\n* If `requires2FA` is true, redirect to the 2FA flow using `challengeToken`.\n\n## Data Flow: Authentication\n\nThe following diagram illustrates the conditional flow of the authentication process defined in `LoginResponse` and `TwoFactorLoginResponse`.\n\n```mermaid\nsequenceDiagram\n participant Client\n participant API\n participant DB\n\n Client->>API: POST /api/startup-auth/login\n API->>DB: Check User & 2FA Config\n alt 2FA Enabled\n API-->>Client: { ok: true, requires2FA: true, challengeToken: \"...\" }\n Client->>Client: Prompt for Code\n Client->>API: POST /api/startup-auth/login/2fa\n API->>DB: Verify Code\n alt Code Valid\n API-->>Client: { ok: true, token: \"...\", user: {...} }\n else Code Invalid\n API-->>Client: { ok: false, error: \"...\" }\n end\n else 2FA Disabled\n API-->>Client: { ok: true, token: \"...\", user: {...} }\n end\n```","harakat-office":"# Harakat_Office\n\n\n\n# Harakat_Office Module Group\n\n## Overview\n\nThe **Harakat_Office** module group implements a multi-tenant project management cockpit designed to coordinate work across teams. Built on Next.js 15 with the App Router, it provides a secure environment featuring 2FA authentication, multi-language support (English/Arabic), and comprehensive project tracking including milestones, deliverables, and audit trails.\n\n## Architecture\n\nThe module group follows a layered architecture where configuration and data contracts define the boundaries for the application logic and UI.\n\n* **Configuration & Contracts:** The `[Harakat_Office](Harakat_Office.md)` module establishes the runtime environment and security policies. Shared TypeScript contracts are defined in `[types](types.md)`, ensuring the client and server remain in sync.\n* **Localization:** Internationalization is managed centrally by the `[i18n](i18n.md)` module, which configures `next-intl` and enforces URL prefixes. Static translation strings are stored in `[messages](messages.md)` and validated by `[scripts](scripts.md)`.\n* **Application Logic:** The `[app](app.md)` module orchestrates routing and layout (`LocaleLayout`, `AppLayout`). It delegates data fetching to the `[lib](lib.md)` module, which handles API clients (`vendorsApi`), authentication state (`auth-context.tsx`), and custom hooks (`useProjectData`).\n* **User Interface:** The `[components](components.md)` module renders the UI. It consumes data from the library layer and handles user interactions through dialogs, tabs, and forms.\n\n## Key Workflows\n\n### Authentication\nUser sessions are managed through the `LoginPage` in the `[app](app.md)` module. Upon login, the `useAuth` hook in `[lib](lib.md)` manages the session and triggers the `completeTwoFactor` flow. The `LocaleLayout` wraps the application with necessary providers (`Providers`) to ensure global state and localization context are available.\n\n### Project Management\nThe `ProjectsPage` lists available workspaces using `vendorsApi.listProjects`. When a user navigates to a specific project, `ProjectDetailPage` loads data via `useProjectData`. This page aggregates multiple sub-modules to display project-specific information:\n* **Audit:** `AuditTab` renders audit logs.\n* **Deliverables:** `DeliverablesTab` manages task status and triggers `AddDeliverableDialog`.\n* **Asks:** `AsksTab` allows team members to request resources.\n* **Milestones:** `MilestonesTab` tracks project timelines.\n\n### Localization\nAll user-facing text is sourced from `[messages](messages.md)`. The `[i18n](i18n.md)` module ensures that navigation and routing preserve locale prefixes (e.g., `/en/projects`). The `check-i18n.mjs` script in `[scripts](scripts.md)` enforces key parity between English and Arabic catalogs to prevent translation drift.\n\n## Module Interaction Flow\n\n```mermaid\ngraph TD\n Config[Harakat_Office Config] -->|Defines Env & i18n| App[App Module]\n App -->|Uses| Lib[Lib Module]\n App -->|Renders| Comp[Components Module]\n Lib -->|Fetches Data| API[External API]\n Lib -->|Manages State| Auth[Auth Context]\n Comp -->|Consumes| Lib\n Comp -->|Uses| Msg[Messages]\n i18n[i18n Module] -->|Configures| App\n i18n -->|Provides| Msg\n Scripts[Scripts] -->|Validates| Msg\n```\n\n## Sub-Modules\n\n* **[app](app.md)**: Core application router, layouts, and page definitions.\n* **[components](components.md)**: Reusable UI elements, dialogs, and tab interfaces.\n* **[i18n](i18n.md)**: Internationalization configuration and routing logic.\n* **[lib](lib.md)**: API clients, authentication hooks, and data utilities.\n* **[messages](messages.md)**: Static translation files for supported locales.\n* **[Harakat_Office](Harakat_Office.md)**: Next.js configuration, middleware, and build settings.\n* **[scripts](scripts.md)**: Validation tools for localization integrity.\n* **[types](types.md)**: Shared TypeScript type definitions for API contracts.","harakatmobile-assets":"# HarakatMobile — assets\n\n\n\n# HarakatMobile — assets\n\n## Overview\nThe `assets` module manages static resource configurations for the application. This documentation covers `icon.json` (`HarakatMobile/assets/expo.icon/icon.json`), which defines the visual rendering properties for the Expo Icon component.\n\nUnlike dynamic modules, this asset configuration does not contain executable logic. It serves as a data source for the rendering engine to determine icon appearance, layering, and platform compatibility.\n\n## Configuration Schema\nThe `icon.json` file utilizes a JSON structure to define icon properties. The following keys control the asset's behavior:\n\n### `fill`\nDefines the color or gradient applied to the icon.\n* **`automatic-gradient`**: Specifies an extended-srgb color value (`0.00000,0.47843,1.00000,1.00000`).\n\n### `groups`\nDefines the composition of the icon using layered images.\n* **`layers`**: An array of image definitions.\n * **`image-name`**: Reference to the underlying asset file (e.g., `expo-symbol 2.svg`, `grid.png`).\n * **`name`**: Logical identifier for the layer.\n * **`position`**: Transforms applied to the layer.\n * **`scale`**: Uniform scaling factor.\n * **`translation-in-points`**: X and Y offset coordinates.\n* **`shadow`**: Visual effects applied to the group.\n * **`kind`**: Shadow type (e.g., `neutral`).\n * **`opacity`**: Shadow transparency (0.5).\n* **`translucency`**: Blending properties.\n * **`enabled`**: Boolean flag.\n * **`value`**: Opacity level (0.5).\n\n### `supported-platforms`\nRestricts or enables the icon based on the target device.\n* **`circles`**: List of platforms supporting circular icon shapes (e.g., `watchOS`).\n* **`squares`**: Platforms supporting square icon shapes (e.g., `shared`).\n\n## Runtime Behavior\nThis module operates as a passive configuration file.\n* **Execution Flows:** No execution flows are detected. The file is loaded by the asset manager at runtime but does not trigger internal logic.\n* **Dependencies:** No internal calls, outgoing calls, or incoming calls are associated with this file. It relies entirely on the Expo Icon rendering engine to interpret the JSON structure.\n\n## Architecture\nThe following diagram illustrates the data structure hierarchy defined within the configuration file.\n\n```mermaid\ngraph TD\n A[icon.json] --> B_fill[\"B(fill)\"]\n A --> C_groups[\"C(groups)\"]\n A --> D_supported-platforms[\"D(supported-platforms)\"]\n C --> E_layers[\"E(layers)\"]\n E --> F_image-name[\"F(image-name)\"]\n E --> G_position[\"G(position)\"]\n C --> H_shadow[\"H(shadow)\"]\n C --> I_translucency[\"I(translucency)\"]\n```\n\n## Integration\nDevelopers should ensure that the referenced image assets (`expo-symbol 2.svg`, `grid.png`) exist in the corresponding asset directories. Changes to this JSON file will affect the visual output of the icon component without requiring code recompilation, provided the asset manager supports dynamic asset loading.\n\n## Modification Guidelines\nWhen modifying this configuration:\n1. Ensure `image-name` values match existing asset filenames.\n2. Verify `supported-platforms` aligns with the app's target deployment targets.\n3. Adjust `translation-in-points` carefully to maintain alignment with the design system.","harakatmobile-harakatmobile":"# HarakatMobile — HarakatMobile\n\n\n\n# HarakatMobile Module Documentation\n\n## Overview\n\nThe **HarakatMobile** module is the primary client application for the Harakat workspace, built as an Expo React Native application. It serves as the entry point for user interaction, handling authentication, biometric security, offline capabilities, and core workspace features (Dashboard, Agent Board, Notifications, Settings).\n\nThis module is configured for store submission (iOS App Store and Google Play) but requires owner activation for credentials and final verification. It relies on external services for API communication, push notifications, and secure storage.\n\n## Technology Stack\n\nThe application utilizes the following core technologies and dependencies defined in `package.json`:\n\n* **Framework:** React Native (0.86.0) via Expo SDK 57.\n* **Language:** TypeScript (Strict mode enabled).\n* **Navigation:** `expo-router` (Typed routes enabled).\n* **State & Storage:** `@react-native-async-storage/async-storage`, `expo-secure-store`.\n* **Authentication:** `expo-local-authentication` (Biometrics), `react-native-passkeys` (Passkeys).\n* **Networking:** `@react-native-community/netinfo` (Offline detection).\n* **Media:** `expo-audio`, `expo-image-picker`, `expo-document-picker`.\n* **Notifications:** `expo-notifications`.\n\n## Configuration\n\nConfiguration is centralized in `app.json` and `eas.json`.\n\n### App Configuration (`app.json`)\n\nThe `app.json` file defines the bundle identifiers, permissions, and native plugin configurations.\n\n* **Bundle Identifier / Package:** `tech.harakat.app` (iOS and Android).\n* **URL Scheme:** `harakat` (Used for deep linking).\n* **API Endpoint:** Configured via `extra.apiUrl` as `https://api.useharakat.com`.\n* **Web Credentials:** `app.useharakat.com` (Associated domains for iOS and Android app links).\n* **Permissions:**\n * **Biometrics:** `NSFaceIDUsageDescription`, `android.permission.USE_BIOMETRIC`.\n * **Media:** Camera, Microphone, Photo Library (Required for document scanning and voice notes).\n * **Push:** `aps-environment=production` entitlement.\n\n### Build Configuration (`eas.json`)\n\nBuild profiles are managed via EAS CLI.\n\n* **Development:** `developmentClient: true`, `distribution: internal`.\n* **Internal:** `distribution: internal`, `channel: internal`.\n* **Production:** `autoIncrement: true`, `channel: production`.\n\n**Submit Profiles:**\n* **iOS:** Requires `appleId`, `ascAppId`, and `appleTeamId` placeholders.\n* **Android:** Requires `serviceAccountKeyPath` and targets the `internal` track.\n\n## Architecture & Execution Flow\n\nWhile internal function call graphs are not exposed at this module level, the application follows a standard Expo Router architecture. The entry point is `expo-router/entry`, which initializes the navigation stack.\n\n### Feature Structure\n\nBased on the verification checklist, the application is organized into the following primary tabs and flows:\n\n1. **Authentication:** Password login, Passkey registration/login, Biometric app-lock.\n2. **Dashboard:** Widget rendering, offline handling.\n3. **Agent Board:** Lane/card management, chat sheet, offline outbox.\n4. **Notifications:** Feed items, preferences, deep-link handling.\n5. **Settings:** Profile, appearance, security, session, governance.\n\n### High-Level Flow\n\n```mermaid\ngraph TD\n A[App Launch] --> B{Auth Check}\n B -->|Offline| C[Show Cached Data]\n B -->|Online| D[Refresh Token Rotation]\n D --> E[Load Dashboard/Agent Board]\n E --> F{User Action}\n F -->|Biometric| G[Local Auth]\n F -->|Deep Link| H[Open Specific Tab]\n F -->|Push| I[Notification Handler]\n G --> E\n H --> E\n I --> E\n E --> J[Offline Mode Trigger]\n J --> K[Queue Actions]\n K --> L[Reconnect]\n L --> M[Flush Outbox]\n```\n\n## Native Integration\n\n### iOS Specifics\n* **Face ID:** Enabled via `NSFaceIDUsageDescription`.\n* **Associated Domains:** `applinks:app.useharakat.com` and `webcredentials:app.useharakat.com`.\n* **Entitlements:** `aps-environment` set to `production`.\n\n### Android Specifics\n* **Biometrics:** `USE_BIOMETRIC` and `USE_FINGERPRINT` permissions.\n* **Intent Filters:** Configured for `app.useharakat.com` with `BROWSABLE` category.\n* **Adaptive Icon:** Defined with foreground/background layers.\n\n## Security & Credentials\n\nSecurity is managed through a combination of native modules and environment variables.\n\n### Secure Storage\n* **Sensitive Data:** Handled by `expo-secure-store`.\n* **Refresh Tokens:** Rotated upon app restart (verified via `MOBILE_REFRESH_ENABLED=on`).\n\n### Passkeys & WebAuthn\n* **RP ID:** `app.harakat.tech`.\n* **Origins:** `WEBAUTHN_ORIGIN` must include `app.harakat.tech` and `apk-key-hash` for Android.\n* **Well-Known Files:** Credentials and fingerprints must be updated in `store/credentials-and-wellknown.md`.\n\n### Offline Mode\n* **Detection:** `@react-native-community/netinfo`.\n* **Behavior:** Caches data, queues supported actions, and flushes the outbox upon reconnection.\n\n## Development Workflow\n\n### Prerequisites\n* Node.js/npm matching `package-lock.json`.\n* EAS CLI (`npx eas`).\n* Physical iOS and Android devices (required for biometric and deep-link verification).\n\n### Local Development\n1. **Install Dependencies:**\n ```bash\n npm install\n ```\n2. **Start Metro:**\n ```bash\n npm start\n ```\n3. **Build Development Client:**\n ```bash\n npx eas build --profile development --platform ios\n npx eas build --profile development --platform android\n ```\n *Note: Expo Go is insufficient for final verification of native modules.*\n\n### Verification\nBefore submission, the following manual checks must pass on physical devices:\n* **Auth:** Password login survives restart; Passkey login works for `app.harakat.tech`.\n* **Biometrics:** App-lock locks after 30s backgrounding; unlocks via Face ID/Touch ID.\n* **Offline:** Dashboard loads cached data; Agent Board preserves outbox state.\n* **Deep Links:** Push notifications open expected tabs.\n* **Layout:** iPad split-width and Android tablet layouts render correctly.\n\n### Build & Submit\n1. **Internal Build:**\n ```bash\n npx eas build --profile internal --platform ios\n npx eas build --profile internal --platform android\n ```\n2. **Submit:**\n ```bash\n npx eas submit --profile internal --platform ios\n npx eas submit --profile internal --platform android\n ```\n\n## Store Submission\n\nThe `store/` folder contains the source of truth for submission answers.\n\n* **Privacy:** `app-store-privacy.md`, `play-data-safety.md`, `ios-privacy-manifest.md`.\n* **Assets:** `assets-spec.md` (sizes, colors, slots).\n* **Credentials:** `credentials-and-wellknown.md` (Apple Team ID, SHA-256 fingerprint).\n\n**Owner Activation Steps:**\n1. Replace placeholder art in `assets/images/*` and `assets/expo.icon/`.\n2. Run `npx eas credentials` for Apple distribution.\n3. Update `eas.json` submit placeholders (`<APPLE_ID>`, `<ASC_APP_ID>`, etc.).\n4. Redeploy web hosts after updating well-known files.\n5. Set production env flags (`MOBILE_REFRESH_ENABLED`, `PUSH_ENABLED`, `FCM_PROJECT_ID`).\n6. Complete App Store privacy and Google Play Data Safety forms.\n\n## Scripts\n\nCommon development and validation scripts defined in `package.json`:\n\n* `npm start`: Starts Metro bundler.\n* `npm test`: Runs i18n checks, harness self-tests, Jest, and icon/OTA safety checks.\n* `npm run lint`: Runs Expo lint.\n* `npm run check:i18n`: Validates internationalization.\n* `npm run check:icons`: Validates asset integrity.\n* `npm run check:ota-safe`: Validates Over-The-Air update safety.\n\n## Troubleshooting\n\n* **Build Failures:** Verify `eas.json` placeholders are replaced before running `eas build --profile production`.\n* **Deep Link Failures:** Ensure `app.useharakat.com` is added to `WEBAUTHN_ORIGIN` and associated domains in `app.json`.\n* **Push Notifications:** Verify `FCM_PROJECT_ID` and `FCM_SERVICE_ACCOUNT_PATH` are set in production environment variables.\n* **Biometric Errors:** Ensure `NSFaceIDUsageDescription` is present in `Info.plist` (derived from `app.json`) and Android permissions are granted.","harakatmobile-ios":"# HarakatMobile — ios\n\n\n\n# HarakatMobile — ios\n\n## Overview\n\nThe `HarakatMobile — ios` module is the native iOS implementation of the Harakat application. It serves as the container for the React Native application, utilizing the Expo SDK to manage the app lifecycle, native permissions, and deep linking. The project is structured as a standard Xcode project (`Harakat.xcodeproj`) with CocoaPods integration for dependency management.\n\n## Architecture\n\nThe module follows the standard Expo Dev Client architecture. The Swift `AppDelegate` acts as the bridge between the iOS system and the React Native JavaScript runtime.\n\n### Initialization Flow\n\nWhen the application launches, the system invokes `application(_:didFinishLaunchingWithOptions:)` in `AppDelegate`. This method initializes the `ExpoReactNativeFactory` and starts the React Native bridge.\n\n```mermaid\nsequenceDiagram\n participant System\n participant AppDelegate\n participant ReactNativeDelegate\n participant RNFactory\n participant JSRuntime\n\n System->>AppDelegate: application(_:didFinishLaunchingWithOptions:)\n AppDelegate->>ReactNativeDelegate: bundleURL()\n ReactNativeDelegate->>RNFactory: startReactNative()\n RNFactory->>JSRuntime: Initialize JS Bundle\n JSRuntime-->>RNFactory: Ready\n RNFactory-->>AppDelegate: didFinishLaunching\n AppDelegate-->>System: true\n```\n\n## Key Components\n\n### AppDelegate.swift\n\nThe `AppDelegate` class is the primary entry point for the iOS application. It extends `ExpoAppDelegate` and manages the following responsibilities:\n\n* **App Lifecycle:** Initializes the React Native environment via `ExpoReactNativeFactory`.\n* **Deep Linking:** Handles URL schemes and Universal Links via `application(_:open:options:)` and `application(_:continue:userActivity:)`.\n* **Window Management:** Creates the `UIWindow` and injects the React Native factory.\n\n#### Bundle Resolution\nThe `ReactNativeDelegate` subclass handles the resolution of the JavaScript bundle URL. It distinguishes between development and production environments:\n\n* **Debug:** Returns the virtual Metro entry URL (`.expo/.virtual-metro-entry`).\n* **Release:** Returns the pre-bundled `main.jsbundle` from the app bundle.\n\n### Configuration Files\n\n#### Info.plist\nLocated at `Harakat/Info.plist`, this file defines the app's metadata and permissions.\n\n* **Bundle Identifier:** `tech.harakat.app`\n* **Deployment Target:** iOS 16.4\n* **Permissions:**\n * `NSCameraUsageDescription`: Required for document photography.\n * `NSMicrophoneUsageDescription`: Required for voice notes.\n * `NSPhotoLibraryUsageDescription`: Required for image attachments.\n * `NSFaceIDUsageDescription`: Required for biometric authentication.\n* **Universal Links:** Configured for `app.useharakat.com`.\n* **URL Schemes:** `harakat` and `tech.harakat.app`.\n\n#### Expo.plist\nLocated at `Harakat/Supporting/Expo.plist`, this file configures OTA updates.\n\n* **Enabled:** `true`\n* **Check On Launch:** `ALWAYS`\n* **Update URL:** `https://u.expo.dev/730a0f9d-bd80-4cc6-b732-3353bd6d2e5a`\n\n### Build System\n\n#### Podfile\nThe `Podfile` manages CocoaPods dependencies and React Native integration.\n\n* **Engine:** Hermes is enabled (`expo.jsEngine: \"hermes\"`).\n* **Deployment Target:** iOS 16.4.\n* **Frameworks:** Uses `use_frameworks!` with dynamic linkage.\n* **Autolinking:** Configured via `expo-modules-autolinking` to ensure native modules are linked correctly.\n\n#### Xcode Project Settings\nDefined in `project.pbxproj`:\n\n* **Swift Version:** 5.0\n* **Optimization:** `-Onone` (Debug) / Standard (Release).\n* **Bitcode:** Disabled.\n* **Code Signing:** `iPhone Developer` identity.\n* **Asset Catalog:** `expo` is used as the app icon name.\n\n## Resources\n\n### Assets\n* **Images.xcassets:** Contains the app icon (`expo.icon`) and splash screen assets.\n* **SplashScreen.storyboard:** Defines the launch screen layout.\n * **Background:** Dark gray (`#032F2F` approx).\n * **Logo:** Centered `SplashScreenLogo` image.\n\n### Supporting Files\n* **Harakat-Bridging-Header.h:** Empty bridging header for Swift/Objective-C interoperability.\n* **Harakat.entitlements:** Defines entitlements for Associated Domains (`applinks:app.useharakat.com`).\n\n## Development Workflow\n\n1. **Dependencies:** Run `pod install` in the `ios` directory to sync CocoaPods dependencies.\n2. **Environment:** The project relies on `.xcode.env` and `.xcode.env.local` for bundling configuration, sourced by the \"Bundle React Native code and images\" build phase.\n3. **Debugging:**\n * **Debug Mode:** Uses the virtual Metro entry for hot reloading.\n * **Release Mode:** Bundles the JavaScript into `main.jsbundle`.\n4. **Updates:** OTA updates are managed via the `Expo.plist` configuration. The app checks for updates on launch.\n\n## Security & Permissions\n\nThe app requests specific permissions defined in `Info.plist`:\n\n| Permission | Key | Description |\n| :--- | :--- | :--- |\n| Camera | `NSCameraUsageDescription` | Photograph documents for assistant. |\n| Microphone | `NSMicrophoneUsageDescription` | Send voice notes. |\n| Photo Library | `NSPhotoLibraryUsageDescription` | Attach images to messages. |\n| Face ID | `NSFaceIDUsageDescription` | Unlock app and protect workspace. |\n\nNetwork security is configured via `NSAppTransportSecurity`, allowing local networking but requiring HTTPS for external connections (`NSAllowsArbitraryLoads` is `false`).","harakatmobile-scripts":"# HarakatMobile — scripts\n\n\n\n# HarakatMobile — scripts Module\n\n## Overview\n\nThe **scripts** module contains quality gates, testing harnesses, and build utilities that ensure the Harakat mobile web application meets its core requirements before code is shipped. These scripts run in Node.js and Playwright, validating i18n parity, RTL behavior, animation accessibility, OTA safety, and render correctness.\n\nThe module is organized into two categories:\n\n| Category | Purpose |\n|----------|---------|\n| **Static Analysis** | `check-i18n.js`, `check-icons.mjs`, `check-ota-safe.mjs` — grep-based validation without running the app |\n| **Render Gates** | `rtl-render-check.mjs`, `stream-render-check.mjs` — boot the app and verify visual behavior |\n| **Infrastructure** | `harness-selftest.mjs`, `settled.mjs`, `sign-in-diagnosis.mjs`, `web-export.mjs` — shared utilities and self-tests |\n| **Utilities** | `reset-project.js` — project structure management |\n\n---\n\n## Architecture\n\n```mermaid\nflowchart TD\n subgraph Static Analysis\n A[check-i18n.js] -->|i18n parity| B[check-icons.mjs]\n A -->|device-locale| C[check-ota-safe.mjs]\n end\n \n subgraph Render Gates\n D[rtl-render-check.mjs] -->|builds export| E[web-export.mjs]\n D -->|uses| F[settled.mjs]\n D -->|uses| G[sign-in-diagnosis.mjs]\n H[stream-render-check.mjs] -->|builds export| E\n end\n \n subgraph Infrastructure\n I[harness-selftest.mjs] -->|tests| E\n I -->|tests| G\n I -->|tests| F\n end\n \n E -->|provides| D\n E -->|provides| H\n G -->|used by| D\n F -->|used by| D\n```\n\n---\n\n## Static Analysis Scripts\n\n### check-i18n.js\n\n**Purpose:** Enforces five mechanical rules that prevent silent i18n and RTL regressions.\n\n**Rules Enforced:**\n\n1. **CATALOGUE PARITY** — `en.json` and `ar.json` must define the same key set. Every plural entry must have exactly the CLDR categories its locale can produce (English: `[\"one\", \"other\"]`, Arabic: `[\"zero\", \"one\", \"two\", \"few\", \"many\", \"other\"]`).\n\n2. **NO DEVICE-LOCALE FORMATTERS** — `toLocaleString`, `toLocaleDateString`, `toLocaleTimeString`, and `Intl.*` with `undefined` are forbidden except in `src/i18n/format.ts`.\n\n3. **NO PHYSICAL DIRECTIONAL STYLES** — `marginLeft`, `paddingRight`, `borderLeft`, `textAlign: \"left\"` are forbidden. Use logical forms (`marginStart`, `textAlign: \"auto\"`). Exceptions require `rtl-ok:` comment.\n\n4. **EVERY ANIMATION READS REDUCED MOTION** — Files using `withSpring`, `withTiming`, `Animated.timing`, or `entering/exiting` props must import from `@/lib/motion/motion`.\n\n5. **NO HAND-ROLLED TAB-BAR CLEARANCE** — `insets.bottom + <n>` is forbidden except in `lib/chrome/tab-bar.tsx`. Use `useTabBarClearance().clearance()`. Exceptions require `clearance-ok:` comment.\n\n**Execution Flow:**\n\n```\ncheckCatalogues()\n └─> flatten() → parse JSON → compare key sets → validate plural categories\ncheckSources()\n └─> walk() → read files → regex patterns → report violations\n```\n\n**Usage:**\n```bash\nnode scripts/check-i18n.js\nnpm run check:i18n\n```\n\n---\n\n### check-icons.mjs\n\n**Purpose:** Ensures `expo-symbols` (iOS-only) is imported in exactly one module and all icon names are mapped.\n\n**Rules Enforced:**\n\n1. `expo-symbols` must not be imported anywhere (it crashes on Android at module load).\n2. Every icon name passed to `<Icon>` must exist in `src/components/ui/icon.tsx`'s `ICONS` map.\n\n**Execution Flow:**\n\n```\nwalk(SRC)\n └─> scan for expo-symbols imports → fail if found\nread ICONS map\n └─> extract mapped names\nfor each file\n └─> match <Icon name=\"...\" /> → validate against map\n```\n\n**Usage:**\n```bash\nnode scripts/check-icons.mjs\n```\n\n---\n\n### check-ota-safe.mjs\n\n**Purpose:** Validates that every native module import is declared in `package.json`.\n\n**Why This Exists:** OTA updates ship JavaScript only. Native modules are compiled into the binary. An import that cannot resolve at bundle load crashes the app at launch.\n\n**Rule:** Every package imported from `src/` that ships native code (has `ios/`, `android/`, or `.podspec`) must be in `package.json`.\n\n**Execution Flow:**\n\n```\nwalk(SRC)\n └─> extract import specs\nfor each spec\n └─> packageOf() → isNative() → declared.has() → fail if missing\n```\n\n**Usage:**\n```bash\nnode scripts/check-ota-safe.mjs\n```\n\n---\n\n## Render Gate Scripts\n\n### rtl-render-check.mjs\n\n**Purpose:** Boots the app in a headless browser and verifies visual behavior across languages, RTL, and error states.\n\n**What It Proves:**\n\n1. The static web export boots and gets past the splash screen.\n2. No catalogue key leaks to screen (missing translations render the key path).\n3. Arabic really renders Arabic on authenticated screens.\n4. The app never claims a restart is owed when it is not.\n5. Read failures render as failures (not as \"Locked\" or empty lists).\n6. Row layout mirrors under `dir=\"rtl\"` (logical styles).\n7. Tab-bar clearance on multiple surfaces.\n8. Action sheet bands and destructive action placement.\n9. Loading debounce timing (nothing for 2s, then skeleton).\n10. Placeholder cross-fades out rather than being swapped.\n\n**Network Isolation:**\n\nAll API calls are intercepted. The app is configured against `https://api.harakat.tech` (production), so requests are stubbed in a table. Anything not stubbed returns 503.\n\n**Scenarios:**\n\n| Scenario | Purpose |\n|----------|---------|\n| `ok` | Everything works |\n| `readsFail` | All reads return 503 |\n| `denied` | Widget data returns `{ denied: true }` |\n| `unknownWidget` | Unknown render type |\n| `slowLayout` | Layout read delayed 4.5s |\n\n**Execution Flow:**\n\n```\nbuildWebExport()\n └─> sweepStaleExports() → build → assertExportTargets()\nsignedInPage()\n └─> route interception → login → wait for tab bar\n └─> openSettings() → openBoardInbox() → text()\n └─> settled() → expectText() → screenshot()\n```\n\n**Usage:**\n```bash\nnode scripts/rtl-render-check.mjs\nnpm run check:rtl\nnode scripts/rtl-render-check.mjs --export <dir>\n```\n\n---\n\n### stream-render-check.mjs\n\n**Purpose:** Similar to `rtl-render-check.mjs` but for stream-based rendering validation. Uses the same export infrastructure and sign-in diagnosis.\n\n**Key Difference:** Builds its export with `EXPO_PUBLIC_API_URL` pointed at a local server, while `rtl-render-check.mjs` uses the app's configured production base.\n\n---\n\n## Shared Utilities\n\n### lib/settled.mjs\n\n**Purpose:** Waits for layout measurements to stabilize before comparing them.\n\n**Why This Exists:** The old wait watched a proxy (sheet transform) for a fact about something else (notice position). Under load, it failed inconsistently. This waits on the boxes being compared, together, in one rAF loop.\n\n**Algorithm:**\n\n1. Read all target boxes in one frame.\n2. Compare against previous frame.\n3. If unchanged within `SETTLE_EPSILON` (0.5px) for `STABLE_FRAMES` (6) consecutive frames, return success.\n4. If timeout (`SETTLE_TIMEOUT_MS` = 20000) reached, fail with last two readings.\n\n**Target Kinds:**\n\n| Kind | Selector |\n|------|----------|\n| `tabbar` | Walk up from `a[href=\"/board\"]` |\n| `testid` | `[data-testid=\"…\"]` |\n| `selector` | CSS selector |\n| `placeholder` | `[placeholder=\"…\"]` with height > 0 |\n| `exactText` | Leaf element with exact text |\n\n**Usage:**\n```javascript\nimport { settledBoxes, settleFailureReason } from \"./lib/settled.mjs\";\n\nconst result = await settledBoxes(page, {\n bar: { kind: \"tabbar\" },\n composer: { kind: \"placeholder\", value: \"What do you want?\" }\n});\n```\n\n---\n\n### lib/sign-in-diagnosis.mjs\n\n**Purpose:** Classifies failed sign-in attempts into named causes rather than generic timeouts.\n\n**Causes:**\n\n| Cause | Condition |\n|-------|-----------|\n| `api_unreachable` | No login POST, escaped hosts present |\n| `login_refused` | Login POST landed, still on login screen |\n| `page_error` | Left login screen, then threw |\n| `shell_absent` | Everything looked fine, shell never appeared |\n\n**Web-Only Noise Filter:**\n\nFilters out React #418, `expo-notifications`, `SymbolView` — these are structural to the setup, not defects.\n\n**Usage:**\n```javascript\nimport { diagnoseSignIn, realPageErrors } from \"./lib/sign-in-diagnosis.mjs\";\n\nconst d = diagnoseSignIn({\n expectedBase: \"http://127.0.0.1:4321\",\n url: \"http://127.0.0.1:4321/login\",\n bodyText: \"Harakat\\nSign in...\",\n apiPaths: [\"/api/startup-auth/login\"],\n escapedHosts: [\"api.harakat.tech\"],\n pageErrors: [\"Error: #418\"],\n waitedMs: 20000\n});\n// d.cause === \"api_unreachable\"\n```\n\n---\n\n### lib/web-export.mjs\n\n**Purpose:** Builds static web exports and validates they target what they were built for.\n\n**The Defect It Closes:** Metro's transform cache does not key on `EXPO_PUBLIC_*` values. Two consecutive exports with different API bases could share a cache, causing one to inherit the other's inlined constant.\n\n**Solution:**\n\n1. Each export gets its own cache directory, keyed on the API base.\n2. After build, the bundle is read and must carry the expected base.\n\n**Functions:**\n\n| Function | Purpose |\n|----------|---------|\n| `exportCacheRoot(apiBaseUrl)` | Returns cache directory path |\n| `bundleFiles(exportDir)` | Returns client bundle file paths |\n| `checkExportTargets(exportDir, apiBaseUrl)` | Validates bundle targets |\n| `describeExportMismatch(result, { apiBaseUrl, exportDir, cacheRoot })` | Human-readable failure message |\n| `buildWebExport({ root, prefix, apiBaseUrl })` | Builds and validates export |\n| `sweepStaleExports(prefix, keep)` | Removes exports from earlier runs |\n\n**Usage:**\n```javascript\nimport { buildWebExport, assertExportTargets } from \"./lib/web-export.mjs\";\n\nconst built = buildWebExport({\n root: ROOT,\n prefix: \"harakat-web-export-\",\n apiBaseUrl: null // app's configured base\n});\n```\n\n---\n\n### harness-selftest.mjs\n\n**Purpose:** Proves the harness infrastructure works by testing it against itself.\n\n**What It Tests:**\n\n1. Export validation (base inlined, loopback leaked, no bundle).\n2. Metro cache isolation (different bases = different caches).\n3. Stale export collection (prefix-based, conservative).\n4. Sign-in diagnosis (all causes, web-only noise filtering).\n\n**Fixtures:**\n\nCreates fake export directories in `/tmp` with controlled content to test each branch.\n\n**Usage:**\n```bash\nnpm run check:harness\n```\n\n---\n\n## Utilities\n\n### reset-project.js\n\n**Purpose:** Resets the project to a blank state.\n\n**What It Does:**\n\n1. Prompts whether to move `src` and `scripts` to `example/` or delete them.\n2. Creates `src/app/` with `index.tsx` and `_layout.tsx`.\n3. Provides next steps for development.\n\n**Usage:**\n```bash\nnode scripts/reset-project.js\n```\n\n---\n\n## Integration with Codebase\n\n### Package.json Scripts\n\n```json\n{\n \"scripts\": {\n \"check:i18n\": \"node scripts/check-i18n.js\",\n \"check:icons\": \"node scripts/check-icons.mjs\",\n \"check:ota-safe\": \"node scripts/check-ota-safe.mjs\",\n \"check:rtl\": \"node scripts/rtl-render-check.mjs\",\n \"check:stream\": \"node scripts/stream-render-check.mjs\",\n \"check:settle\": \"node scripts/settle-selftest.mjs\",\n \"check:harness\": \"node scripts/harness-selftest.mjs\",\n \"reset\": \"node scripts/reset-project.js\"\n }\n}\n```\n\n### Dependency on Other Modules\n\n| Script | Depends On |\n|--------|------------|\n| `check-i18n.js` | `src/i18n/messages/*.json` |\n| `check-icons.mjs` | `src/components/ui/icon.tsx` |\n| `check-ota-safe.mjs` | `node_modules/`, `package.json` |\n| `rtl-render-check.mjs` | `src/` (via expo export), Playwright |\n| `stream-render-check.mjs` | `src/` (via expo export), Playwright |\n| `harness-selftest.mjs` | `lib/web-export.mjs`, `lib/sign-in-diagnosis.mjs` |\n| `settled.mjs` | Playwright |\n| `web-export.mjs` | `node_modules/`, `os.tmpdir()` |\n\n---\n\n## Failure Reporting\n\nAll scripts exit with code 1 on failure and report:\n\n```\ncheck:rtl: 3 failure(s)\n\n check:rtl: en dashboard (healthy): expected to see \"Open tasks\" and did not\n check:rtl: ar dashboard (healthy): no Arabic script on screen — the ar catalogue did not take effect\n check:rtl: clearance: the ask composer runs to y=892 and the tab bar starts at y=893 — it is UNDER the bar\n\n the export is kept for inspection: --export /tmp/harakat-web-export-abc123\n```\n\nGreen runs exit 0 and clean up their exports. Red runs keep their exports for inspection with `--export`.\n\n---\n\n## Best Practices\n\n1. **Static checks first** — Run `check:i18n`, `check:icons`, `check:ota-safe` before render gates.\n2. **Use `--export` on red runs** — Keeps the export for debugging.\n3. **Don't rely on timeouts** — All waits are explicit and documented.\n4. **Comment exceptions** — `rtl-ok:`, `clearance-ok:`, `clearance-ok:` are required for physical styles.\n5. **Test both locales** — `rtl-render-check.mjs` runs in `en` and `ar`.\n\n---\n\n## Known Limitations\n\n| Limitation | Reason |\n|------------|--------|\n| Native RTL mirroring | `I18nManager.forceRTL` is read by native engine at process start |\n| SF Symbols | `expo-symbols` is iOS-only, no Android fallback |\n| Reduced motion | Native accessibility API, not web |\n| Deep-link parking | Requires notification tap, not reproducible in headless |\n| Drag-to-dismiss-keyboard | Raw touch stream, not reproducible |\n\nThese need a device or simulator.","harakatmobile-src":"# HarakatMobile — src\n\n\n\n# HarakatMobile — src Module Documentation\n\n## Overview\n\nThe `src` module implements the main user-facing application shell for HarakatMobile. It provides a tab-based navigation system with four primary destinations: **Dashboard**, **Board**, **Notifications**, and **Settings**. Each screen manages its own data lifecycle, error states, and offline resilience while sharing common UI patterns, theming, and localization.\n\nThe module is built on Expo Router with TypeScript, React Native, and follows a component-based architecture where screens are self-contained but coordinate through shared hooks, context providers, and utility libraries.\n\n---\n\n## Architecture\n\n```mermaid\ngraph TB\n RootLayout[RootLayout] --> ThemeProvider[ThemeProvider]\n ThemeProvider --> I18nProvider[I18nProvider]\n I18nProvider --> AuthProvider[AuthProvider]\n AuthProvider --> RootNavigator[RootNavigator]\n RootNavigator --> TabsLayout[TabsLayout]\n TabsLayout --> Dashboard[DashboardScreen]\n TabsLayout --> Board[BoardScreen]\n TabsLayout --> Notifications[NotificationsScreen]\n TabsLayout --> Settings[SettingsScreen]\n Settings --> Profile[ProfileScreen]\n Settings --> Appearance[AppearanceScreen]\n Settings --> Language[LanguageScreen]\n Settings --> NotificationsSettings[NotificationSettingsScreen]\n Settings --> Security[SecurityScreen]\n Settings --> Governance[GovernanceScreen]\n```\n\n### Provider Stack\n\nThe application uses a layered provider stack that wraps all screens:\n\n```\nSafeAreaProvider\n └─ ThemeProvider\n └─ I18nProvider\n └─ AuthProvider\n └─ RootNavigator\n └─ (tabs) / login\n```\n\nEach provider layer adds specific functionality:\n- **SafeAreaProvider**: Handles device safe areas (notches, home indicators)\n- **ThemeProvider**: Manages color schemes (light/dark/system)\n- **I18nProvider**: Provides translation functions and locale management\n- **AuthProvider**: Manages authentication state, biometric lock, and session\n\n---\n\n## Core Navigation\n\n### Tab Layout (`app/(tabs)/_layout.tsx`)\n\nThe tab bar is the central navigation component that wraps all tab screens. It uses a **clearance contract** pattern where the bar publishes its own height to every screen under it.\n\n```typescript\n<TabBarClearanceProvider>\n <TabsNavigator />\n<\/TabBarClearanceProvider>\n```\n\n**Key Implementation Details:**\n\n| Aspect | Implementation |\n|--------|---------------|\n| **Height Contract** | `useReportTabBarHeight()` publishes height; `useTabBarClearance()` reads it |\n| **Background** | `BlurView` with intensity 30 + `t.card` background |\n| **Safe Areas** | `useSafeAreaInsets()` for bottom padding |\n| **Theme Integration** | `tabBarActiveTintColor: t.primary`, `tabBarInactiveTintColor: t.mutedForeground` |\n\n**Tab Configuration:**\n\n```typescript\n<Tabs.Screen name=\"index\" options={{\n title: tr(\"tabs.dashboard\"),\n tabBarIcon: ({ color }) => <TabBarIcon name=\"square.grid.2x2\" color={color} />\n}} />\n<Tabs.Screen name=\"board\" options={{\n title: tr(\"tabs.board\"),\n tabBarIcon: ({ color }) => <TabBarIcon name=\"rectangle.3.group\" color={color} />\n}} />\n<Tabs.Screen name=\"notifications\" options={{\n title: tr(\"tabs.notifications\"),\n tabBarBadge: unreadNotifications,\n tabBarIcon: ({ color }) => <TabBarIcon name=\"bell\" color={color} />\n}} />\n<Tabs.Screen name=\"settings\" options={{\n title: tr(\"tabs.settings\"),\n tabBarIcon: ({ color }) => <TabBarIcon name=\"gearshape\" color={color} />\n}} />\n```\n\n---\n\n## Screen Modules\n\n### Dashboard Screen (`app/(tabs)/index.tsx`)\n\n**Purpose**: Personalized widget grid with drag-to-dismiss keyboard and inline composer.\n\n**Key State:**\n- `layout: WidgetInstance[]` — Cached widget configuration\n- `loadFailed: boolean` — Separate from empty state\n- `sheetWidget: WidgetInstance | null` — Long-pressed widget for actions\n\n**Data Flow:**\n```mermaid\nsequenceDiagram\n participant User\n participant Screen\n participant Cache\n participant API\n participant Storage\n\n User->>Screen: Load\n Screen->>Cache: get(LAYOUT_CACHE_KEY)\n Cache-->>Screen: Cached layout\n Screen->>API: getLayout()\n API-->>Screen: WidgetInstance[]\n Screen->>Storage: set(LAYOUT_CACHE_KEY)\n Screen->>Screen: Render FlatList\n User->>Screen: Long press widget\n Screen->>Screen: setSheetWidget(item)\n User->>Screen: Tap action\n Screen->>API: putLayout(next)\n API-->>Screen: Saved layout\n```\n\n**Widget Actions:**\n- **Move to Top**: `layoutWithWidgetFirst(layout, widgetId)`\n- **Remove**: `layoutWithoutWidget(layout, widgetId)`\n- **Open Web**: `handoffUrl(\"/dashboard\")`\n\n**Error Handling Strategy:**\n- `viewState === \"stale\"` → `StaleStrip` above cards\n- `viewState === \"error\"` → `LoadError` component\n- `viewState === \"empty\"` → `EmptyState` component\n\n### Board Screen (`app/(tabs)/board.tsx`)\n\n**Purpose**: Chat-first interface with sidebar for inbox and canvas.\n\n**Pane Management:**\n```typescript\nconst [pane, setPane] = useState<\"chat\" | \"inbox\" | \"canvas\">(\"chat\");\n```\n\n**Tablet vs Phone Layout:**\n- **Phone**: Chat takes full screen; sidebar accessed via back button\n- **Tablet**: 320pt rail (inbox) + canvas split view\n\n**Event Handling:**\n```typescript\nconst applyBoardEvent = useCallback(\n (event: BoardEvent) => {\n if (isInboxRowUpsertedEvent(event) || isInboxRowReadEvent(event)) {\n void refetchInbox();\n return;\n }\n if (isPipelineProgressEvent(event)) {\n setProgressByTaskId((current) => ({\n ...current,\n [event.agentTaskId]: {\n progress: event.progress,\n status: event.status,\n },\n }));\n return;\n }\n // ... more event handlers\n },\n [refetchInbox, updateItems],\n);\n```\n\n**Stream Integration:**\n```typescript\nconst stream = useBoardStream(applyBoardEvent);\n```\n\n**Row Actions (Long-Press):**\n- **Open**: Opens chat for conversation\n- **Mark Read**: Updates inbox state\n- **Open Web**: External handoff to `/agent-board`\n\n### Notifications Screen (`app/(tabs)/notifications.tsx`)\n\n**Purpose**: Chronological feed grouped by day with swipe-to-mark-read.\n\n**Data Structure:**\n```typescript\nconst {\n rows,\n unreadCount,\n loading,\n refreshing,\n error,\n refetch,\n markRead,\n markAllRead,\n streamState,\n streamFailureStatus,\n streamReplayGap,\n streamReconnecting,\n reconnectStream,\n} = useNotificationsFeed();\n```\n\n**Day Grouping:**\n```typescript\nconst sections = useMemo(() => \n groupNotificationsByDay(rows, tr, locale), \n [rows, tr, locale]\n);\n```\n\n**Swipe Actions:**\n```typescript\n<Swipeable\n overshootRight={false}\n renderRightActions={() => (\n <Pressable onPress={() => markRead(item.id).catch(() => {})}>\n <Icon name=\"checkmark\" size={22} color={t.primaryForeground} />\n <Text>{tr(\"common.read\")}<\/Text>\n <\/Pressable>\n )}\n onSwipeableOpen={() => markRead(item.id).catch(() => {})}\n>\n <NotificationRow notification={item} onPress={openNotification} />\n<\/Swipeable>\n```\n\n**Error States:**\n- `viewState === \"stale\"` → `StaleStrip` with retry\n- `viewState === \"error\"` → `LoadError` with server message\n- `streamFailureStatus` → `StreamStatusStrip` for connection issues\n\n### Settings Screen (`app/(tabs)/settings/index.tsx`)\n\n**Purpose**: Central hub for all settings sub-screens.\n\n**Navigation Structure:**\n```typescript\n<ListRow\n icon=\"person.crop.circle\"\n title={tr(\"settings.profile\")}\n onPress={() => router.push(\"/(tabs)/settings/profile\")}\n/>\n<ListRow\n icon=\"circle.lefthalf.filled\"\n title={tr(\"settings.appearance\")}\n onPress={() => router.push(\"/(tabs)/settings/appearance\")}\n/>\n<ListRow\n icon=\"globe\"\n title={tr(\"settings.language\")}\n onPress={() => router.push(\"/(tabs)/settings/language\")}\n/>\n<ListRow\n icon=\"bell\"\n title={tr(\"settings.notifications\")}\n onPress={() => router.push(\"/(tabs)/settings/notifications\")}\n/>\n```\n\n**Sign Out Flow:**\n```typescript\nfunction confirmSignOut() {\n Alert.alert(\n tr(\"settings.signOutTitle\"),\n tr(\"settings.signOutBody\"),\n [\n { text: tr(\"common.cancel\"), style: \"cancel\" },\n {\n text: tr(\"settings.signOut\"),\n style: \"destructive\",\n onPress: () => {\n void signOut().finally(() => router.replace(\"/login\"));\n },\n },\n ],\n );\n}\n```\n\n---\n\n## Shared Components\n\n### Action Sheet (`components/ui/action-sheet.tsx`)\n\nUniversal long-press menu for cards and rows.\n\n```typescript\n<ActionSheet\n visible={sheetWidget !== null}\n title={sheetWidget?.title || tr(\"dashboard.cardFallback\")}\n subtitle={tr(\"dashboard.actionSheetSubtitle\")}\n actions={sheetActions}\n onClose={() => setSheetWidget(null)}\n/>\n```\n\n**Action Bands:**\n- **Tile**: Large touch targets (move, open web)\n- **Row**: Destructive actions (remove)\n\n### Glass Card (`components/ui/glass-card.tsx`)\n\nConsistent card styling with blur effect matching the tab bar treatment.\n\n### List Row (`components/ui/list-row.tsx`)\n\nReusable list item with icon, title, subtitle, and trailing content.\n\n```typescript\n<ListRow\n icon=\"lock.shield\"\n title={tr(\"settings.security\")}\n subtitle={tr(\"settings.securityHint\")}\n onPress={() => router.push(\"/(tabs)/settings/security\")}\n/>\n```\n\n### Stream Status Strip (`components/ui/stream-status-strip.tsx`)\n\nConnection state indicator for live feeds.\n\n```typescript\n<StreamStatusStrip\n state={stream.state}\n failureStatus={stream.failureStatus}\n replayGap={stream.replayGap}\n reconnecting={stream.reconnecting}\n onReconnect={stream.reconnect}\n onRefresh={() => {\n void refetchInbox();\n void refetchItems();\n }}\n style={styles.streamStrip}\n/>\n```\n\n---\n\n## State Management\n\n### Theme Context (`theme/theme-context.tsx`)\n\n```typescript\nconst { t } = useTheme();\nconst { t: tr } = useI18n();\n```\n\n**Available Tokens:**\n- `t.primary`, `t.mutedForeground`, `t.background`, `t.card`, `t.border`\n- `t.accent`, `t.destructive`, `t.muted`\n\n### I18n Context (`i18n/i18n-context.tsx`)\n\n```typescript\nconst { t: tr, locale, isRTL, restartOwed, restart } = useI18n();\n```\n\n**Language Switching:**\n```typescript\nasync function choose(next: Locale) {\n if (next === locale) return;\n setRestartFailed(false);\n await setLocale(next);\n}\n```\n\n**Restart Notice:**\n```typescript\n{restartOwed ? (\n <GlassCard style={styles.card}>\n <View style={styles.notice}>\n <Text style={[styles.noticeTitle, { color: t.foreground }]}>\n {tr(\"locale.restartTitle\")}\n <\/Text>\n <Text style={[styles.noticeBody, { color: t.mutedForeground }]}>\n {restartFailed ? tr(\"locale.restartFailed\") : tr(\"locale.restartBody\")}\n <\/Text>\n {restartFailed ? null : (\n <Button\n title={tr(\"locale.restartAction\")}\n onPress={() => void onRestart()}\n style={styles.noticeAction}\n />\n )}\n <\/View>\n <\/GlassCard>\n) : null}\n```\n\n### Auth Context (`lib/auth/auth-context.tsx`)\n\n```typescript\nconst { status, lock, unlock, signInPassword, signInPasskey, passkeyAvailable } = useAuth();\n```\n\n**Auth States:**\n- `loading` → Splash overlay\n- `signedOut` → Redirect to `/login`\n- `locked` → Biometric unlock overlay\n- `signedIn` → Tab navigator\n\n---\n\n## API Integration\n\n### Dashboard API (`lib/api/dashboard.ts`)\n\n```typescript\nconst widgets = await dashboardApi.getLayout();\nawait dashboardApi.putLayout(next);\n```\n\n### Board API (`lib/api/agent-board.ts`)\n\n```typescript\nconst rows = await boardApi.listInbox();\nconst nextItems = await boardApi.list();\n```\n\n### Notifications API (`lib/api/notifications.ts`)\n\n```typescript\nconst prefs = await notificationsApi.getPreferences();\nawait notificationsApi.putPreferences(next);\n```\n\n### Governance API (`lib/api/governance.ts`)\n\n```typescript\nconst [authoringResponse, approvals, capabilityResponse, delegationResponse, policyResponse] =\n await Promise.all([\n governanceApi.listAuthoringRequests(),\n governanceApi.pendingApprovals(),\n governanceApi.listCapabilityApprovals(),\n governanceApi.listDelegations(),\n governanceApi.listPolicies(),\n ]);\n```\n\n---\n\n## Offline & Streaming\n\n### Connection Management (`lib/stream/connection.ts`)\n\n```typescript\nconst stream = useBoardStream(applyBoardEvent);\n```\n\n**Stream States:**\n- `connected` → Live updates\n- `reconnecting` → Backoff retry\n- `disconnected` → Stale state with retry\n\n### Resilient Stream (`lib/stream/use-resilient-stream.ts`)\n\n```typescript\nconst streamDisplayState = useDebouncedDegradation(streamState);\n```\n\n**Debounce Strategy:**\n- 2-second degradation window\n- Same hook used across board and notifications\n\n### Read Cache (`lib/offline/read-cache.ts`)\n\n```typescript\nvoid readCache.get<WidgetInstance[]>(LAYOUT_CACHE_KEY).then((cached) => {\n if (!cancelled && cached?.value) setLayout(cached.value);\n});\n```\n\n**Cache Keys:**\n- `LAYOUT_CACHE_KEY` = \"harakat.dashboard.layout\"\n- `BOARD_INBOX_CACHE_KEY`\n- `BOARD_ITEMS_CACHE_KEY`\n\n---\n\n## Security\n\n### Biometric Lock (`lib/auth/biometric-lock.ts`)\n\n```typescript\nconst [bioLock, setBioLock] = useState(false);\nconst [bioLockFailed, setBioLockFailed] = useState(false);\n\nasync function toggleBioLock(value: boolean) {\n const previous = bioLock;\n setBioLock(value);\n setBioSaving(true);\n try {\n await setBioLockEnabled(value);\n } catch {\n setBioLock(previous);\n Alert.alert(tr(\"security.lockFailedTitle\"), tr(\"security.lockFailedBody\"));\n } finally {\n setBioSaving(false);\n }\n}\n```\n\n### Passkeys (`lib/auth/passkeys.ts`)\n\n```typescript\nconst credential = await registerPasskey(\"Mobile\");\nawait passkeysApi.revoke(credential.id);\n```\n\n### Sessions (`lib/api/sessions.ts`)\n\n```typescript\nawait sessionsApi.revoke(session.id);\nawait sessionsApi.revokeOthers();\n```\n\n---\n\n## Deep Linking & Notifications\n\n### Notification Parker (`lib/deep-link-parker.ts`)\n\nHandles notification taps that occur before app is ready.\n\n```typescript\nconst parkerRef = useRef<DeepLinkParkerState>(initialParkerState);\nconst statusRef = useRef<DeepLinkAuthStatus>(status);\nstatusRef.current = status;\n\nconst feedParker = useCallback(\n (event: Parameters<typeof parkerReducer>[1]) => {\n const result = parkerReducer(parkerRef.current, event, statusRef.current);\n parkerRef.current = result.state;\n if (result.dispatch) router.push(resolveDeepLink(result.dispatch));\n },\n [router],\n);\n```\n\n**Cold Start Handling:**\n```typescript\nNotifications.getLastNotificationResponseAsync()\n .then((response) => {\n const actionUrl = response?.notification.request.content.data?.actionUrl;\n if (typeof actionUrl === \"string\" && actionUrl.length > 0) {\n feedParker({ type: \"link\", actionUrl });\n }\n })\n .catch(() => {});\n```\n\n---\n\n## Error Handling Patterns\n\n### Load Error (`components/ui/load-error.tsx`)\n\n```typescript\n<LoadError\n title={tr(\"dashboard.loadFailedTitle\")}\n body={tr(\"dashboard.loadFailedBody\")}\n onRetry={() => void refresh()}\n/>\n```\n\n### Stale Strip (`components/ui/stale-strip.tsx`)\n\n```typescript\n<StaleStrip\n message={tr(\"dashboard.refreshFailed\")}\n retrying={refreshing}\n onRetry={() => void refresh()}\n/>\n```\n\n### Empty State (`components/ui/empty-state.tsx`)\n\n```typescript\n<EmptyState\n icon=\"bell\"\n title={tr(\"notifications.title\")}\n message={tr(\"notifications.allCaughtUpPeriod\")}\n/>\n```\n\n---\n\n## Key Design Decisions\n\n### 1. Clearance Contract\nThe tab bar height is computed once and published to all screens, preventing height mismatches between the bar and its content.\n\n### 2. Error vs Empty States\nFailed loads never fall back to empty states. A failed read shows error UI; an empty list shows empty state. This prevents misleading \"nothing here\" messages when data is unavailable.\n\n### 3. RTL Support\nBack chevrons flip based on `isRTL`:\n```typescript\n<Icon\n name={isRTL ? \"chevron.right\" : \"chevron.left\"}\n size={18}\n color={t.foreground}\n/>\n```\n\n### 4. Toast Error Messages\nAPI errors are parsed and displayed:\n```typescript\nfunction errorMessage(error: unknown, fallback: string): string {\n if (error instanceof ApiError) {\n try {\n const parsed = JSON.parse(error.body) as { error?: string; message?: string };\n return parsed.error ?? parsed.message ?? fallback;\n } catch {\n return error.body || fallback;\n }\n }\n return error instanceof Error ? error.message : fallback;\n}\n```\n\n---\n\n## Testing Considerations\n\n### State Verification\n- Verify `loadFailed` is separate from `layout.length === 0`\n- Confirm cache is written on successful operations\n- Test offline states don't render empty lists\n\n### Navigation\n- Tab switching preserves state\n- Deep links work from notifications\n- Back navigation respects reading direction\n\n### Accessibility\n- All Pressable elements have `accessibilityRole=\"button\"`\n- Icons have `accessibilityLabel`\n- Swipe actions are announced\n\n---\n\n## Related Modules\n\n| Module | Purpose |\n|--------|---------|\n| `lib/api/*` | API client implementations |\n| `lib/stream/*` | Connection and streaming logic |\n| `lib/auth/*` | Authentication and security |\n| `lib/notifications/*` | Notification feed management |\n| `lib/offline/*` | Cache and offline support |\n| `components/ui/*` | Shared UI components |\n| `theme/*` | Styling and theming |\n| `i18n/*` | Localization and translation |\n\n---\n\n## Contributing Guidelines\n\n1. **Use existing patterns**: Match the error handling, state management, and component structure of existing screens\n2. **Respect clearance contract**: Don't modify tab bar height directly; use `useTabBarClearance()`\n3. **Separate error from empty**: Never conflate failed loads with empty states\n4. **RTL-aware**: Flip chevrons and test in both directions\n5. **Cache writes**: Always write to cache on successful operations\n6. **Stream resilience**: Use `useDebouncedDegradation` for live feeds","harakatmobile-store":"# HarakatMobile — store\n\n\n\n# HarakatMobile — Store Readiness Module\n\n## Overview\n\nThe `store` module contains the configuration, compliance artifacts, and activation runbooks required to submit the Harakat Mobile application (`1.0.0`) to the Apple App Store and Google Play Store. This module does not contain runtime business logic; it serves as the bridge between the application build and the respective app store review processes.\n\nThe module relies on the Expo Application Services (EAS) configuration (`app.json`, `eas.json`) and external web hosts to validate app links, push notifications, and WebAuthn credentials.\n\n## Architecture & Dependencies\n\nThis module is configuration-driven. It does not execute internal logic but defines the environment for the build pipeline.\n\n### Core Configuration\n- **`app.json`**: Defines EAS build profiles (`development`, `internal`, `production`), app links, associated domains, and asset paths.\n- **`eas.json`**: Defines EAS submit profiles (`internal`, `production`) and placeholder strings for credentials.\n- **`assets/images/*`**: Contains raster assets (icons, splash, notification) required for store listings.\n\n### External Dependencies\nThe module requires specific files to be deployed to web hosts to function correctly during store submission:\n- **Apple App Site Association (AASA)**: `https://app.harakat.tech/.well-known/apple-app-site-association`\n- **Android Asset Links**: `https://app.harakat.tech/.well-known/assetlinks.json`\n- **Firebase Cloud Messaging (FCM)**: Configured via environment variables on the production server.\n\n### Execution Flow\nThere are no internal execution flows or runtime calls within this module. The \"execution\" occurs during the EAS build and submit processes triggered by the owner.\n\n```mermaid\nflowchart TD\n A[Developer Config] -->|app.json, eas.json| B_EAS[\"B(EAS\"] Build Pipeline)\n B -->|Credentials| C[Web Hosts]\n C -->|AASA, Asset Links| D[App Store / Play Store]\n B -->|Assets| E[Store Submission]\n E -->|Review| F[Store Approval]\n C -->|WebAuthn, Push| G[Production Server]\n```\n\n## Configuration Files\n\n### Privacy & Compliance\nThis module maintains the answers required for store privacy labels. These must match the actual behavior of the production build.\n\n- **`app-store-privacy.md`**: Defines the App Store Connect privacy nutrition label answers.\n - **Tracking**: Declared as `No`.\n - **Data Collected**: Email Address, Name, Device ID (Push Token), Crash Data (conditional).\n - **Purpose**: All data is declared for `App Functionality`.\n- **`play-data-safety.md`**: Defines the Google Play Data Safety form answers.\n - **Data Types**: Personal Info (Email, Name), Device IDs (Push Token).\n - **Security**: Data encrypted in transit; deletion path available via support/admin.\n- **`ios-privacy-manifest.md`**: Documents the expected `PrivacyInfo.xcprivacy` content generated by EAS.\n - **API Categories**: `UserDefaults`, `FileTimestamp`, `DiskSpace`, `SystemBootTime` (Standard Expo reasons).\n\n### Asset Specification\n- **`assets-spec.md`**: Defines the required brand assets.\n - **Brand Colors**: Ink (`#0A2E1E`), Green Mark (`#359167`), Shadda Accent (`#D2C49E`).\n - **Required Slots**: App Icon (1024x1024), iOS Icon Composer, Android Adaptive (Foreground/Background/Monochrome), Splash, Favicon, Notification Icon.\n - **Current State**: Contains Expo scaffold placeholders. Real brand art must be produced before submission.\n\n### Credentials & Well-Known Runbook\n- **`credentials-and-wellknown.md`**: Owner-only activation steps.\n - **Apple**: Run `npx eas credentials --platform ios`. Replace `<APPLE_TEAM_ID>` in AASA files.\n - **Android**: Run `npx eas credentials --platform android`. Replace `<SHA256_CERT_FINGERPRINT>` in Asset Links files.\n - **WebAuthn**: Set `WEBAUTHN_RP_ID=app.harakat.tech` and `WEBAUTHN_ORIGIN` with `apk-key-hash`.\n - **Push/Refresh**: Set `MOBILE_REFRESH_ENABLED=on` and `PUSH_ENABLED=on` on the production server.\n\n## Deployment & Activation Workflow\n\nThe following workflow outlines the steps required to move from a local build to a store submission.\n\n### 1. Configuration & Credentials\nBefore building, the owner must generate credentials and update the web hosts.\n\n1. **Generate Credentials**:\n ```bash\n npx eas credentials --platform ios\n npx eas credentials --platform android\n ```\n2. **Update Web Hosts**:\n - Replace `<APPLE_TEAM_ID>` in `StartupsPlatform/public/.well-known/apple-app-site-association`.\n - Replace `<SHA256_CERT_FINGERPRINT>` in `StartupsPlatform/public/.well-known/assetlinks.json`.\n - Redeploy both `app.harakat.tech` and `Harakat_Landing` hosts.\n3. **Server Environment**:\n - Configure production server environment variables:\n - `WEBAUTHN_RP_ID=app.harakat.tech`\n - `MOBILE_REFRESH_ENABLED=on`\n - `PUSH_ENABLED=on`\n - `FCM_PROJECT_ID=<FCM_PROJECT_ID>`\n\n### 2. Asset Production\nReplace placeholder assets with final brand art.\n- **iOS**: Update `assets/expo.icon/` bundle.\n- **Android**: Update `assets/images/android-icon-foreground.png`, `android-icon-background.png`, `android-icon-monochrome.png`.\n- **Notifications**: Create `assets/images/notification-icon.png` (White silhouette on transparent).\n\n### 3. Build & Verification\n1. **Internal Build**: Run `npx eas build --platform ios --profile internal` and `--platform android --profile internal`.\n2. **Physical Verification**: Install on physical devices to verify:\n - App links resolve correctly.\n - Push notifications deliver.\n - WebAuthn passkeys validate.\n3. **TestFlight / Internal Testing**: Submit to respective internal testing tracks.\n\n### 4. Submission\n1. **Fill Placeholders**: Update `eas.json` with:\n - `<APPLE_ID>`\n - `<ASC_APP_ID>`\n - `<APPLE_TEAM_ID>`\n - `<GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH>`\n2. **Submit**: Run `npx eas submit --platform ios --profile production` and `--platform android --profile production`.\n\n## Owner Responsibilities\n\nThe following tasks are deferred to the owner and are not part of the standard developer workflow:\n\n1. Produce and replace real brand raster assets.\n2. Run `npx eas credentials` for Apple and Android signing.\n3. Backfill well-known files in both web hosts.\n4. Configure production server env for WebAuthn, mobile refresh, and push.\n5. Build internal iOS and Android apps with EAS.\n6. Install on physical devices and complete manual verification.\n7. Submit to TestFlight and Google Play internal testing.\n\n## Compliance Notes\n\n- **Data Deletion**: Users must be able to request account deletion via Harakat support or workspace-admin. This path must remove account profile data, active sessions, refresh tokens, and server-side workspace records.\n- **Diagnostics**: Crash and performance data are only declared if enabled in the production build.\n- **Third-Party Ads**: Do not select Third-Party Advertising or Marketing purposes unless the production server adds those features later.","harakatmobile":"# HarakatMobile\n\n\n\n# HarakatMobile Module\n\n## Overview\n\nThe **HarakatMobile** module is the primary client application for the Harakat workspace, built as an Expo React Native application. It serves as the entry point for user interaction, handling authentication, biometric security, offline capabilities, and core workspace features (Dashboard, Agent Board, Notifications, Settings).\n\nThe module is structured to support store submission (iOS App Store and Google Play) while relying on external services for API communication, push notifications, and secure storage. It integrates native iOS capabilities with a JavaScript runtime to deliver a responsive, secure user experience.\n\n## Architecture\n\nThe application follows a layered architecture where the native container manages the environment, and the core logic handles data and UI.\n\n* **Native Container:** The `ios` module wraps the React Native application using the Expo SDK, managing the `AppDelegate` lifecycle, native permissions, and deep linking.\n* **Core Logic:** The `src` module contains the business logic, including resilient stream connections, authentication token management, and offline synchronization.\n* **UI & Assets:** Static resources are defined in `assets`, while interactive components (e.g., `AskSheet`, `GovernanceScreen`) reside in `src/components`.\n* **Deployment:** The `store` module configures the build pipeline and compliance artifacts required for App Store and Play Store submission.\n\n## Key Workflows\n\n### Authentication & Security\nUser sessions are managed through the `lib/auth/token-store.ts`, which provides access tokens to the `lib/api/client.ts`. Security features, such as passkey support, are validated in `lib/auth/passkeys.ts` and exposed via the `security.tsx` settings screen.\n\n### Data Synchronization\nThe application maintains data integrity through a resilient stream architecture. The `lib/stream/connection.ts` manages connection states, utilizing `lib/stream/backoff.ts` for reconnection logic and `lib/stream/stall-watchdog.ts` to monitor stream health. Data is persisted locally via `lib/offline/outbox.ts` and `lib/offline/read-cache.ts` when connectivity is lost.\n\n### Internationalization & Rendering\nText rendering and directionality are controlled by the `src/i18n` context. The `direction.ts` module determines the active layout direction, influencing components like `ThemedText` and routing logic in `src/i18n/routing.ts`.\n\n### Build & Validation\nPre-deployment validation is handled by scripts in `scripts/`. These include `check-i18n.js` for catalog validation, `harness-selftest.mjs` for self-tests, and `rtl-render-check.mjs` for right-to-left rendering verification.\n\n## Module Relationships\n\n```mermaid\ngraph TD\n subgraph Native Layer\n iOS[HarakatMobile — ios]\n end\n\n subgraph Application Layer\n App[HarakatMobile — src]\n Assets[HarakatMobile — assets]\n Scripts[HarakatMobile — scripts]\n end\n\n subgraph Deployment Layer\n Store[HarakatMobile — store]\n end\n\n iOS -->|Wraps | App\n App -->|Uses | Assets\n App -->|Executes | Scripts\n App -->|Configured by | Store\n Store -->|Validates | Scripts\n```\n\n## Sub-Modules\n\n* **[HarakatMobile — assets](assets.md)**: Manages static resource configurations, including `icon.json` for visual rendering properties.\n* **[HarakatMobile — ios](ios.md)**: Native iOS implementation utilizing the Expo SDK and Xcode project structure.\n* **[HarakatMobile — scripts](scripts.md)**: Build and validation scripts for i18n checks, self-tests, and RTL rendering verification.\n* **[HarakatMobile — src](src.md)**: Core application logic, including stream connections, authentication, offline caching, and UI components.\n* **[HarakatMobile — store](store.md)**: Configuration and compliance artifacts for App Store and Google Play submission.","harakatoffices-server-docs":"# HarakatOffices_Server — docs\n\n\n\n# HarakatOffices_Server — docs Module\n\n## Overview\n\nThe `docs` module contains comprehensive documentation for the HarakatOffices_Server application, covering authentication flows, service management, database storage, and deployment procedures. This documentation serves as the primary reference for developers working on the backend server.\n\n## Documentation Structure\n\n```\ndocs/\n├── 2FA_TESTING_GUIDE.md # Two-Factor Authentication testing procedures\n├── HERMES_BACKEND_ROLLOUT.md # Backend deployment and rollout procedures\n├── PROVIDER_SERVICES_FLOW_DIAGRAM.md # Visual flow for service management\n├── PROVIDER_SERVICES_STORAGE.md # Database storage details for provider services\n├── SERVICES_STORAGE_QUICK_REF.md # Quick reference for service storage locations\n├── SERVICE_STORAGE_COMPLETE_GUIDE.md # Complete guide for service storage\n└── TWO_FACTOR_AUTH_GUIDE.md # 2FA setup and configuration guide\n```\n\n## Key Documentation Files\n\n### 1. Two-Factor Authentication Documentation\n\n**Files**: `2FA_TESTING_GUIDE.md` and `TWO_FACTOR_AUTH_GUIDE.md`\n\nThese documents cover the complete 2FA implementation including:\n\n- **Email Configuration**: SMTP setup for Gmail, Mailtrap, SendGrid, Mailgun, and AWS SES\n- **Database Schema**: 2FA fields added to the `Admin` table (`twoFactorEnabled`, `twoFactorCode`, `twoFactorExpiry`)\n- **API Endpoints**:\n - `POST /api/settings/toggle-2fa` - Enable/disable 2FA\n - `GET /api/settings/2fa-status` - Check 2FA status\n - `POST /api/auth/verify-2fa` - Verify 2FA code\n\n**Code References**:\n- Controller: `src/modules/admin/auth.controller.ts`\n- Email Service: `src/core/services/email.service.ts`\n- Frontend: `app/login/page.tsx` and `app/admin/settings/page.tsx`\n\n**Execution Flow**:\n\n```mermaid\nsequenceDiagram\n participant User\n participant Frontend\n participant Backend\n participant EmailService\n participant Database\n\n User->>Frontend: Login with email/password\n Frontend->>Backend: POST /api/auth/login\n Backend->>Database: Check user credentials\n Backend->>Database: Check twoFactorEnabled\n alt 2FA Enabled\n Backend->>EmailService: Send verification code\n EmailService->>Database: Store code + expiry\n EmailService->>User: Send email with 6-digit code\n User->>Frontend: Enter verification code\n Frontend->>Backend: POST /api/auth/verify-2fa\n Backend->>Database: Verify code\n Backend->>Frontend: Login success\n else 2FA Disabled\n Backend->>Frontend: Login success\n end\n```\n\n### 2. Provider Services Documentation\n\n**Files**: `PROVIDER_SERVICES_FLOW_DIAGRAM.md`, `PROVIDER_SERVICES_STORAGE.md`, `SERVICES_STORAGE_QUICK_REF.md`, `SERVICE_STORAGE_COMPLETE_GUIDE.md`\n\nThese documents cover the service management system for external providers (companies/freelancers).\n\n**Database Schema**:\n\n```prisma\nmodel ProviderService {\n id Int @id @default(autoincrement())\n providerId Int\n name String\n description String?\n creditCost Int\n provider Provider @relation(fields: [providerId], references: [id], onDelete: Cascade)\n}\n```\n\n**API Endpoints**:\n\n| Action | Method | Endpoint | Controller Function |\n|--------|--------|----------|---------------------|\n| Add Service | POST | `/api/admin/providers/:id/services` | `addProviderService()` |\n| View Services | GET | `/api/admin/providers/:id/services` | `getProviderServices()` |\n| Update Service | PUT | `/api/admin/providers/:id/services/:serviceId` | `updateProviderService()` |\n| Delete Service | DELETE | `/api/admin/providers/:id/services/:serviceId` | `deleteProviderService()` |\n\n**Service Management Flow**:\n\n```mermaid\nflowchart TD\n A[Harakat Admin UI] --> B[Edit Company Dialog]\n B --> C[Add Service Form]\n C --> D[POST /api/admin/providers/:id/services]\n D --> E[providers.controller.ts]\n E --> F[ProviderService.create()]\n F --> G[PostgreSQL Database]\n G --> H[Success Response]\n H --> I[Frontend Update]\n```\n\n**Key Distinctions**:\n\n| Table | Purpose | Used By |\n|-------|---------|---------|\n| `ProviderService` | Services offered by external providers | Harakat Admin marketplace |\n| `Service` | Platform-wide service definitions | Office capabilities |\n| `OfficeService` | Services enabled for specific offices | Office approvals |\n\n### 3. Hermes Backend Rollout Documentation\n\n**File**: `HERMES_BACKEND_ROLLOUT.md`\n\nThis document covers the additive deployment of new backend features:\n\n**New Features Added**:\n- Canonical agent directory\n- Scenario run management\n- Scheduled agent jobs\n- Audit ingestion\n- MCP catalog\n- Docker session infrastructure\n\n**Deployment Order**:\n\n```mermaid\nflowchart LR\n A[1. Database Migration] --> B[2. Generate Prisma Client]\n B --> C[3. Deploy API with New Routes]\n C --> D[4. Deploy Workers with ENABLE_QUEUE_WORKERS=true]\n D --> E[5. Enable WORKER_SCHEDULER_ENABLED in Staging]\n```\n\n**Safety Features**:\n- Migration only adds tables, indexes, foreign keys\n- Existing `AssistantScenarioRun` and `AssistantScheduledTask` paths remain readable\n- New MCP invocation uses server-derived JWT claims\n- Agent session Docker plans default to secure settings (`NetworkMode=none`, `ReadonlyRootfs=true`, `CapDrop=ALL`)\n\n**Rollback Procedure**:\n1. Disable workers: `ENABLE_QUEUE_WORKERS=false`\n2. Cancel pending BullMQ `scheduled-agent-*` jobs\n3. Redeploy previous API image\n4. Leave additive tables in place (inert for previous API)\n\n## Codebase Integration\n\n### Database Layer\n\nAll documentation references the Prisma schema located at `prisma/schema.prisma`. Key models include:\n\n- `Admin` - User accounts with 2FA support\n- `Provider` - External service providers (companies/freelancers)\n- `ProviderService` - Services offered by providers\n- `Service` - Platform-wide service definitions\n- `OfficeService` - Office-approved services\n\n### API Layer\n\nDocumentation references the following controller files:\n\n- `src/modules/admin/providers/providers.controller.ts` - Provider service CRUD operations\n- `src/modules/admin/auth.controller.ts` - Authentication and 2FA handling\n- `src/core/services/email.service.ts` - Email sending functionality\n\n### Frontend Integration\n\n- `Harakat_Admin/components/marketplace/` - Admin panel UI for service management\n- `app/login/page.tsx` - Login flow with 2FA support\n- `app/admin/settings/page.tsx` - Settings page with 2FA toggle\n\n## Contributing to Documentation\n\n### Adding New Documentation\n\n1. Create a new `.md` file in the `docs/` directory\n2. Follow existing documentation style (headings, code blocks, Mermaid diagrams)\n3. Reference actual function names, class names, and file paths\n4. Include code examples where applicable\n5. Link to related documentation files\n\n### Updating Existing Documentation\n\n- Update code examples to match current implementation\n- Add troubleshooting sections for common issues\n- Include security considerations for sensitive features\n- Document API rate limits and error responses\n\n### Documentation Best Practices\n\n- **Accuracy**: Verify all code snippets against the actual implementation\n- **Completeness**: Cover normal flows, error cases, and edge cases\n- **Clarity**: Use Mermaid diagrams for complex flows (5-10 nodes max)\n- **Searchability**: Use consistent headings and terminology\n- **Versioning**: Include last updated dates and version numbers\n\n## Quick Reference\n\n### Common Commands\n\n```bash\n# Database operations\nnpx prisma migrate dev\nnpx prisma generate\nnpx prisma studio\n\n# Server operations\npnpm dev\npnpm build\n```\n\n### Testing 2FA\n\n```bash\n# Test SMTP connection\ncd HarakatOffices_Server\nnpx tsx src/core/services/email.service.ts\n\n# Check 2FA status via API\ncurl http://localhost:5000/api/settings/2fa-status \\\n -H \"Authorization: Bearer <token>\"\n```\n\n### Database Queries\n\n```typescript\n// Get provider with services\nconst provider = await prisma.provider.findUnique({\n where: { id: 1 },\n include: { services: true },\n});\n\n// Get all services for a provider\nconst services = await prisma.providerService.findMany({\n where: { providerId: 1 },\n orderBy: { id: \"desc\" },\n});\n```\n\n## Related Modules\n\n- **Harakat_Admin** - Frontend application\n- **HarakatOffices_Server** - Backend API server\n- **Database** - PostgreSQL with Prisma ORM\n- **Email Service** - SMTP-based email delivery\n\n---\n\n**Last Updated**: October 30, 2025 \n**Documentation Version**: 1.0","harakatoffices-server-harakatoffices-server":"# HarakatOffices_Server — HarakatOffices_Server\n\n\n\n# HarakatOffices_Server Module Documentation\n\n## Overview\n\nThe **HarakatOffices_Server** is a Node.js/TypeScript backend service that provides enterprise-grade document management, AI-powered knowledge extraction, Google Drive synchronization, and ERP integration capabilities. It serves as the core orchestration layer for the Harakat platform, managing file processing pipelines, user authentication, and business logic for multiple tenants.\n\n### Core Responsibilities\n\n- **Document Ingestion & Processing**: Syncs files from Google Drive, extracts content from various formats (PDF, Google Docs, spreadsheets, archives), and builds a knowledge graph\n- **AI/LLM Integration**: Powers assistant conversations, meeting transcripts, and minutes-of-meeting generation\n- **Drive Integration**: Manages Google Drive OAuth, token refresh, and file access with special handling for malware-flagged archives\n- **ERP Integration**: Connects with Odoo ERP for employee data, project stages, and organizational structures\n- **Security & Governance**: Implements 2FA, OpenFGA authorization, personal policies, and tenant isolation\n- **Observability**: Provides diagnostic scripts, health checks, and audit trails\n\n---\n\n## Architecture\n\n### High-Level Flow\n\n```mermaid\ngraph TB\n A[Google Drive] -->|OAuth2| B[Drive Integration]\n B -->|Sync| C[Sync Job Queue]\n C -->|Download| D[File Processor]\n D -->|Extract| E[Content Extractor]\n E -->|Parse| F[Parsed Document]\n F -->|Build| G[Knowledge Graph]\n G -->|Query| H[Assistant AI]\n H -->|Response| I[User Frontend]\n \n J[Odoo ERP] -->|Employee Data| K[StartupUser]\n L[Email Service] -->|2FA| M[User Auth]\n \n N[Diagnostic Scripts] -->|Monitor| O[Prisma DB]\n```\n\n### Service Components\n\n| Component | Purpose | Key Files |\n|-----------|---------|-----------|\n| **Main Server** | Express API, route handlers | `src/server.ts`, `src/app.ts` |\n| **Drive Service** | Google Drive OAuth, file operations | `src/core/drive/google-drive.service.js` |\n| **Archive Extractor** | Handles RAR/ZIP with malware scanning | `src/core/archive/archive-extract.service.js` |\n| **Queue System** | BullMQ workers for async jobs | `src/core/queue/index.js` |\n| **Knowledge Graph** | Node/Edge relationships | `src/core/knowledge/` |\n| **AI Service** | Assistant conversations, transcripts | `src/core/ai/` |\n| **ERP Connector** | Odoo JSON-RPC integration | `src/core/erp/` |\n\n---\n\n## Database Schema (Prisma)\n\n### Core Tables\n\n```typescript\n// Knowledge Graph\nKnowledgeNode {\n id: String\n startupId: Int\n kind: 'file' | 'folder' | 'drive'\n name: String\n externalId: String\n metadata: Json // mimeType, contentExtracted, driveFileId, etc.\n summary: String?\n}\n\nKnowledgeEdge {\n id: String\n startupId: Int\n sourceId: Int\n targetId: Int\n relation: 'CONTAINS' | 'LINKS_TO' | 'PARSING_OF'\n}\n\nParsedDocument {\n id: String\n nodeId: Int\n fileName: String\n parseStatus: 'pending' | 'parsing' | 'completed' | 'failed'\n textLength: Int\n pageCount: Int\n parseMethod: String\n parseError: String?\n}\n\nSyncJob {\n id: String\n startupId: Int\n integrationId: Int\n status: 'pending' | 'running' | 'completed' | 'failed'\n totalFiles: Int\n processedFiles: Int\n errorCount: Int\n}\n\nDriveIntegration {\n id: String\n startupId: Int\n type: String\n refreshToken: String\n accessToken: String\n scopedFolders: Json\n isActive: Boolean\n}\n\nStartupUser {\n id: String\n startupId: Int\n email: String\n name: String\n role: String\n erpUserId: Int?\n isActive: Boolean\n}\n```\n\n### Key Relationships\n\n```mermaid\ngraph LR\n A[DriveIntegration] -->|1:N| B[SyncJob]\n B -->|1:N| C[KnowledgeNode]\n C -->|1:N| D[KnowledgeEdge]\n C -->|1:N| E[ParsedDocument]\n F[StartupUser] -->|1:1| G[Startup]\n H[ERP] -->|1:N| F\n```\n\n---\n\n## Configuration & Environment\n\n### Required Environment Variables\n\n```bash\n# Database\nPOSTGRES_USER=harakat\nPOSTGRES_PASSWORD=harakat_secret\nPOSTGRES_DB=harakatoffices_db\n\n# JWT & Security\nJWT_SECRET=your-super-secret-jwt-key-here\nSECRET_ENCRYPTION_KEY=base64-encoded-32-byte-key\n\n# Google Drive OAuth\nGOOGLE_CLIENT_ID=your-client-id\nGOOGLE_CLIENT_SECRET=your-client-secret\n\n# Email (SMTP)\nSMTP_HOST=smtp.gmail.com\nSMTP_PORT=587\nSMTP_USER=your-email@gmail.com\nSMTP_PASS=your-app-password\n\n# Redis (BullMQ)\nREDIS_URL=redis://localhost:6379\n\n# Odoo ERP\nERP_URL=http://127.0.0.1:8069\nERP_DB=erp\nERP_SERVICE_USER=admin\nERP_SERVICE_PASSWORD=admin\n```\n\n### Environment Files\n\n- `.env` — Production configuration\n- `.env.local` — Local development overrides\n- `.env.production` — Production-specific settings\n\n---\n\n## Diagnostic & Testing Scripts\n\n### Drive & Archive Diagnostics\n\n| Script | Purpose | Usage |\n|--------|---------|-------|\n| `_diag_archive_download.js` | Test malware-flagged archive downloads | `node _diag_archive_download.js` |\n| `_smoke_download_bytes.js` | Verify `downloadDriveFileBytes` recovery | `node _smoke_download_bytes.js` |\n| `_smoke_archive_extract.js` | Test archive extraction pipeline | `node _smoke_archive_extract.js` |\n| `_check_content.js` | Audit content extraction by MIME type | `node _check_content.js` |\n| `_check_db.js` | Database health check | `node _check_db.js` |\n\n### Sync & Queue Diagnostics\n\n| Script | Purpose | Usage |\n|--------|---------|-------|\n| `_progress.js` | Check latest sync job progress | `node _progress.js` |\n| `_verify_sync.js` | Verify sync completeness | `node _verify_sync.js` |\n| `_smoke_queue.js` | Test BullMQ queue plumbing | `node _smoke_queue.js` |\n| `_reset_sync.js` | Clear sync data for fresh run | `node _reset_sync.js` |\n\n### Feature-Specific Diagnostics\n\n| Script | Purpose | Usage |\n|--------|---------|-------|\n| `_check_transcript_status.js` | Meeting transcript pipeline status | `node _check_transcript_status.js [meetingId]` |\n| `_probe19_driveaudit.js` | Drive audit for specific startup | `node _probe19_driveaudit.js` |\n| `_check_all_types.js` | MIME type extraction audit | `node _check_all_types.js` |\n| `_check_doc.js` | Document content verification | `node _check_doc.js` |\n\n### ERP & User Diagnostics\n\n| Script | Purpose | Usage |\n|--------|---------|-------|\n| `_probe_ws5_identity.ts` | ERP employee mapping | `pnpm exec tsx _probe_ws5_identity.ts` |\n| `_probe_ws7_e2e.cjs` | Personal governance e2e | `pnpm exec tsx _probe_ws7_e2e.cjs` |\n| `_probe_ws10_e2e.cjs` | Weekly notes e2e | `pnpm exec tsx _probe_ws10_e2e.cjs` |\n| `check-zoho.js` | SMTP/IMAP testing | `node check-zoho.js` |\n\n---\n\n## Key Workflows\n\n### 1. Drive Sync Pipeline\n\n```mermaid\nsequenceDiagram\n participant Drive as Google Drive\n participant Server as Harakat Server\n participant Queue as BullMQ\n participant Extract as Extractor\n participant DB as Prisma DB\n \n Drive->>Server: Heartbeat (file changes)\n Server->>Queue: Enqueue sync job\n Queue->>Server: Worker claims job\n Server->>Drive: Download file (with copy workaround if needed)\n Drive-->>Server: File bytes\n Server->>Extract: Parse/extract content\n Extract-->>Server: Parsed text\n Server->>DB: Create KnowledgeNode + ParsedDocument\n Server->>DB: Build KnowledgeEdge relationships\n```\n\n### 2. Meeting Transcript Pipeline\n\n```mermaid\ngraph TD\n A[Meeting Ends] --> B{transcriptStatus}\n B -->|null| C[Enqueue Batch]\n B -->|pending| D[Worker Claims]\n B -->|running| E[Google STT Polling]\n B -->|completed| F[Generate MoM]\n E --> G{STT Complete?}\n G -->|yes| F\n G -->|no| E\n F --> H[Send Email]\n```\n\n### 3. Token Refresh with Copy Workaround\n\n```mermaid\nflowchart TD\n A[Download Request] --> B{File Downloadable?}\n B -->|yes| C[Direct Download]\n B -->|no| D[Refresh Access Token]\n D --> E{acknowledgeAbuse=true?}\n E -->|yes| F[Download with Flag]\n E -->|no| G[Copy File to User Drive]\n G --> H[Download Copy]\n F --> I[Cleanup]\n H --> I\n C --> I\n```\n\n---\n\n## API Endpoints\n\n### Authentication\n\n```typescript\nPOST /api/startup-auth/login\n// Returns: { token: string, user: { ... } }\n\nPOST /api/startup-auth/2fa/enable\n// Enables 2FA for user\n\nPOST /api/startup-auth/2fa/verify\n// Verifies 2FA code\n```\n\n### Drive Operations\n\n```typescript\nPOST /api/startup/parsing/backfill\n// Backfill parsed documents\n\nGET /api/startup/assistants/conversations\n// List conversations\n\nPOST /api/startup/assistants/conversations\n// Create conversation\n\nPOST /api/startup/assistants/conversations/{id}/messages/stream\n// Stream assistant response (SSE)\n\nPATCH /api/startup/assistants/conversations/{id}/notes\n// Update weekly notes\n```\n\n### Governance\n\n```typescript\nPOST /api/startup/governance/integration-grants\n// Grant integration access to user\n\nPUT /api/startup/governance/my/integration-toggles/{key}\n// Toggle integration on/off\n\nPOST /api/startup/policies\n// Create personal policy\n```\n\n---\n\n## Build & Deployment\n\n### Docker Build\n\n```bash\n# Multi-stage build (Node 22 required)\ndocker build -t harakatoffices_server:latest .\n\n# Production image\ndocker build --target runner -t harakatoffices_server:prod .\n```\n\n### Local Development\n\n```bash\n# Start server\npnpm dev\n\n# Start with specific features\npnpm dev -- PERSONAL_GOVERNANCE=on\n\n# Run diagnostics\npnpm test:email\npnpm check:2fa\n```\n\n### Docker Compose\n\n```bash\n# Start all services\ndocker compose up -d\n\n# Start workers only\ndocker compose up -d langwatch_workers\n```\n\n---\n\n## Troubleshooting\n\n### Common Issues\n\n#### 1. Archive Download Fails\n\n**Symptom**: `heartbeat_sweep_skipped` with `reason: not_downloadable`\n\n**Solution**:\n```bash\n# Run diagnostic\nnode _diag_archive_download.js\n\n# Check if acknowledgeAbuse=true helps\n# If yes, the copy workaround is working\n```\n\n#### 2. Sync Job Stuck\n\n**Symptom**: `status: running` for extended period\n\n**Solution**:\n```bash\n# Check worker logs\ndocker logs harakatoffices_server\n\n# Reset stuck job\nnode _reset_sync.js\n\n# Verify with progress check\nnode _progress.js\n```\n\n#### 3. 2FA Not Working\n\n**Symptom**: Email not received or code rejected\n\n**Solution**:\n```bash\n# Test email configuration\npnpm test:email\n\n# Check 2FA status\npnpm check:2fa\n\n# Verify SMTP settings in .env\n```\n\n#### 4. Token Refresh Fails\n\n**Symptom**: `ERR_PNPM_IGNORED_BUILDS` or `ERR_UNKNOWN_BUILTIN_MODULE`\n\n**Solution**:\n```bash\n# Ensure Node 22 (pnpm 11 requires it)\nnode --version # Should be 22.x\n\n# Test token refresh directly\nnode _test_refresh.js\n```\n\n---\n\n## Security Considerations\n\n### JWT Handling\n\n```typescript\n// Always use config.JWT_SECRET from environment\nimport { config } from \"./src/core/config/env\";\nconst token = jwt.sign(payload, config.JWT_SECRET, { expiresIn: '1d' });\n```\n\n### Secret Encryption\n\n```typescript\n// Decrypt stored secrets\nfunction decryptToken(stored: string): string {\n const key = keyFrom(process.env.SECRET_ENCRYPTION_KEY);\n return decryptWith(stored, key);\n}\n```\n\n### Tenant Isolation\n\n```typescript\n// Always filter by startupId\nconst nodes = await prisma.knowledgeNode.findMany({\n where: { startupId: currentStartupId }\n});\n```\n\n---\n\n## Contributing Guidelines\n\n### Adding New Diagnostic Scripts\n\n1. Place in root directory with `_` prefix\n2. Use `PrismaClient` for database access\n3. Include clear usage instructions in comments\n4. Ensure read-only operations where possible\n\n### Adding New API Endpoints\n\n1. Define route in `src/app.ts`\n2. Add handler in `src/routes/`\n3. Include validation in `src/middleware/`\n4. Add tests in `src/scripts/`\n\n### Queue Job Patterns\n\n```typescript\n// Standard job structure\n{\n startupId: number,\n integrationId: number,\n nodeId: number,\n driveFileId: string,\n fileName: string,\n mimeType: string,\n trigger: 'sweep' | 'manual' | 'backfill',\n externalId: string,\n accessTier: 'A' | 'B' | 'C'\n}\n```\n\n---\n\n## Related Modules\n\n- **HarakatOffices_Frontend** — React admin interface\n- **LangWatch** — Self-hosted agent tracing\n- **Odoo ERP** — External ERP system\n- **Google Drive API** — Third-party integration\n\n---\n\n## Version History\n\n| Version | Date | Changes |\n|---------|------|---------|\n| 1.0.0 | 2026-07 | Initial documentation |\n| 1.1.0 | 2026-06 | Added archive workaround docs |\n| 1.2.0 | 2026-05 | Added governance features |\n\n---\n\n## Support\n\nFor issues, refer to:\n- `docs/2FA_TESTING_GUIDE.md` — Detailed 2FA testing\n- `QUICK_START_2FA.md` — Quick start guide\n- `src/scripts/` — Self-test scripts for specific features","harakatoffices-server-prisma":"# HarakatOffices_Server — prisma\n\n\n\n# HarakatOffices_Server — prisma (Manual Migrations)\n\n## Overview\n\nThis module contains **gated, manual SQL migration files** stored in `prisma/manual/`. Unlike standard Prisma migrations in `prisma/migrations/`, these files are **never applied automatically** during deployment. They follow a strict \"dump-first\" protocol (tenet #5) to ensure data safety before schema changes are applied to production databases.\n\nThese migrations support major platform features including:\n- Delegation token management\n- ERP integrations (multiple phases)\n- Governance and policy enforcement\n- Evidence Retrieval Interface (ERI)\n- Vendor coordination\n- Employee lifecycle management\n- Conversation lifecycle tracking\n- And more\n\n## Architecture & Design Principles\n\n### Gated Migration Pattern\n\nAll files in this directory follow a consistent pattern:\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│ MANUAL MIGRATION │\n├─────────────────────────────────────────────────────────────┤\n│ 1. LIVES OUTSIDE prisma/migrations/ │\n│ 2. NEVER auto-applied by deploy │\n│ 3. PURELY ADDITIVE (CREATE TABLE, ADD COLUMN) │\n│ 4. IDEMPOTENT (IF NOT EXISTS guards) │\n│ 5. DUMP-FIRST REQUIRED BEFORE APPLY │\n└─────────────────────────────────────────────────────────────┘\n```\n\n### Tenet #5: Dump-First Protocol\n\nEvery manual migration requires this sequence:\n\n```mermaid\nflowchart TD\n A[Developer] --> B{Feature Ready?}\n B -->|Yes| C[pg_dump DB]\n C --> D[Apply SQL via prisma db execute]\n D --> E[prisma migrate resolve]\n E --> F[Deploy Server]\n F --> G[Feature Live]\n \n B -->|No| H[Wait for Activation]\n H --> B\n```\n\n### Safety Guarantees\n\n| Property | Description |\n|----------|-------------|\n| **Additive Only** | No existing data is modified or deleted (except office purge) |\n| **Idempotent** | Safe to re-run; `IF NOT EXISTS` guards prevent errors |\n| **Reversible** | Each file documents rollback commands |\n| **Graceful Degradation** | Missing tables cause 503s, not 500s, until activated |\n\n## Manual Migration Protocol\n\n### Standard Apply Sequence\n\n```bash\n# 1. Backup the database\npg_dump \"$DATABASE_URL\" -Fc > prisma/manual/backups/pre_<feature>_<timestamp>.dump\n\n# 2. Apply the migration\nnpx prisma db execute --file prisma/manual/<filename>.sql --schema prisma/schema.prisma\n\n# 3. Mark as applied\nnpx prisma migrate resolve --applied <filename>\n\n# 4. Deploy server with updated Prisma client\npnpm db:generate\n```\n\n### Example: Delegation Token Migration\n\n```bash\n# Backup\npg_dump harakatoffices_db > prisma/manual/backups/pre_delegation_token_$(date +%s).dump\n\n# Apply\nnpx prisma db execute --file prisma/manual/20260616_delegation_token.sql --schema prisma/schema.prisma\n\n# Resolve\nnpx prisma migrate resolve --applied 20260616_delegation_token\n```\n\n## Key Feature Categories\n\n### 1. Delegation & Token Management\n\n**Files:**\n- `20260616_delegation_token.sql`\n- `20260724_vendors_v1.sql`\n\n**Tables:**\n- `DelegationToken` - Token lifecycle with scope, expiration, and revocation\n- `GovernanceAuthoringGrant` - Scoped, use-bounded grants for governance\n- `PortalApplicantToken` - External portal applicant links\n\n**Key Fields:**\n```sql\n-- DelegationToken\n\"tokenHash\" TEXT NOT NULL -- SHA-256 of the token\n\"scope\" JSONB NOT NULL -- Token permissions\n\"expiresAt\" TIMESTAMP(3) NOT NULL -- Expiration time\n\"revokedAt\" TIMESTAMP(3) -- Revocation time\n\"parentTokenId\" INTEGER -- Hierarchical token support\n```\n\n### 2. ERP Integration Stack\n\n**Files:**\n- `20260617_erp_user_id.sql`\n- `20260618_startup_erp_app.sql`\n- `20260619_erp_l4_customization.sql`\n- `20260619_erp_l4_phase5_quorum.sql`\n- `20260702_erp_gaps_fill.sql`\n- `20260713_company_profile.sql`\n- `20260714_erp_deployment.sql`\n- `20260713_org_erp_mirror_v2.sql`\n- `20260713_org_erp_reconciliation.sql`\n- `20260725_page_documents.sql`\n\n**Core Tables:**\n```sql\n-- StartupErpApp\n\"id\" SERIAL PRIMARY KEY\n\"startupId\" INTEGER NOT NULL\n\"appId\" TEXT NOT NULL\n\"enabled\" BOOLEAN NOT NULL DEFAULT true\n\"removedAt\" TIMESTAMP(3) -- Soft delete tracking\n\"purgeAt\" TIMESTAMP(3) -- Hard delete scheduling\n\n-- ErpDeployment\n\"id\" TEXT PRIMARY KEY\n\"startupId\" INTEGER NOT NULL\n\"mode\" TEXT NOT NULL\n\"status\" TEXT NOT NULL DEFAULT 'PROVISIONING'\n\"baseUrl\" TEXT NOT NULL\n\"database\" TEXT NOT NULL\n```\n\n### 3. Evidence Retrieval Interface (ERI)\n\n**Files:**\n- `20260630_conversation_lifecycle.sql`\n- `20260630_eri_evidence_bundle.sql`\n- `20260630_eri_run_taskstate.sql`\n- `20260702_eri_p7_p10.sql`\n- `20260702_eri_tool_gateway.sql`\n\n**Core Tables:**\n```sql\n-- EriRun\n\"id\" TEXT NOT NULL\n\"startupId\" INTEGER NOT NULL\n\"mode\" TEXT NOT NULL\n\"status\" TEXT NOT NULL DEFAULT 'running'\n\"startedAt\" TIMESTAMP(3) NOT NULL\n\"completedAt\" TIMESTAMP(3)\n\n-- EvidenceBundle\n\"id\" SERIAL NOT NULL\n\"runId\" TEXT NOT NULL\n\"tier\" TEXT NOT NULL\n\"sufficiency\" TEXT NOT NULL DEFAULT 'sufficient'\n\"selectedCount\" INTEGER NOT NULL DEFAULT 0\n\"citations\" JSONB\n\n-- ToolExecution\n\"id\" SERIAL NOT NULL\n\"runId\" TEXT\n\"toolKey\" TEXT NOT NULL\n\"status\" TEXT NOT NULL DEFAULT 'authorized'\n\"riskLevel\" TEXT NOT NULL\n\"gatewayMode\" TEXT NOT NULL DEFAULT 'off'\n```\n\n### 4. Governance & Policy\n\n**Files:**\n- `20260629_governance_authoring_request.sql`\n- `20260629_user_assistant_procedure.sql`\n- `20260710_policy_guardrail.sql`\n- `20260713_governance_authoring_grant.sql`\n- `20260713_governance_creation_session.sql`\n- `20260714_skill_scope_model.sql`\n\n**Core Tables:**\n```sql\n-- GovernanceAuthoringRequest\n\"id\" SERIAL NOT NULL\n\"requesterUserId\" INTEGER NOT NULL\n\"routedToUserId\" INTEGER NOT NULL\n\"kind\" TEXT NOT NULL\n\"status\" TEXT NOT NULL DEFAULT 'pending'\n\"justification\" TEXT NOT NULL\n\n-- PolicyVerdict\n\"id\" SERIAL NOT NULL\n\"startupId\" INTEGER NOT NULL\n\"policyId\" INTEGER\n\"verdict\" TEXT NOT NULL\n\"reasonText\" TEXT\n\"latencyMs\" INTEGER NOT NULL DEFAULT 0\n```\n\n### 5. Employee Lifecycle Management\n\n**Files:**\n- `20260713_employee_offboarding.sql`\n- `20260713_employee_purge.sql`\n\n**Core Tables:**\n```sql\n-- EmployeeOffboardingCase\n\"id\" TEXT NOT NULL\n\"status\" TEXT NOT NULL\n\"effectiveAt\" TIMESTAMP(3) NOT NULL\n\"purgeAfter\" TIMESTAMP(3)\n\"legalHoldAt\" TIMESTAMP(3)\n\"recoveryCount\" INTEGER NOT NULL DEFAULT 0\n\n-- FormerActor\n\"id\" TEXT NOT NULL\n\"startupId\" INTEGER NOT NULL\n\"sourceStartupUserId\" INTEGER NOT NULL\n\"purgedAt\" TIMESTAMP(3) NOT NULL\n```\n\n### 6. Vendor Coordination\n\n**Files:**\n- `20260724_vendors_v1.sql`\n\n**Core Tables:**\n```sql\n-- CoordinationProject\n\"id\" SERIAL NOT NULL\n\"coordinatorStartupId\" INTEGER NOT NULL\n\"status\" \"CoordinationProjectStatus\" NOT NULL DEFAULT 'ACTIVE'\n\n-- CoordinationMember\n\"id\" SERIAL NOT NULL\n\"projectId\" INTEGER NOT NULL\n\"startupId\" INTEGER NOT NULL\n\"role\" \"CoordinationMemberRole\" NOT NULL DEFAULT 'VENDOR'\n\"linkTokenId\" INTEGER\n\n-- CoordinationAsk\n\"id\" SERIAL NOT NULL\n\"projectId\" INTEGER NOT NULL\n\"targetMemberId\" INTEGER NOT NULL\n\"status\" \"CoordinationAskStatus\" NOT NULL DEFAULT 'PENDING'\n```\n\n### 7. Conversation & Board Features\n\n**Files:**\n- `20260621_agent_board.sql`\n- `20260623_shell_board_voice.sql`\n- `20260710_board_v2.sql`\n- `20260701_conversation_checkpoint.sql`\n- `20260630_conversation_lifecycle.sql`\n\n**Core Tables:**\n```sql\n-- BoardItem\n\"id\" SERIAL PRIMARY KEY\n\"startupId\" INTEGER NOT NULL\n\"userId\" INTEGER NOT NULL\n\"kind\" TEXT NOT NULL\n\"x\" INTEGER\n\"y\" INTEGER\n\"pinned\" BOOLEAN NOT NULL DEFAULT false\n\n-- ConversationCheckpoint\n\"id\" SERIAL PRIMARY KEY\n\"conversationId\" INTEGER NOT NULL\n\"weekKey\" TEXT NOT NULL\n\"dayKey\" TEXT NOT NULL\n\"content\" TEXT NOT NULL\n\"sectionTree\" JSONB\n```\n\n### 8. Destructive Migration: Office Tier Purge\n\n**File:** `20260725_office_purge_drop.sql`\n\n**Warning:** This is the **only destructive migration** in the module. It removes the entire Office tier:\n\n```sql\n-- Dropped Tables (27 total)\nDROP TABLE \"Office\", \"OfficeUser\", \"OfficeUserSession\", \"StartupManager\",\n \"AgentAccessRule\", \"AgentExecution\", \"OfficeAgent\", \"OfficeService\",\n \"OfficeProviderService\", \"CreditTransaction\", \"OfficeRequest\",\n \"UsageRecord\", \"AdminOfficeAccess\", \"StartupDocumentRequest\",\n \"AIActionPlan\", \"DocumentReview\", \"WorkspaceUsage\", \"DocumentFile\",\n \"CourseEnrollment\", \"AIOutput\", \"AiTask\", \"AiTaskEvent\", \"CreditHold\",\n \"KnowledgeIndex\", \"OfficeCapabilityAccess\", \"PricingEstimate\",\n \"WorkspaceActivity\", \"Scenario\", \"ScenarioMessage\", \"ScheduledAgentJob\",\n \"OfficeTemplateAccess\";\n\n-- Dropped Enums (8 total)\nDROP TYPE \"OfficeStatus\", \"OfficeUserRole\", \"PricingMode\", \"CreditDirection\",\n \"UsageStatus\", \"DocumentRequestStatus\", \"ActionPlanStatus\", \"WorkspaceStatus\";\n```\n\n**Apply Order:** Server code must be deployed **BEFORE** running this migration.\n\n## Database Schema Evolution\n\n### Core Entity Relationships\n\n```mermaid\nerDiagram\n Startup ||--o{ DelegationToken : creates\n Startup ||--o{ StartupUser : has\n StartupUser ||--o{ UserAssistant : owns\n StartupUser ||--o{ Conversation : participates\n StartupUser ||--o{ GovernanceAuthoringRequest : requests\n StartupUser ||--o{ CoordinationMember : joins\n \n DelegationToken ||--o{ DelegationToken : parent\n DelegationToken ||--o{ GovernanceAuthoringGrant : grants\n \n EriRun ||--o{ TaskState : tracks\n EriRun ||--o{ ToolExecution : executes\n EriRun ||--o{ EvidenceBundle : collects\n \n CoordinationProject ||--o{ CoordinationMember : has\n CoordinationProject ||--o{ CoordinationAsk : receives\n CoordinationProject ||--o{ CoordinationMilestone : tracks\n```\n\n### Index Strategy\n\nAll migrations follow consistent indexing patterns:\n\n```sql\n-- Primary lookup indexes\nCREATE INDEX \"Table_startupId_idx\" ON \"Table\"(\"startupId\");\nCREATE INDEX \"Table_startupId_status_idx\" ON \"Table\"(\"startupId\", \"status\");\n\n-- Unique constraint indexes\nCREATE UNIQUE INDEX \"Table_startupId_key\" ON \"Table\"(\"startupId\");\nCREATE UNIQUE INDEX \"Table_startupId_field_key\" ON \"Table\"(\"startupId\", \"field\");\n\n-- Foreign key indexes\nCREATE INDEX \"Table_startupId_fkey_idx\" ON \"Table\"(\"startupId\");\n```\n\n## Integration Points\n\n### Prisma Client Generation\n\nAfter applying migrations:\n\n```bash\npnpm db:generate # Regenerates Prisma client with new schema\n```\n\n### Server Activation Flags\n\nSome features require environment flags:\n\n```bash\n# Company Profile\nCOMPANY_PROFILE_ENABLED=on\n\n# Vendors Platform\nVENDORS_PLATFORM=on\n\n# Personal Governance\nPERSONAL_GOVERNANCE=on\n```\n\n### Graceful Degradation\n\nWhen a table doesn't exist:\n\n| Scenario | Behavior |\n|----------|----------|\n| Missing `DelegationToken` | Delegation endpoints 503 |\n| Missing `ErpDeployment` | ERP resolver uses legacy shared ERP |\n| Missing `PolicyVerdict` | Policy enforcement defaults to SOFT |\n| Missing `ConversationCheckpoint` | Checkpoint features disabled |\n\n## Common Patterns\n\n### 1. Idempotent Table Creation\n\n```sql\nCREATE TABLE IF NOT EXISTS \"TableName\" (\n \"id\" SERIAL PRIMARY KEY,\n \"startupId\" INTEGER NOT NULL,\n \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Idempotent FK (skip if exists)\nDO $\nBEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_constraint WHERE conname = 'TableName_startupId_fkey'\n ) THEN\n ALTER TABLE \"TableName\" \n ADD CONSTRAINT \"TableName_startupId_fkey\" \n FOREIGN KEY (\"startupId\") \n REFERENCES \"Startup\"(\"id\") \n ON DELETE CASCADE ON UPDATE CASCADE;\n END IF;\nEND $;\n```\n\n### 2. Enum Type Creation\n\n```sql\nDO $ BEGIN\n CREATE TYPE \"EnumName\" AS ENUM ('VALUE1', 'VALUE2');\nEXCEPTION WHEN duplicate_object THEN NULL; END $;\n\n-- Add value to existing enum\nALTER TYPE \"EnumName\" ADD VALUE IF NOT EXISTS 'NEW_VALUE';\n```\n\n### 3. Backfill with UPDATE\n\n```sql\n-- Backfill existing rows\nUPDATE \"Table\" SET \"field\" = 'default' WHERE \"field\" IS NULL;\n\n-- Conditional backfill\nUPDATE \"Table\" SET \"field\" = 'value' \nWHERE \"startupId\" IS NOT NULL AND \"field\" IS NULL;\n```\n\n### 4. CHECK Constraints (NOT VALID)\n\n```sql\n-- Add constraint without validating existing rows\nALTER TABLE \"Table\"\n ADD CONSTRAINT \"Table_check_name\"\n CHECK (\"name\" IS NOT NULL AND \"name\" ~ '^[a-zA-Z0-9]+$') \n NOT VALID;\n\n-- Validate later after data is corrected\nALTER TABLE \"Table\" VALIDATE CONSTRAINT \"Table_check_name\";\n```\n\n## Rollback Procedures\n\n### Standard Rollback\n\n```bash\n# 1. Restore from backup\npg_restore -d harakatoffices_db prisma/manual/backups/pre_<feature>.dump\n\n# 2. Revert migration\nnpx prisma migrate resolve --unapplied <filename>\n\n# 3. Re-deploy server\npnpm db:generate\n```\n\n### Table-Specific Rollback Commands\n\nEach migration file documents its own rollback:\n\n```sql\n-- Example: Delegation Token\nDROP TABLE \"DelegationToken\";\n\n-- Example: Policy Project Binding\nALTER TABLE \"Policy\" DROP COLUMN \"projectId\";\n```\n\n## Safety Checklist\n\nBefore applying any manual migration:\n\n- [ ] Full database backup created\n- [ ] Migration file reviewed for destructive operations\n- [ ] Prisma client regenerated after apply\n- [ ] Server deployment plan confirmed\n- [ ] Rollback procedure documented\n- [ ] Feature activation flags identified\n- [ ] No existing data will be modified\n\n## Related Documentation\n\n- `docs/architecture/p1_delegation_tokens_design.md`\n- `docs/architecture/p2_hybrid_embeddings_build.md`\n- `.sdd-work/office-purge/PLAN-APPROVED.md`\n- `docs/superpowers/specs/2026-07-02-erp-gaps-fill-program-design.md`\n\n## Maintenance Notes\n\n1. **Never run `prisma migrate dev`** for manual migrations\n2. **Always use `prisma db execute`** for applying SQL files\n3. **Keep backups in `prisma/manual/backups/`** with timestamps\n4. **Document rollback commands** in each migration file\n5. **Test migrations in staging** before production apply\n\n---\n\n*This module is intentionally outside the standard Prisma migration workflow to enable controlled, manual activation of major platform features while maintaining data safety through the dump-first protocol.*","harakatoffices-server-scripts":"# HarakatOffices_Server — scripts\n\n\n\n# HarakatOffices_Server/scripts\n\nThis module contains operational scripts, integration tests, and data maintenance utilities. These scripts are not part of the production API surface but are essential for development validation, database backfills, and manual operational tasks. They rely heavily on the Prisma client and core service modules (e.g., `weekly-rollup.service`, `chat-protocol`, `tool-registry`).\n\n## Categories\n\nScripts are organized into four functional categories:\n\n1. **Protocol & Access Validation:** Unit tests for core protocol helpers and access control logic.\n2. **Board & Weekly Rollup Backfills:** One-time data migrations to enforce weekly thread structures.\n3. **Operational & Testing Utilities:** Tools for forcing job queues, seeding data, and running integration tests.\n4. **Infrastructure & Schema Testing:** Bash scripts for validating database constraints against throwaway schemas.\n\n## Protocol & Access Validation\n\nThese scripts verify that internal protocol helpers and access control logic behave as expected without external dependencies.\n\n### `b3_create_sentinel_check.ts`\nValidates the server-side wiring for the `__CREATE__` protocol sentinel. It ensures that creation kickoffs are parsed correctly and do not appear as visible chat bubbles.\n\n* **Key Logic:**\n * Calls `isProtocolSentinel` to verify the `__CREATE__` prefix is recognized.\n * Calls `parseCreationSentinel` to extract `kind` and `scopeHint`.\n * Verifies normalization (e.g., `\"SKILL\"` → `\"skill\"`).\n * Ensures malformed JSON or unknown kinds return `null`.\n* **Dependencies:** `modules/startup/chat-protocol`.\n* **Usage:**\n ```bash\n cd HarakatOffices_Server && npx tsx scripts/b3_create_sentinel_check.ts\n ```\n\n### `test-mcp-access.ts`\nTests the MCP access control logic, specifically tier requirements and startup visibility.\n\n* **Key Logic:**\n * Uses `deriveAccessKey` to generate mock claims.\n * Asserts `canUseMinTier` behavior based on user level.\n * Verifies `explainMcpInvocationRejection` returns correct error messages for tier, startup, and agent mismatches.\n* **Dependencies:** `core/access-key`, `modules/tool-registry/access`.\n* **Usage:**\n ```bash\n npx tsx scripts/test-mcp-access.ts\n ```\n\n### `verify-url-normalizer.ts`\nA manual verification runner for the `normalizeMeetingUrl` function. It executes `__verifyNormalizer` to ensure invariants hold.\n\n* **Dependencies:** `modules/meeting/url-normalizer`.\n* **Usage:**\n ```bash\n npx tsx scripts/verify-url-normalizer.ts\n ```\n\n## Board & Weekly Rollup Backfills\n\nThese scripts perform one-time data migrations to enforce the \"One Assistant Thread Per User Per Week\" rule (R4-1) and standardize naming. They operate within database transactions to ensure atomicity.\n\n### `merge-weekly-threads.ts`\nEnforces a single durable assistant thread per user per ISO week. It groups conversations by `(startupId, startupUserId, weekKey)` and merges sources into a target.\n\n* **Key Logic:**\n * **Plan Phase:** `buildPlans` iterates over all startups and conversations. It groups conversations by week and identifies a target (preferably the one already tagged with `weekKey`).\n * **Apply Phase:** `applyPlan` executes a transaction that:\n 1. Updates `conversationMessage`, `assistantTask`, `conversationCheckpoint`, `scenario`, `usageEvent`, `agentCreditLog`, `assistantFeedback`, `eriRun`, and `actionPreview` to point to the target conversation ID.\n 2. Updates `conversationLifecycleEvent` to link `previousConversationId` to the target.\n 3. Deletes source conversations.\n 4. Updates the target with `weekKey` and `isActiveWeek`.\n * **Safety:** Defaults to dry-run (`--apply` required to mutate). Verifies message count is lossless.\n* **Dependencies:** `modules/board/weekly-rollup.service` (for `currentWeekKey`).\n* **Usage:**\n ```bash\n # Dry-run\n npx tsx scripts/merge-weekly-threads.ts\n # Apply to specific startup\n npx tsx scripts/merge-weekly-threads.ts --startup 19 --apply\n ```\n\n### `merge-weekly-conversations.ts`\nA backfill script to merge existing conversations into per-ISO-week rolling threads. It is less granular than `merge-weekly-threads.ts` and focuses on the `conversation` table directly.\n\n* **Key Logic:**\n * Groups conversations by ISO week of `createdAt` (tenant timezone).\n * Keeps the earliest conversation in each week as the target.\n * Moves messages and deletes source conversations.\n * Nulls out legacy `voice.local` audio URLs.\n* **Dependencies:** `modules/board/weekly-rollup.service`.\n* **Usage:**\n ```bash\n # Requires environment variables: MERGE_STARTUP_ID, MERGE_USER_ID, MERGE_TZ\n npx tsx scripts/merge-weekly-conversations.ts\n ```\n\n### `retitle-weekly-threads.ts`\nStandardizes the `title` field for weekly threads. Replaces raw keys or first-message slices with human-readable names (e.g., \"Week of Jul 6\").\n\n* **Key Logic:**\n * Filters conversations where `weekKey` is set and `agentId` is 1.\n * Calls `weeklyThreadTitle` to generate the new name.\n * Updates the `title` field.\n* **Dependencies:** `modules/board/weekly-rollup.service`.\n* **Usage:**\n ```bash\n # Dry-run\n npx tsx scripts/retitle-weekly-threads.ts\n # Apply\n npx tsx scripts/retitle-weekly-threads.ts --apply\n ```\n\n## Operational & Testing Utilities\n\n### `board-v2-selftest.ts`\nA comprehensive integration test suite for the Board v2 features. It creates a throwaway startup, runs controller tests, and cleans up.\n\n* **Key Logic:**\n * **Fixture:** `createFixture` creates a startup, users, agent, and conversation.\n * **Cleanup:** `cleanup` deletes the startup and agent in the `finally` block.\n * **Test Cases:**\n * Verifies `BOARD_V2` flag off behavior (returns disabled envelopes).\n * Tests `updateConversationWeekNotesController` roundtrip.\n * Tests `editConversationMessageController` (user message edit truncates downstream).\n * Tests permission denial for non-owners.\n * Tests `getOrCreateActiveWeeklyConversation` idempotency.\n* **Dependencies:** `src/core/config/prisma`, `src/modules/startup/startup.controller`, `src/modules/board/weekly-rollup.service`.\n* **Usage:**\n ```bash\n npx tsx scripts/board-v2-selftest.ts\n ```\n\n### `force-transcript-batch.ts`\nManually enqueues a `transcript_batch` job in the `ExecutionQueue` table. Bypasses the planner cron, useful for testing the worker or when the cron is not registered.\n\n* **Key Logic:**\n * Validates meetings exist and belong to a single startup.\n * Checks `transcriptStatus` is null (not already processed).\n * Sets `transcriptStatus` to `pending` and assigns a `transcriptBatchId`.\n * Mints callback tokens via `mintMeetingCallbackToken`.\n * Creates the `ExecutionQueue` row with `scheduledAt = now`.\n* **Dependencies:** `src/middleware/meeting-callback-auth`, `src/core/config/env`.\n* **Usage:**\n ```bash\n npx tsx scripts/force-transcript-batch.ts <meetingId> [<meetingId> ...]\n ```\n\n### `seed-tool-registry.ts`\nSyncs the `ToolRegistration` table from a JSON manifest.\n\n* **Key Logic:**\n * Reads a JSON file (e.g., `Harakat_AI/tool_manifest.json`).\n * Calls `bulkUpsertCatalog` to upsert tools.\n * Idempotent: re-running updates existing entries.\n* **Dependencies:** `modules/tool-registry/tool-registry.service`.\n* **Usage:**\n ```bash\n npx ts-node scripts/seed-tool-registry.ts ../Harakat_AI/tool_manifest.json\n ```\n\n## Infrastructure & Schema Testing\n\n### `procedure-assignee-live.sh`\nA Bash script that creates a throwaway database schema (structure only, no data) from a live source database to test constraints and triggers.\n\n* **Key Logic:**\n * Reads `DATABASE_URL` from `.env.local` or `.env`.\n * Uses `pg_dump --schema-only` to extract schema.\n * Creates a new database `procedure_assignee_sdd_test`.\n * Runs `procedure-assignee-live-test.ts` against this schema.\n * Drops the test database on exit.\n* **Purpose:** Ensures that tests pass against a schema with real constraints (CHECK, triggers) rather than a `db-push` schema which lacks them.\n* **Usage:**\n ```bash\n bash scripts/procedure-assignee-live.sh\n ```\n\n## Architecture & Execution Flow\n\nScripts in this module primarily interact with the database via the Prisma client (`prisma`). They are designed to be idempotent where possible (e.g., merge scripts check for existing tags) and safe (e.g., `board-v2-selftest.ts` cleans up fixtures).\n\nThe following diagram illustrates the execution flow of the most complex backfill script, `merge-weekly-threads.ts`.\n\n```mermaid\nflowchart TD\n Start[main] --> Build[buildPlans]\n Build --> Check{Plans > 0?}\n Check -- No --> Exit[Exit]\n Check -- Yes --> Apply[applyPlan]\n Apply --> Loop{More Plans?}\n Loop -- Yes --> Apply\n Loop -- No --> Verify[Verify Message Count]\n Verify --> Exit\n Build --> Group[Group by User/Week]\n Group --> Select[Select Target]\n Select --> Update[Update Tables]\n Update --> Delete[Delete Sources]\n Update --> SetTags[Set weekKey/isActiveWeek]\n```\n\n## Safety & Best Practices\n\n1. **Transactions:** Scripts that modify data (e.g., `merge-weekly-threads.ts`, `merge-weekly-conversations.ts`) wrap changes in `prisma.$transaction` to ensure atomicity.\n2. **Dry-Run Defaults:** Backfill scripts default to dry-run mode (`--apply` required) to prevent accidental data loss.\n3. **Cleanup:** Integration tests (`board-v2-selftest.ts`) explicitly clean up created resources in a `finally` block.\n4. **Environment Variables:** Scripts rely on specific env vars (e.g., `BOARD_V2`, `MERGE_STARTUP_ID`). Ensure these are set before execution.\n5. **Schema Consistency:** `procedure-assignee-live.sh` ensures tests run against a schema with constraints, preventing false positives from `db-push` environments.","harakatoffices-server-src":"# HarakatOffices_Server — src\n\n\n\n# HarakatOffices_Server — src Module\n\n## Overview\n\nThis module is the core application server for the Harakat platform. It provides:\n\n1. **HTTP API Gateway** — Express-based routing for all platform endpoints\n2. **Access Control System** — Multi-layer authorization combining tier-based filtering, OpenFGA node-graph checks, and workspace membership\n3. **Session Management** — Admin session validation with revocation support\n4. **Middleware Infrastructure** — Drain admission, CORS, helmet, request timing, and capability guards\n\nThe server is designed with **fail-closed security** as a core principle: any authorization check that cannot resolve must deny access rather than leak it.\n\n---\n\n## Architecture\n\n```mermaid\ngraph TD\n A[HTTP Request] --> B[Middleware Chain]\n B --> C{Route Match}\n C -->|Matched| D[Handler]\n C -->|No Match| E[404]\n \n B --> M1[Helmet]\n B --> M2[CORS]\n B --> M3[JSON Parser]\n B --> M4[Request Timing]\n B --> M5[Drain Admission]\n B --> M6[Agent Capability Guard]\n B --> M7[Startup Auth Barrier]\n \n M6 --> G1{AGENT_CAPABILITY_ENFORCEMENT}\n G1 -->|On| M7\n G1 -->|Off| D\n \n M7 --> G2{orgAuthorityEditsEnabled}\n G2 -->|On| D\n G2 -->|Off| D\n \n D --> H1[Access Control]\n H1 --> H2[Tier/Branch Filter]\n H1 --> H3[OpenFGA Check]\n H1 --> H4[Workspace Membership]\n \n H2 --> H5[canSee]\n H3 --> H6[OpenFGA Client]\n H4 --> H7[Workspace Access]\n \n H5 --> H8[AccessClaims]\n H6 --> H9[Node Graph]\n H7 --> H10[Membership]\n \n H8 --> D\n H9 --> D\n H10 --> D\n```\n\n---\n\n## Core Components\n\n### 1. Application Entry Point (`app.ts`)\n\nThe main Express application that registers all routes and middleware.\n\n#### Middleware Order (Critical)\n\n```typescript\n// 1. Security headers\napp.use(helmet());\n\n// 2. CORS configuration\napp.use(cors({...}));\n\n// 3. JSON parsing with raw body capture for mail webhooks\napp.use(express.json({\n limit: \"50mb\",\n verify: (req, _res, buf) => {\n if (req.url?.startsWith(\"/api/internal/mail/\")) \n (req as any).rawBody = Buffer.from(buf);\n }\n}));\n\n// 4. Request timing instrumentation\napp.use(requestTiming);\n\n// 5. Agent capability guard (P1)\napp.use(agentCapabilityGuard());\n\n// 6. Drain admission (mounted ABOVE health)\napp.use(drainAdmission);\n\n// 7. Health routes (FIRST, before auth)\napp.use(\"/api/health\", healthRoutes);\n```\n\n#### Route Registration Patterns\n\nRoutes are mounted in **specificity order** — more specific paths must come before generic ones:\n\n```typescript\n// ✅ Correct: specific before generic\napp.use(\"/api/startup/meetings/recording\", meetingRecordingRoutes);\napp.use(\"/api/startup/meetings\", meetingRoutes);\n\n// ❌ Wrong: generic would match first\napp.use(\"/api/startup/meetings\", meetingRoutes);\napp.use(\"/api/startup/meetings/recording\", meetingRecordingRoutes);\n```\n\n#### Conditional Route Mounting\n\nSome routes are gated by environment flags:\n\n```typescript\n// ERP per-employee mode\nconst erpAuth: RequestHandler[] =\n process.env.ERP_PER_EMPLOYEE === \"on\" \n ? [verifyStartupUser, agentCapabilityGuard()] \n : [];\napp.use(\"/erp\", ...erpAuth, erpRoutes);\n\n// Agent board (flag-gated)\nif (process.env.AGENT_BOARD_ENABLED === \"on\") {\n app.use(\"/api/startup/agent-board\", agentBoardRoutes);\n}\n\n// Company profile (router-level 503)\napp.use(\"/api/startup/company-profile\", companyProfileRoutes);\n```\n\n---\n\n### 2. Access Control System (`core/access-key/`)\n\nThe access control system is the heart of authorization. It uses **three complementary layers**:\n\n#### Layer 1: Tier-Based Filtering (`canSee.ts`)\n\nEvery tierable item (documents, knowledge nodes, etc.) has an `accessTier` string:\n- `\"A\"` = most restrictive (super-admin only)\n- `\"B\"-\"Z\"` = progressively less restrictive\n- `\"B-marketing\"` = branch-scoped (branch \"marketing\")\n\n```typescript\n// Tier semantics (post-polarity-flip)\n// CALLER side: 'A' = most privileged, 'Z' = least privileged\n// ITEM side: 'A' = most restrictive, 'Z' = readable by everyone\n\nexport function canSee(claims: AccessClaims, item: TierableItem): boolean {\n // 1. Per-assistant branch allowlist (narrowing)\n if (hasBranchRestriction(claims.customOverrides)) {\n // Check branchIds or branchSlugs\n }\n \n // 2. Super-admin short-circuit\n if (claims.isSuperAdmin) return true;\n \n // 3. Tier rank comparison\n const myLevel = tierLevel(claims.tier);\n const itemLevel = itemTierLevel(item.accessTier);\n if (itemLevel < myLevel) return false; // item is more restrictive\n \n // 4. Branch check (explicit branchId or tier suffix)\n if (item.branchId != null && item.branchId !== claims.branchId) {\n return false;\n }\n \n return true;\n}\n```\n\n**Key invariant**: `canSee` is **fail-closed** — malformed tiers default to most restrictive.\n\n#### Layer 2: Per-Assistant Claims (`assistant-claims.ts`)\n\nAssistants inherit their employee's access **at read time**, not at creation:\n\n```typescript\nexport async function lookupAssistantClaims(\n startupId: number,\n assistantId: number,\n source: AssistantAccessSource = prismaAssistantAccessSource\n): Promise<AssistantClaimsLookup> {\n // 1. Load assistant row (with gated columns)\n const row = await source.loadAssistant(startupId, assistantId);\n \n // 2. Validate assistant is active\n if (row.isActive === false) return deny(\"assistant_inactive\");\n \n // 3. Validate owner is active (revoking human revokes assistant)\n if (!row.owner.isActive) return deny(\"owner_inactive\");\n \n // 4. Parse and apply overrides (narrow-only)\n const { overrides, rejected } = parseAccessOverrides(row.customAccessOverrides);\n \n // 5. Derive claims from live employee data\n const claims = deriveAccessKey({\n userId: row.owner.id,\n startupId,\n role: row.owner.role,\n level: row.owner.level,\n branchId: row.owner.branchId,\n branchName: row.owner.branchName,\n customOverrides: overrides,\n });\n \n return { claims, failed: false, reason: \"ok\", ... };\n}\n```\n\n**Why this matters**: Previously, `UserAssistant.accessTier` was copied at creation and never updated. Promoting an employee left their assistant with stale, overly-permissive access. This resolves claims **every request** from live employee data.\n\n#### Layer 3: OpenFGA Node-Graph (`openfga/`)\n\nP1 introduces OpenFGA for **ancestry-based governance**:\n\n```typescript\n// model.fga defines:\n// - startup_user.manager: direct manager (parentId)\n// - startup_user.governor: transitive manager chain\n// - resource.viewer: owner OR governor of owner\n\nexport async function canGovern(actorUserId: number, targetUserId: number): Promise<boolean> {\n return openFga().check(fgaUser(actorUserId), \"governor\", fgaUser(targetUserId));\n}\n\nexport async function canViewResource(userId: number, resourceId: number | string): Promise<boolean> {\n return openFga().check(fgaUser(userId), \"viewer\", fgaResource(resourceId));\n}\n```\n\n**Governance query**: An upper principal reaches a lower resource **only if** there's a real management path through the org tree, not just a smaller level number.\n\n**Seed process**: `seedStartup()` reads `StartupUser`/`StartupBranch`/`UserAssistant` from DB and writes tuples to OpenFGA. Idempotent — re-running only writes missing tuples.\n\n#### Layer 4: Workspace Membership (`workspace-access.ts`)\n\nItem 6 adds a **widening** rule: workspace membership grants access even where tier hierarchy would deny:\n\n```typescript\nexport function canSeeWorkspaceDocument(\n membership: WorkspaceMembership,\n doc: { workspaceId?: number | null },\n alreadyVisible: boolean,\n): boolean {\n return alreadyVisible || membership.grants(doc.workspaceId ?? null);\n}\n```\n\n**Composition rule**: `visible == member(workspaceId) OR <existing rule>`\n\n**Fail-closed**: Membership tables are gated — if the table doesn't exist, return empty membership.\n\n---\n\n### 3. Tier Decision Logic (`node-tier.ts`)\n\nThis is the **single source of truth** for document tier assignment. Previously, all 77,655 `KnowledgeNode` rows had `accessTier = \"A\"` because no ingest path called `deriveAccessTier`.\n\n```typescript\nexport function decideNodeTier(source: TierSource): NodeTier {\n switch (source.from) {\n case \"connected_source\":\n case \"shared_upload\":\n // Company-wide source → \"Z\" (least restrictive)\n return { tier: \"Z\", basis: \"company_source\" };\n \n case \"private_upload\":\n // Personal upload → owner's rank\n return { tier: withBranch(letterForLevel(ownerLevel), ownerBranchName), basis: \"owner_private\" };\n \n case \"parent\":\n // Child inherits parent's tier\n return { tier: source.parentTier, basis: \"inherited\" };\n \n case \"explicit\":\n // Server-decided tier (meeting artifact, ERP row)\n return { tier: source.tier, basis: \"explicit\" };\n \n default:\n return { tier: null, basis: \"undecidable\" }; // default-deny\n }\n}\n```\n\n**Basis tracking**: Every tier decision records **why** it was made (`accessTierBasis` in metadata). This is critical for auditing and debugging.\n\n---\n\n### 4. Admin Session Validation (`admin-session/decide.ts`)\n\nReplaces the broken `jwt.verify`-only auth with **revocable sessions**:\n\n```typescript\nexport function decideAdminSession(input: DecideAdminSessionInput): AdminSessionDecision {\n // 1. Token present?\n if (!input.tokenPresent) return refuse(\"no_token\");\n \n // 2. Valid claims?\n if (!input.claims) return refuse(\"token_invalid\");\n \n // 3. Has session claim?\n if (input.claims.type !== \"access\") return refuse(\"token_invalid\");\n const sid = Number(input.claims.sid);\n if (!Number.isSafeInteger(sid) || sid <= 0) return refuse(\"no_session_claim\");\n \n // 4. Session exists and is live?\n if (!input.session) return refuse(\"session_unknown\");\n if (input.session.expiresAt < input.now) return refuse(\"session_expired\");\n if (input.session.revokedAt) return refuse(\"session_revoked\");\n \n // 5. Admin still exists?\n if (!input.admin) return refuse(\"admin_missing\");\n \n return { ok: true, admin: input.admin, sessionId: sid };\n}\n```\n\n**Session causes**:\n- `session_revoked` — operator was fired, session was invalidated\n- `admin_missing` — session is live but operator was deleted\n- `unavailable` — database error (not a denial)\n\n---\n\n### 5. Tenant Owner Resolution (`tenant-owner.ts`)\n\nDetermines who the **first initialized tier-A admin** is for a tenant:\n\n```typescript\nexport async function resolveTenantOwner(startupId: number): Promise<TenantOwnerResult> {\n const owner = await prisma.startupUser.findFirst({\n where: { startupId, isActive: true, role: \"admin\", level: 0 },\n orderBy: [{ createdAt: \"asc\" }, { id: \"asc\" }], // EXPLICIT ordering\n select: { id: true },\n });\n \n if (!owner) return { userId: null, cause: \"no_tier_a_user\", resolved: true };\n return { userId: owner.id, resolved: true };\n}\n```\n\n**Why ordering matters**: Multiple tier-A admins exist. The **first created** is the owner. Without `orderBy`, \"first\" is undefined and the answer drifts.\n\n---\n\n### 6. Per-Assistant Overrides (`overrides.ts`)\n\n`UserAssistant.customAccessOverrides` is a JSON column for **narrowing-only** adjustments:\n\n```typescript\nexport interface AccessOverrides {\n tierFloor?: string; // Cap authority BELOW employee's rank\n branchIds?: readonly number[]; // Restrict branch-scoped items\n branchSlugs?: readonly string[]; // Legacy tier-suffix allowlist\n}\n```\n\n**Why \"by construction\"**: The type has **no widening form**. `tierFloor` uses `Math.max`, `branchIds` is an extra AND-clause. An override can only narrow.\n\n**Parsing**: Untrusted JSON is validated field-by-field. Unknown keys are reported, not silently ignored.\n\n---\n\n## Key Patterns\n\n### 1. Fail-Closed Security\n\nEvery authorization check defaults to deny:\n\n```typescript\n// Tier parsing\nexport function itemTierLevel(tier: string | null | undefined): number {\n if (!isValidTier(tier)) return 0; // \"A\" = most restrictive\n return (tier as string).charCodeAt(0) - 65;\n}\n\n// Null accessTier\nexport const MOST_RESTRICTIVE_TIER = \"A\";\n// canSee treats null item tier as \"A\"\n```\n\n### 2. Single Source of Truth\n\n- **Tier decision**: `node-tier.ts` (one place to decide)\n- **Tenant owner**: `tenant-owner.ts` (one function)\n- **Session check**: `admin-session/decide.ts` (one decision)\n- **Workspace membership**: `workspace-access.ts` (one predicate)\n\nTwo implementations of an authority rule = two that can disagree.\n\n### 3. Read-Time Resolution\n\nClaims are **never stored** — recomputed every request:\n\n```typescript\n// assistant-claims.ts\nexport async function resolveAssistantClaims(\n startupId: number,\n assistantId: number,\n source: AssistantAccessSource = prismaAssistantAccessSource\n): Promise<AccessClaims | null> {\n return (await lookupAssistantClaims(startupId, assistantId, source)).claims;\n}\n```\n\nPromoting an employee immediately updates their assistant's access.\n\n### 4. Gated Columns\n\nDatabase columns that may not exist degrade gracefully:\n\n```typescript\n// assistant-claims.ts\nlet _columnCache: { isActive: boolean; overrides: boolean } | null = null;\n\nasync function assistantColumns(): Promise<{ isActive: boolean; overrides: boolean }> {\n if (_columnCache) return _columnCache;\n try {\n const rows = await prisma.$queryRaw<Array<{ column_name: string }>>`\n SELECT column_name FROM information_schema.columns\n WHERE table_name = 'UserAssistant'\n AND column_name IN ('isActive', 'customAccessOverrides')`;\n // ...\n } catch {\n // Assume narrow shape (pre-column behavior)\n _columnCache = { isActive: false, overrides: false };\n }\n return _columnCache;\n}\n```\n\n### 5. Drain Admission\n\nMounted **above health** to allow probes during graceful shutdown:\n\n```typescript\n// app.ts\nimport { drainAdmission } from \"./middleware/drain\";\napp.use(drainAdmission);\n\napp.use(\"/api/health\", healthRoutes); // Health bypasses drain\n```\n\n---\n\n## Integration Points\n\n### Scripts\n\n```typescript\n// test-mcp-access.ts\nimport { deriveAccessKey } from \"core/access-key/deriver\";\nimport { isToolRegistrationVisibleToClaims } from \"modules/tool-registry/access\";\n\n// board-v2-selftest.ts\nimport { getOrCreateActiveWeeklyConversation } from \"modules/board/weekly-rollup.service\";\n```\n\n### Modules\n\n```typescript\n// modules/tool-registry/access.ts\nexport function isToolRegistrationVisibleToClaims(claims: AccessClaims, tool: Tool): boolean {\n // Uses canSee + OpenFGA checks\n}\n\n// modules/startup/assistant.service.ts\nexport async function sendMessageStream(...) {\n // Resolves assistant claims before sending\n}\n```\n\n---\n\n## Environment Variables\n\n| Variable | Purpose | Default |\n|----------|---------|---------|\n| `ALLOWED_ORIGINS` | CORS origins (comma-separated) | localhost:3000-3003,5173 |\n| `CLIENT_ORIGIN` | Additional client origin | — |\n| `AGENT_CAPABILITY_ENFORCEMENT` | Enable agent capability guard | off |\n| `ERP_PER_EMPLOYEE` | ERP per-employee mode | off |\n| `OPENFGA_ENABLED` | Enable OpenFGA | on |\n| `OPENFGA_API_URL` | OpenFGA HTTP API URL | http://127.0.0.1:8081 |\n| `OPENFGA_STORE_NAME` | OpenFGA store name | harakat |\n| `ORG_AUTHORITY_EDITS_ENABLED` | Enable org authority barrier | off |\n| `EMPLOYEE_OFFBOARDING_ENABLED` | Enable offboarding checks | off |\n| `AGENT_BOARD_ENABLED` | Enable agent board routes | off |\n\n---\n\n## Testing\n\n### Self-Tests\n\n```typescript\n// openfga-selftest.ts\nimport { openFga } from \"core/access-key/openfga/client\";\nimport { HARAKAT_AUTH_MODEL } from \"core/access-key/openfga/model\";\n\n// Validates model structure against live OpenFGA\n```\n\n### Mirror Visibility\n\n```typescript\n// mirror-visibility:selftest\n// Asserts canSee(with) ⟹ canSee(without) over the whole matrix\n```\n\n---\n\n## Common Pitfalls\n\n### 1. Route Ordering\n\n```typescript\n// ❌ Wrong: generic matches first\napp.use(\"/api/startup/meetings\", meetingRoutes);\napp.use(\"/api/startup/meetings/recording\", meetingRecordingRoutes);\n\n// ✅ Correct\napp.use(\"/api/startup/meetings/recording\", meetingRecordingRoutes);\napp.use(\"/api/startup/meetings\", meetingRoutes);\n```\n\n### 2. Tier Confusion\n\n```typescript\n// ❌ Wrong: mixing caller/item semantics\n// Caller: 'A' = most privileged\n// Item: 'A' = most restrictive\n\n// ✅ Correct: use tierLevel() for caller, itemTierLevel() for item\nconst myLevel = tierLevel(claims.tier);\nconst itemLevel = itemTierLevel(item.accessTier);\nif (itemLevel < myLevel) return false;\n```\n\n### 3. Null Handling\n\n```typescript\n// ❌ Wrong: assuming null tier means \"no tier\"\nif (item.accessTier) { /* ... */ }\n\n// ✅ Correct: null = default-deny\nif (canSee(claims, item)) { /* ... */ }\n```\n\n### 4. Workspa |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment