Sync repo to deployed state: SEO recovery, Publishing Desk, Play games, emoji picker
The deploy pipeline runs from the working tree, so a wave of shipped features
had never been committed. This snapshots git to what's actually running.
SEO impression recovery (live + verified):
- Duplicate /a/{id} now 301-redirect to their canonical twin instead of 404
(a hard 404 silently dropped already-indexed URLs and tanked impressions).
- Dedup representative selection reworked: accepted/serveable -> established
rep (URL stability) -> quality score, so an accepted page never retires to a
rejected rep and an indexed canonical doesn't churn when a newer twin arrives.
- HEAD /a/{id} returns the same status as GET (api_route GET+HEAD) instead of
falling through to the static mount and 404ing.
- `dedup --force-recluster`: cycle-locked, model-free re-cluster to re-apply the
policy to the existing corpus (shared cycle_lock context manager).
- CLI honors GOODNEWS_DB for its default --db (was silently ignored).
Publishing Desk (admin tool to post highlights to X via Web Intents):
- publishing.py queue/rank/handle-resolution; admin UI; full searchable emoji
picker (bundled data, no CDN) for the blurb editor.
Play games + site:
- Bloom (word-wheel), Memory Match, daily ritual set, Zen Den (dev-gated).
- English-only language gate; source prospecting; paywall + dedup hardening.
Tests: full suite green (349). Ignores tightened (node_modules, data/*.db).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+80
-1
@@ -28,6 +28,7 @@ CREATE TABLE IF NOT EXISTS sources (
|
||||
retry_after_at TEXT,
|
||||
review_flag INTEGER NOT NULL DEFAULT 0,
|
||||
review_reason TEXT,
|
||||
x_handle TEXT, -- the source's own verified X handle, if known
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -69,6 +70,7 @@ CREATE TABLE IF NOT EXISTS article_scores (
|
||||
reason_text TEXT,
|
||||
topic TEXT,
|
||||
flavor TEXT,
|
||||
language TEXT,
|
||||
model_name TEXT,
|
||||
scored_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -300,6 +302,13 @@ CREATE TABLE IF NOT EXISTS daily_puzzles (
|
||||
UNIQUE (puzzle_date, game, variant)
|
||||
);
|
||||
|
||||
-- Full-text search over the PUBLIC article corpus (title/description/source/tags).
|
||||
-- Standalone FTS5 (not external-content) since the searchable text spans tables;
|
||||
-- rebuilt from the accepted, non-duplicate set on each ingest cycle (+ lazily).
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS article_search USING fts5(
|
||||
article_id UNINDEXED, title, body, source_name, tags
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS game_state (
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
game TEXT NOT NULL, -- 'word' | 'wordsearch'
|
||||
@@ -310,6 +319,30 @@ CREATE TABLE IF NOT EXISTS game_state (
|
||||
PRIMARY KEY (user_id, game, variant, puzzle_date)
|
||||
);
|
||||
|
||||
-- Bloom runtime word curation (no deploy needed). The accepted set is computed
|
||||
-- live as: broad dictionary ∪ {allow} − {block}. Admin-managed; one row per word.
|
||||
CREATE TABLE IF NOT EXISTS bloom_word_overrides (
|
||||
word TEXT PRIMARY KEY, -- lowercase
|
||||
action TEXT NOT NULL, -- 'allow' | 'block'
|
||||
reason TEXT,
|
||||
created_by TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Player "this should count" reports → admin queue (approve→allow / block / dismiss).
|
||||
CREATE TABLE IF NOT EXISTS bloom_word_reports (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
word TEXT NOT NULL, -- lowercase
|
||||
puzzle_date TEXT,
|
||||
mode TEXT, -- 'daily' | 'free'
|
||||
format TEXT, -- 'center' | 'wild'
|
||||
letters TEXT, -- the wheel's 7 letters (for context)
|
||||
reason TEXT, -- why it was rejected (e.g. 'not in the word list')
|
||||
status TEXT NOT NULL DEFAULT 'pending', -- 'pending' | 'approved' | 'blocked' | 'dismissed'
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_bloom_reports_status ON bloom_word_reports(status, created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_follows (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
@@ -327,6 +360,49 @@ CREATE TABLE IF NOT EXISTS digest_sends (
|
||||
sent_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (user_id, brief_date)
|
||||
);
|
||||
|
||||
-- Publishing Desk: a platform-NEUTRAL outbound-share record (X first; Bluesky /
|
||||
-- Threads / newsletter later reuse this). One row per (article, platform); the
|
||||
-- queue tops up without ever overwriting saved text/handles. opened != posted —
|
||||
-- Web Intents can't confirm a post, so the human confirms the terminal state.
|
||||
CREATE TABLE IF NOT EXISTS outbound_shares (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
article_id INTEGER NOT NULL REFERENCES articles(id) ON DELETE CASCADE,
|
||||
platform TEXT NOT NULL DEFAULT 'x',
|
||||
status TEXT NOT NULL DEFAULT 'queued', -- queued|drafting|opened|posted|skipped|snoozed
|
||||
social_score INTEGER, -- LLM "stop-scrolling" interest (0-10)
|
||||
rationale TEXT, -- why someone would stop scrolling
|
||||
talking_points TEXT, -- JSON array of factual points
|
||||
angle TEXT, -- a suggested conversational angle
|
||||
entities TEXT, -- JSON array of raw named entities (LLM-extracted)
|
||||
suggested_handles TEXT, -- JSON array of {handle, profile_url, via}
|
||||
draft_text TEXT, -- autosaved in-progress blurb (the human writes it)
|
||||
final_text TEXT, -- what was actually posted (teaches voice later)
|
||||
share_url TEXT, -- the exact /a/{id}?utm... link used
|
||||
post_url TEXT, -- the resulting tweet URL, if captured
|
||||
snooze_until TEXT, -- 'not right now' (re-eligible after this)
|
||||
opened_at TEXT,
|
||||
posted_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (article_id, platform)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_outbound_shares_status ON outbound_shares(platform, status);
|
||||
|
||||
-- Verified handle directory — the LLM only ever proposes NAMES; the @handle comes
|
||||
-- only from here (or a source's own x_handle). Aliases resolve consistently by each
|
||||
-- having its own row pointing at the same handle (e.g. "Johns Hopkins University"
|
||||
-- and "Johns Hopkins").
|
||||
CREATE TABLE IF NOT EXISTS entity_handles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
entity_name TEXT NOT NULL, -- display name as entered
|
||||
normalized_name TEXT NOT NULL, -- lowercased/stripped match key
|
||||
platform TEXT NOT NULL DEFAULT 'x',
|
||||
handle TEXT NOT NULL, -- e.g. @AnthropicAI
|
||||
profile_url TEXT,
|
||||
verified_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (normalized_name, platform)
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
@@ -359,7 +435,7 @@ def _migrate(conn: sqlite3.Connection) -> None:
|
||||
need an explicit, idempotent ALTER guarded by the current column set.
|
||||
"""
|
||||
score_cols = {row["name"] for row in conn.execute("PRAGMA table_info(article_scores)")}
|
||||
for column in ("topic", "flavor"):
|
||||
for column in ("topic", "flavor", "language"):
|
||||
if column not in score_cols:
|
||||
conn.execute(f"ALTER TABLE article_scores ADD COLUMN {column} TEXT")
|
||||
|
||||
@@ -397,6 +473,9 @@ def _migrate(conn: sqlite3.Connection) -> None:
|
||||
for column, decl in health_columns.items():
|
||||
if column not in source_cols:
|
||||
conn.execute(f"ALTER TABLE sources ADD COLUMN {column} {decl}")
|
||||
# Publishing Desk: the source's own verified X handle (suggested when sharing).
|
||||
if "x_handle" not in source_cols:
|
||||
conn.execute("ALTER TABLE sources ADD COLUMN x_handle TEXT")
|
||||
|
||||
# Lifecycle: status (active/paused/retired) + content_visible. `active` is
|
||||
# kept as a synced mirror so legacy code (scheduler/CLI) keeps working.
|
||||
|
||||
Reference in New Issue
Block a user