From 0bd362f582e404c001b093f535c06be0b5eb30cc Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 8 Apr 2026 10:11:25 -0400 Subject: [PATCH] docs: add Knowledge task consolidation implementation plan --- ...2026-04-08-knowledge-task-consolidation.md | 746 ++++++++++++++++++ 1 file changed, 746 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-08-knowledge-task-consolidation.md diff --git a/docs/superpowers/plans/2026-04-08-knowledge-task-consolidation.md b/docs/superpowers/plans/2026-04-08-knowledge-task-consolidation.md new file mode 100644 index 0000000..1ee0d6c --- /dev/null +++ b/docs/superpowers/plans/2026-04-08-knowledge-task-consolidation.md @@ -0,0 +1,746 @@ +# Knowledge View Task Consolidation — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Consolidate tasks into the Knowledge view as a fifth card type, deprecate `/notes` and `/tasks` list routes, and simplify navigation down to a single Knowledge hub. + +**Architecture:** The backend knowledge service (`services/knowledge.py`) stops excluding tasks from queries and adds `type=task` filtering via the `is_task` property (`Note.status IS NOT NULL`). The knowledge route validation gains `"task"` as a valid type. The frontend KnowledgeView gains task card rendering with status/priority/due-date badges. Router redirects replace the deleted list views. + +**Tech Stack:** Python/Quart backend (SQLAlchemy), Vue 3 + TypeScript frontend, Pinia stores, Vue Router. + +--- + +## File Map + +| Action | Path | +|--------|------| +| Modify | `src/fabledassistant/services/knowledge.py` | +| Modify | `src/fabledassistant/routes/knowledge.py` | +| Modify | `frontend/src/views/KnowledgeView.vue` | +| Modify | `frontend/src/router/index.ts` | +| Modify | `frontend/src/components/AppHeader.vue` | +| Modify | `frontend/src/App.vue` | +| Delete | `frontend/src/views/NotesListView.vue` | +| Delete | `frontend/src/views/TasksListView.vue` | + +--- + +### Task 1: Backend — Include tasks in knowledge queries + +**Files:** +- Modify: `src/fabledassistant/services/knowledge.py` +- Modify: `src/fabledassistant/routes/knowledge.py` + +**Context:** The knowledge service currently excludes tasks by filtering `Note.status.is_(None)`. Every query function (`query_knowledge`, `query_knowledge_ids`, `_semantic_knowledge_search`, `get_knowledge_tags`, `get_knowledge_counts`) has this exclusion. Adding task support means: (1) removing the task exclusion from the "all types" queries, (2) adding `type=task` as a filter option that maps to `Note.status.isnot(None)`, (3) enriching `_note_to_item` with task-specific fields, (4) updating counts to include tasks. + +- [ ] **Step 1: Add `"task"` to `_VALID_TYPES` in the route file** + +In `src/fabledassistant/routes/knowledge.py`, change: + +```python +_VALID_TYPES = {"note", "person", "place", "list"} +``` + +to: + +```python +_VALID_TYPES = {"note", "person", "place", "list", "task"} +``` + +- [ ] **Step 2: Update `_note_to_item` to include task fields** + +In `src/fabledassistant/services/knowledge.py`, the `_note_to_item` function builds the item dict. After the existing `elif note.entity_type == "list":` block (which ends around line 48), add a task branch. Find: + +```python + elif note.entity_type == "list": + # Parse markdown task list syntax into structured items + body = note.body or "" + list_items = [] + for line in body.split("\n"): + stripped = line.strip() + if stripped.startswith("- [ ] ") or stripped.startswith("- [x] ") or stripped.startswith("- [X] "): + checked_item = not stripped.startswith("- [ ] ") + list_items.append({"text": stripped[6:], "checked": checked_item}) + item["list_items"] = list_items + item["item_count"] = len(list_items) + item["checked_count"] = sum(1 for i in list_items if i["checked"]) + item["body"] = body + return item +``` + +Replace with: + +```python + elif note.entity_type == "list": + # Parse markdown task list syntax into structured items + body = note.body or "" + list_items = [] + for line in body.split("\n"): + stripped = line.strip() + if stripped.startswith("- [ ] ") or stripped.startswith("- [x] ") or stripped.startswith("- [X] "): + checked_item = not stripped.startswith("- [ ] ") + list_items.append({"text": stripped[6:], "checked": checked_item}) + item["list_items"] = list_items + item["item_count"] = len(list_items) + item["checked_count"] = sum(1 for i in list_items if i["checked"]) + item["body"] = body + + # Task fields — included for all items but only meaningful when is_task + if note.is_task: + item["note_type"] = "task" + item["status"] = note.status + item["priority"] = note.priority + item["due_date"] = note.due_date.isoformat() if note.due_date else None + + return item +``` + +This overrides `note_type` to `"task"` for task items (since `entity_type` returns the `note_type` column which is `"note"` for tasks) and adds status/priority/due_date fields. + +- [ ] **Step 3: Update `query_knowledge` to include tasks** + +In the `query_knowledge` function, the "all types" filter currently excludes tasks. Change the base query and the `else` branch. + +Find: + +```python + base = ( + select(Note) + .where(Note.user_id == user_id) + .where(Note.status.is_(None)) # exclude tasks + ) + + if note_type: + base = base.where(Note.note_type == note_type) + else: + # Exclude tasks — already done above; also exclude any legacy nulls + base = base.where(Note.note_type.in_(["note", "person", "place", "list"])) +``` + +Replace with: + +```python + base = select(Note).where(Note.user_id == user_id) + + if note_type == "task": + base = base.where(Note.status.isnot(None)) + elif note_type: + base = base.where(Note.note_type == note_type).where(Note.status.is_(None)) + else: + # All types including tasks + pass +``` + +- [ ] **Step 4: Update `_semantic_knowledge_search` to include tasks** + +Find: + +```python + candidates = await semantic_search_notes( + user_id=user_id, + query=q, + limit=min(200, limit * 8), + threshold=0.3, + is_task=False, + ) +``` + +Replace with: + +```python + is_task_filter = True if note_type == "task" else (False if note_type else None) + candidates = await semantic_search_notes( + user_id=user_id, + query=q, + limit=min(200, limit * 8), + threshold=0.3, + is_task=is_task_filter, + ) +``` + +Also update the type matching in the filter loop — find: + +```python + for _score, note in candidates: + if note_type and note.entity_type != note_type: + continue +``` + +Replace with: + +```python + for _score, note in candidates: + if note_type == "task" and not note.is_task: + continue + elif note_type and note_type != "task" and note.entity_type != note_type: + continue +``` + +- [ ] **Step 5: Update `query_knowledge_ids` to include tasks** + +Find: + +```python + base = ( + select(Note.id) + .where(Note.user_id == user_id) + .where(Note.status.is_(None)) + ) + if note_type: + base = base.where(Note.note_type == note_type) + else: + base = base.where(Note.note_type.in_(["note", "person", "place", "list"])) +``` + +Replace with: + +```python + base = select(Note.id).where(Note.user_id == user_id) + + if note_type == "task": + base = base.where(Note.status.isnot(None)) + elif note_type: + base = base.where(Note.note_type == note_type).where(Note.status.is_(None)) + else: + pass +``` + +- [ ] **Step 6: Update `get_knowledge_tags` to include task tags** + +Find: + +```python + base = ( + select(func.unnest(Note.tags).label("tag")) + .where(Note.user_id == user_id) + .where(Note.status.is_(None)) + ) + if note_type: + base = base.where(Note.note_type == note_type) + else: + base = base.where(Note.note_type.in_(["note", "person", "place", "list"])) +``` + +Replace with: + +```python + base = ( + select(func.unnest(Note.tags).label("tag")) + .where(Note.user_id == user_id) + ) + if note_type == "task": + base = base.where(Note.status.isnot(None)) + elif note_type: + base = base.where(Note.note_type == note_type).where(Note.status.is_(None)) + else: + pass +``` + +- [ ] **Step 7: Update `get_knowledge_counts` to include tasks** + +Find: + +```python + async with async_session() as session: + stmt = ( + select(Note.note_type, func.count(Note.id)) + .where(Note.user_id == user_id) + .where(Note.status.is_(None)) + .where(Note.note_type.in_(["note", "person", "place", "list"])) + .group_by(Note.note_type) + ) + if tags: + for tag in tags: + stmt = stmt.where(Note.tags.contains([tag])) + rows = list((await session.execute(stmt)).all()) + counts = {row[0]: row[1] for row in rows} + # Ensure all types present even if zero + for t in ("note", "person", "place", "list"): + counts.setdefault(t, 0) + counts["total"] = sum(counts[t] for t in ("note", "person", "place", "list")) + return counts +``` + +Replace with: + +```python + async with async_session() as session: + # Count non-task types + stmt = ( + select(Note.note_type, func.count(Note.id)) + .where(Note.user_id == user_id) + .where(Note.status.is_(None)) + .where(Note.note_type.in_(["note", "person", "place", "list"])) + .group_by(Note.note_type) + ) + if tags: + for tag in tags: + stmt = stmt.where(Note.tags.contains([tag])) + rows = list((await session.execute(stmt)).all()) + counts = {row[0]: row[1] for row in rows} + + # Count tasks separately (is_task = status IS NOT NULL) + task_stmt = ( + select(func.count(Note.id)) + .where(Note.user_id == user_id) + .where(Note.status.isnot(None)) + ) + if tags: + for tag in tags: + task_stmt = task_stmt.where(Note.tags.contains([tag])) + task_count: int = (await session.execute(task_stmt)).scalar_one() + counts["task"] = task_count + + for t in ("note", "person", "place", "list", "task"): + counts.setdefault(t, 0) + counts["total"] = sum(counts[t] for t in ("note", "person", "place", "list", "task")) + return counts +``` + +- [ ] **Step 8: Verify backend changes** + +```bash +cd /path/to/fabledassistant +make typecheck +make test +``` + +Expected: no errors. + +- [ ] **Step 9: Commit** + +```bash +git add src/fabledassistant/services/knowledge.py src/fabledassistant/routes/knowledge.py +git commit -m "feat(knowledge): include tasks in knowledge queries and counts" +``` + +--- + +### Task 2: Frontend — Task card rendering in KnowledgeView + +**Files:** +- Modify: `frontend/src/views/KnowledgeView.vue` + +**Context:** `KnowledgeView.vue` has a `KnowledgeItem` interface and renders cards in a grid. Each card type has type-specific content (person shows relationship/email, list shows checkboxes, etc.). Task cards need status, priority, and due date display. The `activeType` ref controls filtering; it needs `"task"` as a valid value. The type filter sidebar needs a "Tasks" button. The new-note button interaction changes from split-button to toggle. + +- [ ] **Step 1: Add `"task"` to the KnowledgeItem interface and filter type** + +In the `