Summary briefing layer: Today pre-summarized, /a is the canonical read

Make summaries the core reading experience (summary-first, source-forward):
- Cycle pre-warms summaries for Today's 7 (idempotent → only new ones hit the LLM).
- /api/brief items carry their cached summary; Today cards (hero + tiles) show it
  inline, so Today reads as a calm briefing.
- Card title/image now open the /a summary page (the canonical artifact), with a
  visible "Full story" link straight to the source on every card (the escape hatch).
- /a gains related-grouping chips + a Copy-link/share control.
- Tighten the summary prompt: original, factual, no quotations / no close paraphrase.
Long tail stays lazy+cached. No article bodies stored. 129 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-04 19:48:32 +00:00
parent 3924d927aa
commit cfde4e22db
6 changed files with 80 additions and 14 deletions
+4 -1
View File
@@ -243,11 +243,13 @@ class Article(BaseModel):
rank: int | None = None # position within a brief, when applicable
paywalled: bool = False
tags: list[str] = []
summary: str | None = None # our own cached summary (present on the brief)
@classmethod
def from_row(cls, row: dict) -> "Article":
raw_tags = row.get("tags")
return cls(
summary=row.get("summary"),
id=row["id"],
title=row["title"],
description=row.get("description"),
@@ -693,7 +695,8 @@ def create_app() -> FastAPI:
with get_conn() as conn:
row = conn.execute(
"SELECT a.id, a.title, a.description, a.image_url, a.canonical_url, "
"a.duplicate_of, src.name AS source_name, s.reason_text, s.accepted "
"a.duplicate_of, src.name AS source_name, s.reason_text, s.accepted, "
"(SELECT group_concat(t.tag) FROM article_tags t WHERE t.article_id = a.id) AS tags "
"FROM articles a JOIN sources src ON src.id = a.source_id "
"LEFT JOIN article_scores s ON s.article_id = a.id WHERE a.id = ?",
(aid,),