adding claude file and some plans

This commit is contained in:
2026-08-12 18:55:18 -06:00
parent b206964417
commit 4da16c7573
19 changed files with 1294 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
# 10 — Full-text Search (Meilisearch)
Meilisearch holds five indexes kept in sync by domain events (`search_sync` jobs). Postgres remains the source of truth; the index is always rebuildable.
## Indexes & documents
- [ ] `projects``{id, title, description, status, member_ids[]}` — filterable: `member_ids`
- [ ] `issues``{id, project_id, project_issue_id, title, description, status, priority, labels[], assignee, epic_title}` — filterable: `project_id`
- [ ] `epics``{id, project_id, title, description, status}` — filterable: `project_id`
- [ ] `comments``{id, project_id, issue_id, issue_num, issue_title, body, author}` — filterable: `project_id`
- [ ] `wiki_pages``{id, project_id, slug, title, body}` — filterable: `project_id`
- [ ] Searchable attributes ordered (title > description/body > rest); typo tolerance default; doc ids = Postgres ids
- [ ] Index settings applied idempotently at startup (create-if-missing, update settings)
## Sync
- [ ] Event → sync mapping in the dispatcher's search consumer:
- [ ] `*.created` / `*.updated` (incl. label/status/assignee changes on issues) → `{index, doc_id, op: upsert}` job
- [ ] `*.deleted` (soft delete!) → `{op: delete}` job — soft-deleted content must leave the index immediately
- [ ] `project.member_added/removed` → upsert the project doc (member_ids changed)
- [ ] `search_sync` job handler loads current row from Postgres (not the event payload — always index latest state); row gone or soft-deleted → delete op
- [ ] Jobs idempotent (upsert/delete by id); retries via queue backoff
- [ ] `POST /admin/search/reindex` + CLI `solopm reindex`: drop-and-rebuild all five indexes from Postgres, streaming in batches of 1000
## Query path
- [ ] `GET /api/v1/search?q=&types=&project_id=`:
- [ ] Resolve caller's project memberships once
- [ ] `projects` index filtered `member_ids CONTAINS user`; other indexes filtered `project_id IN (memberships)` (narrowed to `project_id=` param when present)
- [ ] Multi-index federated query; merge by Meilisearch ranking score; cap 50 results
- [ ] Response items carry enough to render + navigate: type, project, title/snippet (highlighted), route params (`issue_num`, `slug`…)
- [ ] Permission guarantee: a user must never see results (even titles) from projects they're not a member of — enforced by the filter, verified by an integration test
- [ ] Meilisearch down/unconfigured → 503 with `{"error":{"code":"search_unavailable"}}`; rest of the app unaffected
## Frontend
- [ ] **Command palette** (Cmd/Ctrl+K, [13-frontend.md](13-frontend.md)): debounced search-as-you-type against `/search`, grouped by type, arrow-key navigation, Enter → route; recent visits shown when query empty
- [ ] `/search` page: same query with type filter tabs and full result list