chore: remove pre-pivot dead code + finish Scribe rebrand (#599 t1-3)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 51s
CI & Build / Build & push image (push) Successful in 1m1s

- Header wordmark Fabled -> Scribe; fable:calendar-changed event ->
  scribe:calendar-changed; SettingsView CSS comment.
- Drop dead Project.auto_summary + summary_updated_at columns (migration
  0063) -- the Ollama-era summarizer is gone; model + 2 frontend types +
  projects test updated.
- Remove pivot vestiges: diagnostics _curator_busy()/curator_busy
  heartbeat field, tz BRIEFING_DAY_START_HOUR/user_briefing_date dead
  aliases, the ignored 'model' param on get_embedding (+ its test).

ruff src/ clean; CI is the gate. Part of scribe plan #599.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 16:16:44 -04:00
parent b255a0f90e
commit 70ab3f38c6
12 changed files with 50 additions and 57 deletions
@@ -0,0 +1,34 @@
"""drop dead Project.auto_summary columns
Revision ID: 0063
Revises: 0062
Create Date: 2026-06-03
auto_summary + summary_updated_at were written by generate_project_summary
(Ollama), removed in the MCP pivot. Nothing has populated them since; the
field was stale-or-NULL. Drop the columns.
"""
from alembic import op
import sqlalchemy as sa
revision = "0063"
down_revision = "0062"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.drop_column("projects", "auto_summary")
op.drop_column("projects", "summary_updated_at")
def downgrade() -> None:
op.add_column(
"projects",
sa.Column("summary_updated_at", sa.DateTime(timezone=True), nullable=True),
)
op.add_column(
"projects",
sa.Column("auto_summary", sa.Text(), nullable=True),
)
+1 -1
View File
@@ -39,7 +39,7 @@ router.afterEach(() => {
<!-- Left: brand --> <!-- Left: brand -->
<router-link to="/" class="nav-brand"> <router-link to="/" class="nav-brand">
<AppLogo :size="34" /> <AppLogo :size="34" />
<span class="brand-text">Fabled</span> <span class="brand-text">Scribe</span>
</router-link> </router-link>
<!-- Center: primary navigation (desktop) --> <!-- Center: primary navigation (desktop) -->
+2 -2
View File
@@ -168,11 +168,11 @@ function onCalendarChanged() {
onMounted(() => { onMounted(() => {
document.addEventListener("mousedown", onDocClick); document.addEventListener("mousedown", onDocClick);
document.addEventListener("fable:calendar-changed", onCalendarChanged); document.addEventListener("scribe:calendar-changed", onCalendarChanged);
}); });
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener("mousedown", onDocClick); document.removeEventListener("mousedown", onDocClick);
document.removeEventListener("fable:calendar-changed", onCalendarChanged); document.removeEventListener("scribe:calendar-changed", onCalendarChanged);
}); });
// ── Calendar callbacks ───────────────────────────────────────────────────── // ── Calendar callbacks ─────────────────────────────────────────────────────
-1
View File
@@ -22,7 +22,6 @@ interface Project {
goal: string | null; goal: string | null;
status: "active" | "paused" | "completed" | "archived"; status: "active" | "paused" | "completed" | "archived";
color: string | null; color: string | null;
auto_summary: string | null;
permission?: string; permission?: string;
is_shared?: boolean; is_shared?: boolean;
created_at: string; created_at: string;
-1
View File
@@ -37,7 +37,6 @@ interface Project {
goal: string | null; goal: string | null;
status: "active" | "paused" | "completed" | "archived"; status: "active" | "paused" | "completed" | "archived";
color: string | null; color: string | null;
auto_summary: string | null;
permission?: string; permission?: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
+1 -1
View File
@@ -3076,7 +3076,7 @@ function formatUserDate(iso: string): string {
.settings-empty { opacity: 0.5; margin-top: 1rem; } .settings-empty { opacity: 0.5; margin-top: 1rem; }
.settings-description { opacity: 0.7; margin-bottom: 1rem; line-height: 1.5; } .settings-description { opacity: 0.7; margin-bottom: 1rem; line-height: 1.5; }
/* Fable MCP section */ /* Scribe MCP section */
.mcp-status { opacity: 0.6; font-size: 0.9rem; } .mcp-status { opacity: 0.6; font-size: 0.9rem; }
.mcp-unavailable p { opacity: 0.7; } .mcp-unavailable p { opacity: 0.7; }
.mcp-available { display: flex; flex-direction: column; gap: 1.25rem; } .mcp-available { display: flex; flex-direction: column; gap: 1.25rem; }
+1 -7
View File
@@ -1,6 +1,5 @@
import enum import enum
from datetime import datetime from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy import DateTime, ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base from scribe.models import Base
from scribe.models.base import TimestampMixin, SoftDeleteMixin from scribe.models.base import TimestampMixin, SoftDeleteMixin
@@ -22,10 +21,6 @@ class Project(Base, TimestampMixin, SoftDeleteMixin):
goal: Mapped[str] = mapped_column(Text, default="") goal: Mapped[str] = mapped_column(Text, default="")
status: Mapped[str] = mapped_column(Text, default="active") status: Mapped[str] = mapped_column(Text, default="active")
color: Mapped[str | None] = mapped_column(Text, nullable=True) # hex color color: Mapped[str | None] = mapped_column(Text, nullable=True) # hex color
auto_summary: Mapped[str | None] = mapped_column(Text, nullable=True, default=None)
summary_updated_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True, default=None
)
def to_dict(self) -> dict: def to_dict(self) -> dict:
return { return {
@@ -36,7 +31,6 @@ class Project(Base, TimestampMixin, SoftDeleteMixin):
"goal": self.goal, "goal": self.goal,
"status": self.status, "status": self.status,
"color": self.color, "color": self.color,
"auto_summary": self.auto_summary,
"created_at": self.created_at.isoformat(), "created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(), "updated_at": self.updated_at.isoformat(),
} }
+6 -13
View File
@@ -5,11 +5,10 @@ clear cause in the logs. This module adds three things designed to make
the crash class identifiable from logs alone: the crash class identifiable from logs alone:
1. **Heartbeat** — once per minute, log a snapshot of process resources 1. **Heartbeat** — once per minute, log a snapshot of process resources
(RSS memory, asyncio task count, DB pool checked-in/out, curator (RSS memory, asyncio task count, DB pool checked-in/out). A sudden
busy state). A sudden silence in heartbeats lets you bound the silence in heartbeats lets you bound the crash time to within a
crash time to within a minute, and the last snapshot before silence minute, and the last snapshot before silence usually rules in or out
usually rules in or out memory growth / pool exhaustion / hung memory growth / pool exhaustion.
curator pass.
2. **Signal handler** — catches SIGTERM and SIGINT, logs them with the 2. **Signal handler** — catches SIGTERM and SIGINT, logs them with the
sender's intent ("docker stop", "swarm restart", "manual ctrl-C") sender's intent ("docker stop", "swarm restart", "manual ctrl-C")
@@ -108,11 +107,6 @@ def _asyncio_task_count() -> int | None:
return None return None
def _curator_busy() -> bool | None:
# Curator was removed in Phase 8 of the MCP-first pivot. Always False.
return False
def _uptime_secs() -> float | None: def _uptime_secs() -> float | None:
if _started_at is None: if _started_at is None:
return None return None
@@ -134,7 +128,6 @@ def _snapshot_dict(reason: str = "heartbeat") -> dict[str, Any]:
"rss_mb": _process_rss_mb(), "rss_mb": _process_rss_mb(),
"asyncio_tasks": _asyncio_task_count(), "asyncio_tasks": _asyncio_task_count(),
"db_pool": _db_pool_stats(), "db_pool": _db_pool_stats(),
"curator_busy": _curator_busy(),
"pid": os.getpid(), "pid": os.getpid(),
} }
@@ -284,9 +277,9 @@ async def _heartbeat_loop() -> None:
_persistent_log( _persistent_log(
logging.INFO, logging.INFO,
"diag heartbeat: uptime=%ss rss=%sMB asyncio_tasks=%s " "diag heartbeat: uptime=%ss rss=%sMB asyncio_tasks=%s "
"db_pool=%s curator_busy=%s", "db_pool=%s",
snap["uptime_secs"], snap["rss_mb"], snap["asyncio_tasks"], snap["uptime_secs"], snap["rss_mb"], snap["asyncio_tasks"],
snap["db_pool"], snap["curator_busy"], snap["db_pool"],
) )
_write_state_atomic(_CURRENT_STATE_PATH, snap) _write_state_atomic(_CURRENT_STATE_PATH, snap)
except Exception: except Exception:
+1 -4
View File
@@ -53,12 +53,9 @@ async def _get_model():
return _model return _model
async def get_embedding(text: str, model: str | None = None) -> list[float]: async def get_embedding(text: str) -> list[float]:
"""Get an embedding vector for the given text. """Get an embedding vector for the given text.
The ``model`` parameter is preserved for backward compatibility with the
Ollama era but is now ignored — fastembed uses a single fixed model.
Raises if the fastembed model fails to load. Callers should catch and Raises if the fastembed model fails to load. Callers should catch and
degrade to keyword search. degrade to keyword search.
""" """
+3 -10
View File
@@ -13,14 +13,11 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from scribe.services.settings import get_setting from scribe.services.settings import get_setting
# Day-rollover boundary for journal/day-anchored views. The "day" flips at # Day-rollover boundary for day-anchored "today" logic. The "day" flips at
# this local hour (not midnight) so the 00:0004:00 local window still # this local hour (not midnight) so the 00:0004:00 local window still
# shows yesterday's content until the user "starts" the new day. # shows yesterday's content until the user "starts" the new day.
DAY_ROLLOVER_HOUR = 4 DAY_ROLLOVER_HOUR = 4
# Backwards-compat alias for code paths still referencing the old name.
BRIEFING_DAY_START_HOUR = DAY_ROLLOVER_HOUR
async def get_user_tz(user_id: int) -> ZoneInfo: async def get_user_tz(user_id: int) -> ZoneInfo:
"""Return the user's IANA ``ZoneInfo``, falling back to UTC.""" """Return the user's IANA ``ZoneInfo``, falling back to UTC."""
@@ -41,12 +38,8 @@ async def user_day_date(user_id: int) -> date:
"""Return the current "day" in the user's local timezone. """Return the current "day" in the user's local timezone.
The day flips at ``DAY_ROLLOVER_HOUR`` (4am local) rather than midnight, The day flips at ``DAY_ROLLOVER_HOUR`` (4am local) rather than midnight,
so the 00:0004:00 local window still returns *yesterday* — the user's so the 00:0004:00 local window still returns *yesterday* — a day-anchored
journal stays anchored to yesterday until they cross the rollover. view stays on yesterday until the user crosses the rollover.
""" """
tz = await get_user_tz(user_id) tz = await get_user_tz(user_id)
return (datetime.now(tz) - timedelta(hours=DAY_ROLLOVER_HOUR)).date() return (datetime.now(tz) - timedelta(hours=DAY_ROLLOVER_HOUR)).date()
# Backwards-compat alias used by briefing-only modules being torn down in Stage B.
user_briefing_date = user_day_date
-16
View File
@@ -62,22 +62,6 @@ async def test_get_embedding_returns_list_of_floats():
fake_embedder.embed.assert_called_once_with(["hello world"]) fake_embedder.embed.assert_called_once_with(["hello world"])
@pytest.mark.asyncio
async def test_get_embedding_ignores_legacy_model_param():
"""The `model` kwarg is preserved for backward-compat but should not affect
the fastembed call."""
fake_vec = MagicMock()
fake_vec.tolist.return_value = [0.0]
fake_embedder = MagicMock()
fake_embedder.embed = MagicMock(return_value=iter([fake_vec]))
with patch(
"scribe.services.embeddings._get_model",
AsyncMock(return_value=fake_embedder),
):
out = await get_embedding("x", model="ignored-model-name")
assert out == [0.0]
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_embedding_propagates_model_load_failures(): async def test_get_embedding_propagates_model_load_failures():
"""If fastembed can't initialize, the error propagates — callers catch """If fastembed can't initialize, the error propagates — callers catch
+1 -1
View File
@@ -20,7 +20,7 @@ def _bind_user():
def _fake_project(**overrides) -> MagicMock: def _fake_project(**overrides) -> MagicMock:
p = MagicMock() p = MagicMock()
base = {"id": 1, "title": "P", "description": "", "goal": "", base = {"id": 1, "title": "P", "description": "", "goal": "",
"status": "active", "color": None, "auto_summary": None} "status": "active", "color": None}
base.update(overrides) base.update(overrides)
p.to_dict.return_value = base p.to_dict.return_value = base
return p return p