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 -->
<router-link to="/" class="nav-brand">
<AppLogo :size="34" />
<span class="brand-text">Fabled</span>
<span class="brand-text">Scribe</span>
</router-link>
<!-- Center: primary navigation (desktop) -->
+2 -2
View File
@@ -168,11 +168,11 @@ function onCalendarChanged() {
onMounted(() => {
document.addEventListener("mousedown", onDocClick);
document.addEventListener("fable:calendar-changed", onCalendarChanged);
document.addEventListener("scribe:calendar-changed", onCalendarChanged);
});
onUnmounted(() => {
document.removeEventListener("mousedown", onDocClick);
document.removeEventListener("fable:calendar-changed", onCalendarChanged);
document.removeEventListener("scribe:calendar-changed", onCalendarChanged);
});
// ── Calendar callbacks ─────────────────────────────────────────────────────
-1
View File
@@ -22,7 +22,6 @@ interface Project {
goal: string | null;
status: "active" | "paused" | "completed" | "archived";
color: string | null;
auto_summary: string | null;
permission?: string;
is_shared?: boolean;
created_at: string;
-1
View File
@@ -37,7 +37,6 @@ interface Project {
goal: string | null;
status: "active" | "paused" | "completed" | "archived";
color: string | null;
auto_summary: string | null;
permission?: string;
created_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-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-unavailable p { opacity: 0.7; }
.mcp-available { display: flex; flex-direction: column; gap: 1.25rem; }
+1 -7
View File
@@ -1,6 +1,5 @@
import enum
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, Text
from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, SoftDeleteMixin
@@ -22,10 +21,6 @@ class Project(Base, TimestampMixin, SoftDeleteMixin):
goal: Mapped[str] = mapped_column(Text, default="")
status: Mapped[str] = mapped_column(Text, default="active")
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:
return {
@@ -36,7 +31,6 @@ class Project(Base, TimestampMixin, SoftDeleteMixin):
"goal": self.goal,
"status": self.status,
"color": self.color,
"auto_summary": self.auto_summary,
"created_at": self.created_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:
1. **Heartbeat** — once per minute, log a snapshot of process resources
(RSS memory, asyncio task count, DB pool checked-in/out, curator
busy state). A sudden silence in heartbeats lets you bound the
crash time to within a minute, and the last snapshot before silence
usually rules in or out memory growth / pool exhaustion / hung
curator pass.
(RSS memory, asyncio task count, DB pool checked-in/out). A sudden
silence in heartbeats lets you bound the crash time to within a
minute, and the last snapshot before silence usually rules in or out
memory growth / pool exhaustion.
2. **Signal handler** — catches SIGTERM and SIGINT, logs them with the
sender's intent ("docker stop", "swarm restart", "manual ctrl-C")
@@ -108,11 +107,6 @@ def _asyncio_task_count() -> int | 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:
if _started_at is None:
return None
@@ -134,7 +128,6 @@ def _snapshot_dict(reason: str = "heartbeat") -> dict[str, Any]:
"rss_mb": _process_rss_mb(),
"asyncio_tasks": _asyncio_task_count(),
"db_pool": _db_pool_stats(),
"curator_busy": _curator_busy(),
"pid": os.getpid(),
}
@@ -284,9 +277,9 @@ async def _heartbeat_loop() -> None:
_persistent_log(
logging.INFO,
"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["db_pool"], snap["curator_busy"],
snap["db_pool"],
)
_write_state_atomic(_CURRENT_STATE_PATH, snap)
except Exception:
+1 -4
View File
@@ -53,12 +53,9 @@ async def _get_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.
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
degrade to keyword search.
"""
+3 -10
View File
@@ -13,14 +13,11 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
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
# shows yesterday's content until the user "starts" the new day.
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:
"""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.
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
journal stays anchored to yesterday until they cross the rollover.
so the 00:0004:00 local window still returns *yesterday* — a day-anchored
view stays on yesterday until the user crosses the rollover.
"""
tz = await get_user_tz(user_id)
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"])
@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
async def test_get_embedding_propagates_model_load_failures():
"""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:
p = MagicMock()
base = {"id": 1, "title": "P", "description": "", "goal": "",
"status": "active", "color": None, "auto_summary": None}
"status": "active", "color": None}
base.update(overrides)
p.to_dict.return_value = base
return p