refactor(scribe): retire calendar/events + person/place/list entities (backend)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 11s
CI & Build / integration (push) Successful in 30s
CI & Build / Python tests (push) Failing after 31s
CI & Build / Build & push image (push) Has been skipped

Narrow Scribe to a Claude-Code work system-of-record (milestone #194,
decision note #1759). Wholesale removal per rule #22 — backend + schema half.

Calendar/events + CalDAV: delete models/event, services/{events,caldav,
caldav_sync}, routes/events, mcp/tools/events; strip event branches from
backup (bump v3->v4), dashboard (upcoming_events), trash, recent, and the
mcp server read-only allowlist + instructions.

Typed entities (person/place/list): delete mcp/tools/entities; drop the
notes.metadata (entity_meta) column from model/service/routes and the
knowledge browse service. note_type STAYS — it also marks 'process' notes.

Scheduler: event_scheduler -> recurrence_scheduler, keeping only the
recurring-task spawn job (drops event reminders + CalDAV sync).

Schema: migration 0069 drops the events table + notes.metadata column +
orphan caldav settings rows (faithful downgrade recreates them).

KEEP: recurrence.py (task recurrence), notifications task reminders, graph
view, and every work surface. Frontend + plugin/docs true-up follow next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPtbSzA4JLMAKgFZ8VTg7Q
This commit is contained in:
2026-07-19 13:29:14 -04:00
parent f6629d4bcf
commit b49efdcb11
33 changed files with 194 additions and 3453 deletions
+6 -75
View File
@@ -1,4 +1,4 @@
"""Knowledge service — unified query across notes, people, places, and lists."""
"""Knowledge service — unified query across notes, tasks, plans, and processes."""
import logging
from sqlalchemy import func, select
@@ -12,46 +12,16 @@ _SNIPPET_LEN = 200
def _note_to_item(note: Note) -> dict:
meta = note.entity_meta or {}
item: dict = {
"id": note.id,
"note_type": note.entity_type,
"note_type": note.note_type or "note",
"title": note.title,
"snippet": (note.body or "")[:_SNIPPET_LEN],
"tags": note.tags or [],
"project_id": note.project_id,
"metadata": meta,
"created_at": note.created_at.isoformat(),
"updated_at": note.updated_at.isoformat(),
}
# Type-specific convenience fields
if note.entity_type == "person":
item["relationship"] = meta.get("relationship", "")
item["email"] = meta.get("email", "")
item["phone"] = meta.get("phone", "")
item["birthday"] = meta.get("birthday", "")
item["organization"] = meta.get("organization", "")
item["address"] = meta.get("address", "")
elif note.entity_type == "place":
item["address"] = meta.get("address", "")
item["phone"] = meta.get("phone", "")
item["hours"] = meta.get("hours", "")
item["website"] = meta.get("website", "")
item["category"] = meta.get("category", "")
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 — override note_type and add status/priority/due_date
if note.is_task:
item["note_type"] = "task"
@@ -190,7 +160,7 @@ async def _semantic_knowledge_search(
continue
elif note_type == "plan" and (not note.is_task or note.task_kind != "plan"):
continue
elif note_type and note_type not in ("task", "plan") and note.entity_type != note_type:
elif note_type and note_type not in ("task", "plan") and note.note_type != note_type:
continue
if tags and not all(t in (note.tags or []) for t in tags):
continue
@@ -237,7 +207,7 @@ async def get_knowledge_counts(user_id: int, tags: list[str] | None = None) -> d
.where(Note.user_id == user_id)
.where(Note.status.is_(None))
.where(Note.deleted_at.is_(None))
.where(Note.note_type.in_(["note", "person", "place", "list", "process"]))
.where(Note.note_type.in_(["note", "process"]))
.group_by(Note.note_type)
)
if tags:
@@ -273,9 +243,9 @@ async def get_knowledge_counts(user_id: int, tags: list[str] | None = None) -> d
plan_stmt = plan_stmt.where(Note.tags.contains([tag]))
counts["plan"] = (await session.execute(plan_stmt)).scalar_one()
for t in ("note", "person", "place", "list", "task", "plan", "process"):
for t in ("note", "task", "plan", "process"):
counts.setdefault(t, 0)
counts["total"] = sum(counts[t] for t in ("note", "person", "place", "list", "task", "process"))
counts["total"] = sum(counts[t] for t in ("note", "task", "process"))
return counts
@@ -335,42 +305,3 @@ async def get_knowledge_by_ids(user_id: int, ids: list[int]) -> list[dict]:
rows = list((await session.execute(stmt)).scalars().all())
by_id = {n.id: n for n in rows}
return [_note_to_item(by_id[i]) for i in ids if i in by_id]
async def get_people_and_places_context(user_id: int) -> str:
"""Return a compact summary of known people and places for LLM system prompt injection."""
async with async_session() as session:
stmt = (
select(Note)
.where(Note.user_id == user_id)
.where(Note.note_type.in_(["person", "place"]))
.where(Note.status.is_(None))
.where(Note.deleted_at.is_(None))
.order_by(Note.title.asc())
.limit(50)
)
rows = list((await session.execute(stmt)).scalars().all())
if not rows:
return ""
people = [n for n in rows if n.entity_type == "person"]
places = [n for n in rows if n.entity_type == "place"]
lines = []
if people:
parts = []
for p in people:
meta = p.entity_meta or {}
rel = meta.get("relationship", "")
parts.append(f"{p.title}" + (f" ({rel})" if rel else ""))
lines.append("Known people: " + ", ".join(parts))
if places:
parts = []
for p in places:
meta = p.entity_meta or {}
addr = meta.get("address", "")
parts.append(f"{p.title}" + (f" {addr}" if addr else ""))
lines.append("Known places: " + "; ".join(parts))
return "\n".join(lines)