Files
athena-oracle/oracle/recency.py
T
Epictetus 07c5f9a5c2 Sprint 0+1: Package restructure, source tiers, verdicts, multi-variant editions
- New oracle/ package (11 modules) with unified CLI (python -m oracle)
- Source tiers: Tier 1 (arxiv/github/hf), Tier 2 (rss/hn), Tier 3 (reddit)
- Composite verdicts: PUBLISH/WATCH/ARCHIVE/DROP based on signal score + age
- Content-hash dedup: SHA-256[:16] normalized, atomic at insert time
- Multi-variant editions: 4 YAML configs (default/research/devops/brief)
- Variant engine: filter → rank → render (HTML + JSON, themed)
- Per-adapter timeout (10s) + threading fallback
- Consolidated 12 root scripts → thin wrappers + oracle/ package
- Archived stale scripts (_engagement, _live_compare, reddit_proof)
- Updated .gitignore, README.md, schema.sql
2026-07-22 13:32:15 +00:00

141 lines
4.1 KiB
Python

"""Recency guard — Athena "how old is this news?" gate.
Age is the dominant gate. TODAY's items are always eligible.
Older items are eligible ONLY if never posted before.
"""
import json
import os
import re
from datetime import datetime, timezone
from oracle.config import SEEN_JSON
ORACLE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def _parse(ts):
if not ts:
return None
try:
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
except Exception:
return None
def _norm_url(u):
if not u:
return ""
return u.split("?")[0].split("#")[0].rstrip("/").lower()
def _norm_title(t):
if not t:
return ""
t = t.lower()
t = re.sub(r"[^a-z0-9 ]", " ", t)
return re.sub(r"\s+", " ", t).strip()[:60]
def load_posted(md_dir: str = ORACLE_DIR, seen_json: str = str(SEEN_JSON)):
"""Return (md_urls:set, md_titles:set, seen_urls:set)."""
md_urls, md_titles, seen_urls = set(), set(), set()
for fn in sorted(os.listdir(md_dir)):
if re.match(r"athena_top.*\.md$", fn):
try:
txt = open(os.path.join(md_dir, fn), encoding="utf-8", errors="replace").read()
except OSError:
continue
for m in re.findall(r"\]\((https?://[^)\s]+)\)", txt):
nu = _norm_url(m)
if nu:
md_urls.add(nu)
for t in re.findall(r"^\|\s*\d+\s*\|\s*(.+?)\s*\|", txt, re.M):
nt = _norm_title(t)
if nt:
md_titles.add(nt)
if os.path.exists(seen_json):
try:
with open(seen_json, encoding="utf-8") as f:
for u in json.load(f):
nu = _norm_url(u)
if nu:
seen_urls.add(nu)
except (json.JSONDecodeError, OSError):
pass
return md_urls, md_titles, seen_urls
def is_today(first_seen, now=None):
now = now or datetime.now(timezone.utc)
d = _parse(first_seen)
return bool(d) and d.strftime("%Y-%m-%d") == now.strftime("%Y-%m-%d")
def age_days(first_seen, now=None):
now = now or datetime.now(timezone.utc)
d = _parse(first_seen)
if not d:
return 9999.0
return max((now - d).total_seconds() / 86400.0, 0.0)
def day_bucket(first_seen, now=None):
days = age_days(first_seen, now)
if days < 1:
return "today"
if days < 2:
return "yesterday"
if days <= 6:
return "this-week"
return "older"
def already_posted(url, title, first_seen, now=None,
md_urls=None, md_titles=None, seen_urls=None):
"""Age-aware dedup. Today's items are NEVER flagged."""
if md_urls is None or md_titles is None or seen_urls is None:
md_urls, md_titles, seen_urls = load_posted()
if _norm_url(url) in md_urls:
return True
nt = _norm_title(title)
if nt and nt in md_titles:
return True
if is_today(first_seen, now):
return False
if _norm_url(url) in seen_urls:
return True
return False
def recency_weight(first_seen, now=None, half_life_days=2.0):
"""1.0 for today, decays ~halving every 2 days."""
return 0.5 ** (age_days(first_seen, now) / half_life_days)
def blend_score(item, now=None):
"""clickability_decayed * recency_weight."""
base = item.get("clickability_decayed", 0) or 0
return base * recency_weight(item.get("first_seen"), now)
def filter_fresh(items, now=None, recent_window_days=4.0):
"""Split into (today_items, older_new_items, dropped_items)."""
now = now or datetime.now(timezone.utc)
md_urls, md_titles, seen_urls = load_posted()
today_items, older_new, dropped = [], [], []
for it in items:
fs = it.get("first_seen")
if is_today(fs, now):
today_items.append(it)
continue
posted = already_posted(it.get("url"), it.get("title"), fs, now,
md_urls, md_titles, seen_urls)
if posted and age_days(fs, now) > recent_window_days:
dropped.append(it)
else:
older_new.append(it)
return today_items, older_new, dropped