Files
taskpile/docs/guide/architecture.md
Alvis 9b77d6ea67 Add MLOps feature store, fix UI layout, add docs and Gitea remote
Backend:
- Replace on-the-fly Ollama calls with versioned feature store (task_features, task_edges)
- Background Tokio worker drains pending rows; write path returns immediately
- MLConfig versioning: changing model IDs triggers automatic backfill via next_stale()
- AppState with FromRef; new GET /api/ml/status observability endpoint
- Idempotent mark_pending (content hash guards), retry failed rows after 30s
- Remove tracked build artifacts (backend/target/, frontend/.next/, node_modules/)

Frontend:
- TaskItem: items-center alignment (fixes checkbox/text offset), break-words for overflow
- TaskDetailPanel: fix invisible AI context (text-gray-700→text-gray-400), show all fields
- TaskDetailPanel: pending placeholder when latent_desc not yet computed, show task ID
- GraphView: surface pending_count as amber pulsing "analyzing N tasks…" hint in legend
- Fix Task.created_at type (number/Unix seconds, not string)
- Auth gate: LoginPage + sessionStorage; fix e2e tests to bypass gate in jsdom
- Fix deleteTask test assertion and '1 remaining'→'1 left' stale text

Docs:
- VitePress docs in docs/ with guide, MLOps pipeline, and API reference

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 06:16:28 +00:00

112 lines
5.0 KiB
Markdown

# Architecture
## Stack
```
┌─────────────────────────────────────────┐
│ Frontend (Next.js 14 + Tailwind CSS) │
│ port 3003 — proxies /api → backend │
└────────────────────┬────────────────────┘
│ HTTP
┌────────────────────▼────────────────────┐
│ Backend (Rust / Axum 0.7) │
│ port 3001 — Basic Auth │
│ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ HTTP routes │ │ ML worker │ │
│ │ /api/tasks │ │ (Tokio task) │ │
│ │ /api/graph │ │ Ollama client │ │
│ │ /api/ml/… │ └──────┬───────────┘ │
│ └──────┬───────┘ │ │
│ └────────┬────────┘ │
│ SQLite (taskpile.db) │
│ ├── tasks │
│ ├── task_features │
│ └── task_edges │
└─────────────────────────────────────────┘
│ HTTP
┌────────────────────▼────────────────────┐
│ Ollama (localhost:11434) │
│ nomic-embed-text — embeddings │
│ qwen2.5:1.5b — descriptions │
└─────────────────────────────────────────┘
```
## Key modules
| Path | Responsibility |
|------|---------------|
| `backend/src/ml/config.rs` | Single source of truth for model IDs, prompt version, similarity threshold. Changing any field triggers automatic backfill. |
| `backend/src/ml/features.rs` | Content hash, embedding encode/decode, `mark_pending`, `compute`, `next_stale`. |
| `backend/src/ml/edges.rs` | Pairwise cosine similarity, canonical ordering, transactional edge recompute. |
| `backend/src/ml/worker.rs` | Tokio background task. Drains pending rows, retries failures after 30 s, 60 s slow-poll. |
| `backend/src/routes/graph.rs` | Pure read over `task_features` + `task_edges`. Zero Ollama calls at query time. |
| `backend/src/state.rs` | `AppState` with `SqlitePool`, `Arc<Notify>`, `Arc<MLConfig>`. `FromRef` lets read-only routes extract just the pool. |
| `frontend/src/components/GraphView.tsx` | `react-force-graph-2d` canvas with 3-phase node centering animation, 2-hop BFS filtering, edge threshold slider. |
## Data flow
```
User creates task
POST /api/tasks
INSERT tasks
INSERT task_features (status='pending', content_hash=sha256(pv+title))
notify.notify_one()
▼ (async)
ML Worker
next_stale() → pick pending row
Ollama generate_description()
Ollama get_embedding()
UPDATE task_features (status='ready', embedding=blob)
DELETE task_edges WHERE source=id OR target=id
INSERT task_edges for each pair with sim ≥ threshold
▼ (next request)
GET /api/graph
SELECT tasks → nodes
SELECT task_edges → edges
SELECT COUNT(*) WHERE status IN ('pending','failed') → pending_count
Return JSON (zero Ollama calls)
```
## Database schema
### `tasks`
| Column | Type | Notes |
|--------|------|-------|
| id | TEXT PK | UUID v4 |
| title | TEXT | May contain `@project` and `#tag` tokens |
| description | TEXT | Optional user description |
| completed | BOOLEAN | |
| created_at | INTEGER | Unix seconds |
| project | TEXT | Parsed from title |
| tags | TEXT | Comma-separated, parsed from title |
| latent_desc | TEXT | Legacy — kept for migration, not read |
### `task_features`
| Column | Type | Notes |
|--------|------|-------|
| task_id | TEXT PK FK | → tasks.id ON DELETE CASCADE |
| content_hash | TEXT | sha256(prompt_version + title) |
| latent_desc | TEXT | AI-generated standalone description |
| embedding | BLOB | LE-encoded f32 array |
| embed_dim | INTEGER | Length of embedding |
| desc_model | TEXT | e.g. `qwen2.5:1.5b` |
| embed_model | TEXT | e.g. `nomic-embed-text` |
| prompt_version | TEXT | e.g. `v1` |
| status | TEXT | `pending` \| `ready` \| `failed` |
| error | TEXT | Last error message if failed |
| updated_at | INTEGER | Unix seconds |
### `task_edges`
| Column | Type | Notes |
|--------|------|-------|
| source | TEXT FK | Canonical: source < target |
| target | TEXT FK | → tasks.id ON DELETE CASCADE |
| weight | REAL | Cosine similarity ∈ [0, 1] |
| model_key | TEXT | `{embed_model}@{prompt_version}` |
| updated_at | INTEGER | Unix seconds |