Files
FabledScribe/src/scribe/models/note.py
T
bvandeusen b49efdcb11
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
refactor(scribe): retire calendar/events + person/place/list entities (backend)
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
2026-07-19 13:29:14 -04:00

119 lines
4.9 KiB
Python

import enum
from datetime import date, datetime
from sqlalchemy import Date, DateTime, ForeignKey, Index, Integer, Text
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, SoftDeleteMixin
class TaskStatus(str, enum.Enum):
todo = "todo"
in_progress = "in_progress"
done = "done"
cancelled = "cancelled"
class TaskPriority(str, enum.Enum):
none = "none"
low = "low"
medium = "medium"
high = "high"
class Note(Base, TimestampMixin, SoftDeleteMixin):
__tablename__ = "notes"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True
)
title: Mapped[str] = mapped_column(Text, default="")
body: Mapped[str] = mapped_column(Text, default="")
description: Mapped[str | None] = mapped_column(Text, nullable=True)
consolidated_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
tags: Mapped[list[str]] = mapped_column(ARRAY(Text), default=list)
parent_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("notes.id", ondelete="SET NULL"), nullable=True
)
# Provenance: the task/feature an issue arose from. Distinct from parent_id
# (sub-task hierarchy) — this is "what spawned this". Only meaningful for
# issues; nullable for every record.
arose_from_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("notes.id", ondelete="SET NULL"), nullable=True
)
project_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("projects.id", ondelete="SET NULL"), nullable=True
)
milestone_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("milestones.id", ondelete="SET NULL"), nullable=True
)
status: Mapped[str | None] = mapped_column(Text, nullable=True)
priority: Mapped[str | None] = mapped_column(Text, nullable=True)
due_date: Mapped[date | None] = mapped_column(Date, nullable=True)
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
recurrence_rule: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
recurrence_next_spawn_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# Note type — 'note' (default) or 'process' (a stored process). Task-ness is
# tracked by `status`, not here. (person/place/list entity types removed 2026-07.)
note_type: Mapped[str] = mapped_column(Text, default="note", server_default="note")
# Task sub-kind — 'work' (default), 'plan', or 'issue' (corrective work).
# Only meaningful when the note is a task (status is not None); ordinary
# notes keep the 'work' default and ignore it. Orthogonal to note_type
# (which is the note/entity axis).
task_kind: Mapped[str] = mapped_column(Text, default="work", server_default="work")
__table_args__ = (
Index("ix_notes_tags", "tags", postgresql_using="gin"),
Index("ix_notes_status", "status"),
Index("ix_notes_title", "title"),
Index("ix_notes_user_id", "user_id"),
Index("ix_notes_project_id", "project_id"),
Index("ix_notes_milestone_id", "milestone_id"),
Index("ix_notes_note_type", "note_type"),
Index("ix_notes_arose_from_id", "arose_from_id"),
)
@property
def is_task(self) -> bool:
return self.status is not None
def to_dict(self) -> dict:
return {
"id": self.id,
"title": self.title,
"body": self.body,
"description": self.description,
"consolidated_at": (
self.consolidated_at.isoformat() if self.consolidated_at else None
),
"tags": self.tags or [],
"parent_id": self.parent_id,
"arose_from_id": self.arose_from_id,
"project_id": self.project_id,
"milestone_id": self.milestone_id,
"status": self.status,
"priority": self.priority,
"due_date": self.due_date.isoformat() if self.due_date else None,
"started_at": self.started_at.isoformat() if self.started_at else None,
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
"recurrence_rule": self.recurrence_rule,
"recurrence_next_spawn_at": (
self.recurrence_next_spawn_at.isoformat()
if self.recurrence_next_spawn_at
else None
),
"is_task": self.is_task,
"note_type": self.note_type or "note",
"task_kind": self.task_kind,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
}