CREATE TABLE feeds ( id TEXT PRIMARY KEY, url TEXT NOT NULL UNIQUE, title TEXT NOT NULL, last_fetched_at TEXT ); CREATE TABLE articles ( id TEXT PRIMARY KEY, feed_id TEXT NOT NULL REFERENCES feeds(id), url TEXT NOT NULL UNIQUE, title TEXT NOT NULL, summary TEXT NOT NULL, content TEXT, published_at TEXT, fetched_at TEXT NOT NULL, topics TEXT NOT NULL DEFAULT '[]', -- JSON array of strings embedding TEXT, -- JSON array of floats embedding_score REAL, llm_score REAL, llm_rationale TEXT, final_score REAL, estimated_read_seconds INTEGER ); CREATE INDEX idx_articles_feed_id ON articles(feed_id); CREATE INDEX idx_articles_final_score ON articles(final_score); -- Append-only interaction log. Affinities and any future scoring model are -- derived from this; it is never mutated except to backfill dwell_seconds -- once known. CREATE TABLE reading_events ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL REFERENCES articles(id), occurred_at TEXT NOT NULL, outcome TEXT NOT NULL, -- 'impression' | 'opened' | 'starred' | 'dismissed' dwell_seconds INTEGER ); CREATE INDEX idx_reading_events_article_id ON reading_events(article_id); -- Single-row-per-user table (multi-user support can add a user_id column -- later); holds the current TopicAffinities snapshot as JSON so it doesn't -- need to be recomputed from the full event log on every request. Rebuild -- from reading_events at any time if the scoring model changes. CREATE TABLE topic_affinities ( user_id TEXT PRIMARY KEY DEFAULT 'default', affinities TEXT NOT NULL DEFAULT '{}', updated_at TEXT NOT NULL ); -- Free-text preferences the user writes explicitly (e.g. "I care about -- distributed systems and Rust internals, not general AI news"), embedded -- once and used as the anchor for the embedding-similarity stage. CREATE TABLE preferences ( user_id TEXT PRIMARY KEY DEFAULT 'default', description TEXT NOT NULL DEFAULT '', embedding TEXT, updated_at TEXT NOT NULL );