"""Multi-variant edition engine for Athena. One data pipeline → multiple audience-specific editions. Each variant is a YAML config that defines filters, ranking, and display. World Monitor pattern: 6 variants from 1 codebase. """ import os import sqlite3 import yaml from datetime import datetime, timezone from pathlib import Path from typing import Optional from oracle.config import ROOT, DB_PATH, VERDICT_THRESHOLDS # ── Variant config defaults ──────────────────────────────────────────────── DEFAULT_CONFIG = { "name": "Athena AI News", "description": "", "output": "index.html", "filters": { "verdicts": ["PUBLISH", "WATCH", "ARCHIVE", "DROP"], "sources": [], "min_score": 0, "max_age_h": 0, "max_items": 0, }, "ranking": { "by": "clickability", "half_life_h": 18, }, "display": { "top_n": 8, "show_summary": True, "show_score": True, "show_tier": False, "show_verdict": False, "theme": "dark", "accent": "#5b8cff", "logo": "🏛️", }, } def load_variant(name: str) -> dict: """Load a variant config by name, merging with defaults. Looks in variants/.yaml relative to project root. Returns merged config dict. """ variants_dir = ROOT / "variants" variant_file = variants_dir / f"{name}.yaml" if not variant_file.exists(): available = [p.stem for p in sorted(variants_dir.glob("*.yaml"))] raise FileNotFoundError( f"Variant '{name}' not found. Available: {', '.join(available)}" ) with open(variant_file) as f: raw = yaml.safe_load(f) or {} # Deep merge with defaults config = _deep_merge(DEFAULT_CONFIG, raw) config["_name"] = name config["_file"] = str(variant_file) return config def list_variants() -> list[str]: """List all available variant names.""" variants_dir = ROOT / "variants" if not variants_dir.exists(): return [] return sorted(p.stem for p in variants_dir.glob("*.yaml")) def apply_filters(conn: sqlite3.Connection, variant: dict) -> list[dict]: """Fetch entries from DB filtered by variant config. Returns list of entry dicts matching the variant's filter criteria. """ f = variant["filters"] # Build query clauses = [] params = [] # Verdict filter if f.get("verdicts"): verdicts = [v for v in f["verdicts"] if v] # strip empties if verdicts: placeholders = ", ".join("?" for _ in verdicts) clauses.append(f"verdict IN ({placeholders})") params.extend(verdicts) # Source filter (empty = all) if f.get("sources"): sources = [s for s in f["sources"] if s] if sources: placeholders = ", ".join("?" for _ in sources) clauses.append(f"source IN ({placeholders})") params.extend(sources) # Min signal score if f.get("min_score", 0) > 0: clauses.append("COALESCE(signal_score, 0) >= ?") params.append(f["min_score"]) # Max age if f.get("max_age_h", 0) > 0: max_age = f["max_age_h"] cutoff = datetime.now(timezone.utc).timestamp() - (max_age * 3600) clauses.append("first_seen >= datetime(?, 'unixepoch')") params.append(cutoff) # Base query sql = "SELECT * FROM entries" if clauses: sql += " WHERE " + " AND ".join(clauses) sql += " ORDER BY first_seen DESC" # Max items if f.get("max_items", 0) > 0: sql += f" LIMIT {int(f['max_items'])}" conn.row_factory = sqlite3.Row cur = conn.cursor() cur.execute(sql, params) rows = cur.fetchall() return [dict(r) for r in rows] def rank_items(items: list[dict], variant: dict) -> list[dict]: """Rank items according to variant ranking config. Supports: clickability, signal_score, verdict_priority, freshness """ ranking = variant["ranking"] by = ranking.get("by", "clickability") half_life = ranking.get("half_life_h", 18) if by == "clickability": # Use clickability module for decayed scoring from oracle.clickability import compute_index, decay_index items = compute_index(items) items = decay_index(items, half_life) items.sort(key=lambda x: x.get("clickability_decayed", 0), reverse=True) elif by == "signal_score": items.sort(key=lambda x: float(x.get("signal_score") or 0), reverse=True) elif by == "verdict_priority": # PUBLISH > WATCH > ARCHIVE > DROP priority = {"PUBLISH": 0, "WATCH": 1, "ARCHIVE": 2, "DROP": 3} items.sort(key=lambda x: ( priority.get(x.get("verdict", "DROP"), 4), float(x.get("signal_score") or 0), ), reverse=False) elif by == "freshness": items.sort(key=lambda x: x.get("first_seen", ""), reverse=True) else: # Default fallback: signal score items.sort(key=lambda x: float(x.get("signal_score") or 0), reverse=True) return items def render_variant(variant: dict, dry_run: bool = False, webroot: Optional[str] = None): """Full render pipeline for one variant. Load config → filter → rank → render HTML. Returns (output_path, item_count). """ from oracle.render import render_variant_html, render_variant_json conn = sqlite3.connect(str(DB_PATH)) conn.row_factory = sqlite3.Row items = apply_filters(conn, variant) items = rank_items(items, variant) conn.close() if not items: print(f"[variant:{variant['_name']}] No items matched filters") return None, 0 page = render_variant_html(items, variant) # Determine output path output_rel = variant.get("output", "index.html") if dry_run: out_dir = ROOT / "_preview" / variant["_name"] out_dir.mkdir(parents=True, exist_ok=True) # Use just the filename for dry-run (output_rel may have subdirs) dry_filename = os.path.basename(output_rel) out_path = str(out_dir / dry_filename) with open(out_path, "w") as f: f.write(page) # Also write JSON feed json_path = str(out_dir / "feed.json") render_variant_json(items, variant, json_path) display = variant["display"] top_n = display.get("top_n", 8) top = items[:top_n] print(f"[variant:{variant['_name']}] wrote {out_path} ({len(items)} items)") print(f" TOP {top_n}:") for i, it in enumerate(top, 1): score = f" sig={it.get('signal_score', 0):.1f}" if display.get("show_score") else "" tier = f" tier={it.get('source_tier', '?')}" if display.get("show_tier") else "" verdict = f" [{it.get('verdict', '?')}]" if display.get("show_verdict") else "" print(f" {i}. {it['title'][:60]}{score}{tier}{verdict}") return out_path, len(items) # Production render target = webroot or str(ROOT / "site") out_path = os.path.join(target, output_rel) os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True) with open(out_path, "w") as f: f.write(page) # JSON feed alongside json_path = os.path.join(os.path.dirname(out_path), "feed.json") render_variant_json(items, variant, json_path) print(f"[variant:{variant['_name']}] wrote {out_path} ({len(items)} items)") return out_path, len(items) # ── Helpers ──────────────────────────────────────────────────────────────── def _deep_merge(base: dict, override: dict) -> dict: """Recursively merge override into base dict.""" merged = base.copy() for key, value in override.items(): if key in merged and isinstance(merged[key], dict) and isinstance(value, dict): merged[key] = _deep_merge(merged[key], value) else: merged[key] = value return merged