# Sofiia CTO Agent — Gaps & Recovery Plan (E) > Generated: 2026-02-26 | P0 = блокуюче | P1 = критичне для vNext | P2 = покращення --- ## Критичне резюме **Що вже готово і може йти в UI:** Chat, Voice, Projects CRUD, File upload, Sessions, Dialog Map tree, Ops actions, Node health. **Що не готово і блокує vNext:** Tasks/Kanban, Meetings, Dialog Map canvas + Postgres schema, Doc versions, CTO Repo/Ops flow, Supervisor через BFF, Semantic search. --- ## Таблиця прогалин з пріоритетами | # | Gap | Пріоритет | Складність | Блокує | |---|-----|-----------|-----------|--------| | G1 | `dialog_nodes`/`dialog_edges` Postgres tables + API | P0 | Medium | Dialog Map vNext | | G2 | `tasks` table + CRUD API + Kanban UI | P0 | Medium | Projects Board | | G3 | `meetings` table + CRUD API | P0 | Medium | Projects Meetings tab | | G4 | Supervisor не проксюється через BFF | P0 | Low | CTO workflow access | | G5 | `docs_versions` table + API | P1 | Low | Doc history/rollback | | G6 | `entity_links` table + API | P1 | Low | Cross-entity linking | | G7 | `repo_changesets` + `repo_patches` + PR flow | P1 | High | CTO code workflow | | G8 | `ops_runs` job system (not one-shot) | P1 | Medium | CTO ops audit trail | | G9 | Semantic search (Qdrant/Meilisearch) | P1 | Medium | Doc/Project search | | G10 | NATS `attachment.created` on upload | P1 | Low | Parser pipeline hook | | G11 | `DELETE` endpoints (projects/docs) | P1 | Low | CRUD completeness | | G12 | Real-time WS events for map/tasks | P1 | Medium | Live UI updates | | G13 | E2EE / confidential mode | P2 | Very High | Privacy | | G14 | 2-step Plan → Apply for dangerous actions | P2 | High | Safe ops flow | | G15 | `agent_id="l"` vs `"sofiia"` inconsistency | P1 | Low | Config correctness | | G16 | `dialog_views` saved views | P2 | Low | UX | | G17 | NODA3 integration | P2 | Medium | AI/ML workstation | | G18 | Meilisearch deployment | P2 | Low | Full-text search | | G19 | Privacy Gate middleware (Router) | P2 | High | Confidential mode | | G20 | Wiki Markdown editor UI | P2 | Medium | Docs/Wiki experience | | G21 | `doc_index_state` table + reindex jobs | P2 | Low | AI doc indexing | | G22 | Meeting reminders (push/WS) | P2 | Medium | Meetings UX | | G23 | `DELETE /api/nodes/{id}` | P2 | Low | Node management | | G24 | S3/MinIO для file storage | P2 | High | Scale (replace volume) | --- ## P0 — Блокуючі прогалини (потрібні для vNext) ### G1: Dialog Map — Postgres schema + API **Що зроблено:** SQLite tree via `parent_msg_id`. Works for conversation branching. **Чого не вистачає:** - Postgres tables: `dialog_nodes`, `dialog_edges`, `dialog_views` - API: `GET /api/projects/{id}/dialog-map`, `POST /api/links` - WS event: `dialog_map.updated` - Auto-edge creation from NATS events **Recovery plan:** ```sql -- Step 1: Add to sofiia-console db.py (SQLite first, Postgres later) CREATE TABLE IF NOT EXISTS dialog_nodes ( node_id TEXT PRIMARY KEY, project_id TEXT NOT NULL, node_type TEXT NOT NULL CHECK(node_type IN ('message','task','doc','meeting','agent_run','decision','goal')), ref_id TEXT NOT NULL, -- FK to actual entity title TEXT DEFAULT '', created_at TEXT NOT NULL, created_by TEXT DEFAULT 'system' ); CREATE TABLE IF NOT EXISTS dialog_edges ( edge_id TEXT PRIMARY KEY, project_id TEXT NOT NULL, from_node_id TEXT NOT NULL REFERENCES dialog_nodes(node_id), to_node_id TEXT NOT NULL REFERENCES dialog_nodes(node_id), edge_type TEXT NOT NULL CHECK(edge_type IN ('references','resolves','derives_task','updates_doc','schedules','summarizes')), created_at TEXT NOT NULL, props TEXT DEFAULT '{}' -- JSON ); CREATE TABLE IF NOT EXISTS dialog_views ( view_id TEXT PRIMARY KEY, project_id TEXT NOT NULL, name TEXT NOT NULL, filters TEXT DEFAULT '{}', layout TEXT DEFAULT '{}' ); ``` ```python # Step 2: New endpoint in docs_router.py @router.get("/api/projects/{project_id}/dialog-map") async def get_project_dialog_map(project_id: str): nodes = await db.get_dialog_nodes(project_id) edges = await db.get_dialog_edges(project_id) return {"nodes": nodes, "edges": edges} @router.post("/api/links") async def create_link(body: LinkCreate): # Creates dialog_edge between two entities ... ``` **Оцінка:** 4–6 годин роботи. --- ### G2: Tasks + Kanban **Що зроблено:** Немає. **Recovery plan:** ```sql CREATE TABLE IF NOT EXISTS tasks ( task_id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(project_id), title TEXT NOT NULL, description TEXT DEFAULT '', status TEXT DEFAULT 'backlog' CHECK(status IN ('backlog','in_progress','review','done')), priority TEXT DEFAULT 'medium', assignee_id TEXT DEFAULT '', labels TEXT DEFAULT '[]', -- JSON due_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, msg_id TEXT -- Optional: link to originating message ); ``` - API: `GET/POST /api/projects/{id}/tasks`, `PATCH /api/tasks/{id}`, `DELETE /api/tasks/{id}` - UI: Kanban board з drag-drop (можна почати з простим list + status buttons) - Dialog Map auto-edge: `POST /api/links` after task creation **Оцінка:** 1–2 дні (backend + basic UI). --- ### G3: Meetings **Recovery plan:** ```sql CREATE TABLE IF NOT EXISTS meetings ( meeting_id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(project_id), title TEXT NOT NULL, starts_at TEXT NOT NULL, duration_min INTEGER DEFAULT 60, attendees TEXT DEFAULT '[]', -- JSON location TEXT DEFAULT '', agenda TEXT DEFAULT '', created_at TEXT NOT NULL ); ``` - API: `GET/POST /api/projects/{id}/meetings`, `PATCH /api/meetings/{id}` - UI: simple form (title, date/time, duration, attendees) - Reminders: Phase 2 (WS push) **Оцінка:** 1 день. --- ### G4: Supervisor → BFF proxy **Що зроблено:** Supervisor API exists at `http://sofiia-supervisor:8080` (або port 9400). **Recovery plan:** ```python # Add to services/sofiia-console/app/main.py: SUPERVISOR_URL = os.getenv("SUPERVISOR_URL", "http://sofiia-supervisor:8080") @app.post("/api/supervisor/runs") async def run_supervisor_graph(body: dict, _auth: str = Depends(require_auth)): async with httpx.AsyncClient() as c: resp = await c.post(f"{SUPERVISOR_URL}/v1/graphs/{body['graph']}/runs", json=body, timeout=60) return resp.json() @app.get("/api/supervisor/runs/{run_id}") async def get_supervisor_run(run_id: str, _auth: str = Depends(require_auth)): async with httpx.AsyncClient() as c: resp = await c.get(f"{SUPERVISOR_URL}/v1/runs/{run_id}", timeout=10) return resp.json() ``` **Оцінка:** 30 хвилин. --- ## P1 — Критичні для vNext ### G5: Doc versions ```sql CREATE TABLE IF NOT EXISTS doc_versions ( version_id TEXT PRIMARY KEY, doc_id TEXT NOT NULL REFERENCES documents(doc_id), content TEXT NOT NULL, -- full text author_id TEXT DEFAULT 'system', created_at TEXT NOT NULL ); ``` ```python # New endpoints in docs_router.py: # GET /api/projects/{pid}/documents/{did}/versions # POST /api/projects/{pid}/documents/{did}/restore ``` **Оцінка:** 2 години. --- ### G7: Repo Changesets (CTO Code Flow) Це найскладніша частина. **Рекомендація:** почати з mock endpoints, потім реалізувати реальну логіку. **Mock endpoint (30 хв):** ```python @app.post("/api/repo/changesets") async def create_changeset_mock(body: dict, _auth=Depends(require_auth)): # Mock: store in SQLite, return changeset_id cs_id = str(uuid.uuid4()) # await db.save_changeset(cs_id, body) return {"changeset_id": cs_id, "status": "draft", "mock": True} ``` **Реальна реалізація (2–3 дні):** ```sql CREATE TABLE repo_changesets ( cs_id TEXT PRIMARY KEY, project_id TEXT, repo TEXT NOT NULL, -- e.g., "github.com/IvanTytar/microdao-daarion" base_ref TEXT NOT NULL, -- branch/commit intent TEXT NOT NULL, risk_level TEXT DEFAULT 'low', status TEXT DEFAULT 'draft', created_by TEXT, created_at TEXT NOT NULL ); CREATE TABLE repo_patches ( patch_id TEXT PRIMARY KEY, cs_id TEXT NOT NULL REFERENCES repo_changesets(cs_id), file_path TEXT NOT NULL, patch_text TEXT NOT NULL, -- unified diff created_at TEXT NOT NULL ); CREATE TABLE pull_requests ( pr_id TEXT PRIMARY KEY, cs_id TEXT NOT NULL REFERENCES repo_changesets(cs_id), provider TEXT DEFAULT 'github', -- github/gitlab/gitea pr_url TEXT, pr_number INTEGER, status TEXT DEFAULT 'draft', created_at TEXT NOT NULL ); ``` --- ### G8: Ops Runs (Job System) Поточний `/api/ops/run` — one-shot dispatch. Потрібен job tracking. ```sql CREATE TABLE ops_runs ( run_id TEXT PRIMARY KEY, project_id TEXT, node_id TEXT NOT NULL, -- noda1/noda2 action TEXT NOT NULL, -- з allowlist params TEXT DEFAULT '{}', -- JSON dry_run INTEGER DEFAULT 1, status TEXT DEFAULT 'pending', -- pending/running/success/failed result TEXT DEFAULT '', started_at TEXT, finished_at TEXT, created_by TEXT ); ``` **API:** - `POST /api/ops/runs` (створити job, dry_run=true за замовч.) - `GET /api/ops/runs/{id}` (статус) - `GET /api/ops/runs?project_id=&limit=20` (список) **Оцінка:** 4 години (backend) + 2 год (UI list). --- ### G10: NATS attachment.created Одна зміна в `docs_router.py`: ```python # After successful file save: try: import nats nc = await nats.connect(NATS_URL) await nc.publish(f"attachment.created.{mime_category}", json.dumps({"file_id": file_id, "doc_id": doc_id, ...}).encode()) await nc.close() except Exception: pass # best-effort ``` **Оцінка:** 1 година. --- ### G15: agent_id "l" vs "sofiia" У `services/router/router-config.yml` для NODA2: ```yaml # Check if there's "l:" entry that should be "sofiia:" ``` **Action:** знайти і замінити `"l"` → `"sofiia"` у router-config відповідної ноди. **Оцінка:** 15 хвилин. --- ## P2 — Покращення ### G13: E2EE (confidential mode) **Складність:** Дуже висока. Потребує: 1. Client-side key generation (WebCrypto API) 2. Server-side: store only ciphertext + key_id 3. Router Privacy Gate middleware 4. Dialog Map: тільки user-created edges (не semantic auto-edges) 5. Search: тільки metadata, не plaintext **Рекомендація:** Не реалізовувати до завершення Projects + Dialog Map. Спочатку `mode=public` тільки. --- ### G20: Wiki Markdown Editor Потрібна бібліотека (CodeMirror / Monaco / Tiptap). Для Phase 1 — textarea з preview. ```html