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
co-authored by Claude Opus 4.8
parent f6629d4bcf
commit b49efdcb11
33 changed files with 194 additions and 3453 deletions
+9 -80
View File
@@ -4,7 +4,6 @@ from datetime import date, datetime, timezone
from sqlalchemy import or_, select
from scribe.models import async_session
from scribe.models.event import Event
from scribe.models.milestone import Milestone
from scribe.models.note import Note
from scribe.models.note_draft import NoteDraft
@@ -25,9 +24,10 @@ from scribe.models.user import User
logger = logging.getLogger(__name__)
# Backup format version. v3 (2026-06) added rulebooks/topics/rules + their
# project subscription/suppression join tables, and events — all silently
# dropped by v2. Bump when the serialized schema changes.
BACKUP_VERSION = 3
# project subscription/suppression join tables. v4 (2026-07) dropped events
# when the calendar surface was retired — old v3 events are skipped on restore.
# Bump when the serialized schema changes.
BACKUP_VERSION = 4
# Tables intentionally NOT in the backup, surfaced in the payload so the gap is
# explicit rather than silent. ACL (groups/shares) is a coherent follow-up;
@@ -80,7 +80,6 @@ async def export_full_backup() -> dict:
rulebooks = (await session.execute(select(Rulebook))).scalars().all()
topics = (await session.execute(select(RulebookTopic))).scalars().all()
rules = (await session.execute(select(Rule))).scalars().all()
events = (await session.execute(select(Event))).scalars().all()
subscriptions = (await session.execute(
select(project_rulebook_subscriptions)
)).all()
@@ -245,27 +244,6 @@ async def export_full_backup() -> dict:
"rulebook_subscriptions": _subscription_rows(subscriptions),
"rule_suppressions": _rule_suppression_rows(rule_suppressions),
"topic_suppressions": _topic_suppression_rows(topic_suppressions),
"events": [
{
"id": e.id,
"user_id": e.user_id,
"project_id": e.project_id,
"uid": e.uid,
"caldav_uid": e.caldav_uid,
"title": e.title,
"start_dt": e.start_dt.isoformat() if e.start_dt else None,
"duration_minutes": e.duration_minutes,
"all_day": e.all_day,
"description": e.description,
"location": e.location,
"color": e.color,
"recurrence": e.recurrence,
"reminder_minutes": e.reminder_minutes,
"created_at": e.created_at.isoformat(),
"updated_at": e.updated_at.isoformat(),
}
for e in events
],
}
@@ -314,9 +292,6 @@ async def export_user_backup(user_id: int) -> dict:
rules = (await session.execute(
select(Rule).where(or_(*rule_filters))
)).scalars().all() if rule_filters else []
events = (await session.execute(
select(Event).where(Event.user_id == user_id)
)).scalars().all()
if project_ids:
subscriptions = (await session.execute(
select(project_rulebook_subscriptions).where(
@@ -480,27 +455,6 @@ async def export_user_backup(user_id: int) -> dict:
"rulebook_subscriptions": _subscription_rows(subscriptions),
"rule_suppressions": _rule_suppression_rows(rule_suppressions),
"topic_suppressions": _topic_suppression_rows(topic_suppressions),
"events": [
{
"id": e.id,
"user_id": e.user_id,
"project_id": e.project_id,
"uid": e.uid,
"caldav_uid": e.caldav_uid,
"title": e.title,
"start_dt": e.start_dt.isoformat() if e.start_dt else None,
"duration_minutes": e.duration_minutes,
"all_day": e.all_day,
"description": e.description,
"location": e.location,
"color": e.color,
"recurrence": e.recurrence,
"reminder_minutes": e.reminder_minutes,
"created_at": e.created_at.isoformat(),
"updated_at": e.updated_at.isoformat(),
}
for e in events
],
}
@@ -591,17 +545,17 @@ async def _restore_v1(data: dict) -> dict:
async def _restore_v2(data: dict) -> dict:
"""Restore v2/v3 backup with full FK re-mapping.
Conversations + push subscriptions in pre-pivot backups are silently
skipped — those subsystems were removed in the MCP-first pivot. v3-only
sections (rulebooks/topics/rules/join-tables/events) are guarded by
data.get so a v2 payload restores without them.
Conversations, push subscriptions, and (as of v4) events in older backups
are silently skipped — those subsystems were removed. v3+ sections
(rulebooks/topics/rules/join-tables) are guarded by data.get so a v2
payload restores without them.
"""
stats: dict[str, int] = {
"users": 0, "projects": 0, "milestones": 0, "notes": 0,
"task_logs": 0, "note_drafts": 0, "note_versions": 0,
"settings": 0, "rulebooks": 0, "rulebook_topics": 0, "rules": 0,
"rulebook_subscriptions": 0, "rule_suppressions": 0,
"topic_suppressions": 0, "events": 0,
"topic_suppressions": 0,
}
async with async_session() as session:
@@ -860,31 +814,6 @@ async def _restore_v2(data: dict) -> dict:
))
stats["topic_suppressions"] += 1
# 15. Events (v3)
for e_data in data.get("events", []):
mapped_uid = user_id_map.get(e_data.get("user_id", 0))
if mapped_uid is None:
continue
ev = Event(
user_id=mapped_uid,
project_id=project_id_map.get(e_data["project_id"]) if e_data.get("project_id") else None,
uid=e_data.get("uid", ""),
caldav_uid=e_data.get("caldav_uid", ""),
title=e_data.get("title", ""),
start_dt=_dt(e_data.get("start_dt")),
duration_minutes=e_data.get("duration_minutes"),
all_day=e_data.get("all_day", False),
description=e_data.get("description", ""),
location=e_data.get("location", ""),
color=e_data.get("color", ""),
recurrence=e_data.get("recurrence"),
reminder_minutes=e_data.get("reminder_minutes"),
created_at=_dt(e_data.get("created_at")),
updated_at=_dt(e_data.get("updated_at")),
)
session.add(ev)
stats["events"] += 1
await session.commit()
logger.info("Restored v2/v3 backup: %s", stats)