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
This commit is contained in:
+6
-275
@@ -1,277 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One-shot stack propagator (explicit user request 2026-07-12, rev 2.2).
|
||||
"""Thin wrapper — delegates to oracle.render (full publish, not dry-run)."""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
Editorial rules applied (reuses BUILT-IN pipeline functions, no pipeline edits):
|
||||
- clickability.compute_index / decay_index (virality rank + 18h decay)
|
||||
- generate_from_athena.clean_headline (repo-prefix trim, emoji strip, length cap)
|
||||
- generate_from_athena.add_prefix (Breaking | text prefix, BREAKING GATE)
|
||||
- render_site._clean_summary (one-liner descriptions on every card)
|
||||
|
||||
USER DIRECTIVES (2026-07-12):
|
||||
1. GitHub source EXCLUDED entirely (until further notice).
|
||||
2. 'update' green tier REMOVED. Only 'breaking' (rare real events) or 'normal'.
|
||||
3. Curated Picks section surfaces two flavors (tight deterministic phrase match,
|
||||
no broad keywords to avoid false positives):
|
||||
(a) QUIRKY + agents roasting their humans
|
||||
(b) people who BUILT / SHIPPED / EARNED from an AI product (indie hackers)
|
||||
Window: last DAYS days (default 4). Cap: LIMIT (default 200) — GitHub ban caps the
|
||||
real max at ~180 over 4 days; we render whatever is eligible (never fake count).
|
||||
"""
|
||||
import os, re, sys, json, sqlite3, html as _html
|
||||
from datetime import datetime as dt, timezone, timedelta
|
||||
from collections import OrderedDict
|
||||
|
||||
ORACLE = "/home/vpsadmin/oracle"
|
||||
sys.path.insert(0, ORACLE)
|
||||
sys.path.insert(0, "/home/vpsadmin/ai-oracle-site")
|
||||
import clickability as cb
|
||||
import render_site as rs
|
||||
import generate_from_athena as ga
|
||||
|
||||
DB = os.path.join(ORACLE, "oracle.db")
|
||||
WEBROOT = "/var/www/preprod3"
|
||||
FALLBACK = os.path.join(ORACLE, "site")
|
||||
NOW = dt.now(timezone.utc)
|
||||
DAYS = 14 # span whole DB so all 182 non-GitHub entries are eligible (DB only goes back ~7d)
|
||||
LIMIT = 200 # hard ceiling: DB only has 182 non-GitHub entries total, so 182 will render
|
||||
EXCLUDE_SOURCES = {"github"} # banned until further notice
|
||||
|
||||
# BREAKING GATE (verbatim pipeline logic; repos/papers/models never breaking)
|
||||
REPO_SOURCES = {"github", "gitlab", "huggingface", "arxiv"}
|
||||
IMPORTANCE = re.compile(
|
||||
r"\b(sues?|sue|lawsuit|launches?|launch|releases?|release|"
|
||||
r"bans?|ban|war|strikes?|attack|acquires?|acquisition|trillion|billions?|"
|
||||
r"layoffs?|declares?|emergency|outage|breach|stolen|steals?|theft|antitrust|"
|
||||
r"monopoly|reveals?|exposed|breakthrough|first|warns?|crackdown|shutdown|"
|
||||
r"GPT-?5|Claude|Gemini|OpenAI|Anthropic|Google|Apple|Microsoft|Meta|xAI|"
|
||||
r"Musk|Altman|Grok|DeepSeek|Llama|NVIDIA|AMD|FCC|EU|antitrust|"
|
||||
r"folded|spins? off|partners?|raises?|ipo|funding)\\b", re.I)
|
||||
BREAKING_PCT = 0.90
|
||||
|
||||
# --- CURATION: QUIRKY + agents roasting their humans ONLY (deterministic; no LLM) ---
|
||||
# Standing directive 2026-07-12 (end of session): "shipped & paid / built & earned"
|
||||
# was WALKED BACK ("looking for people who build and ship products is a whole
|
||||
# separate issue"). Do NOT bake it in. Curation = quirky + agents-roasting-humans.
|
||||
QUIRKY = [
|
||||
"hit piece", "roast", "roasting", "insult", "revenge", "betray",
|
||||
"bizarre", "weird", "cursed", "font humans", "brain region",
|
||||
"conspiracy", "haunted", "absurd", "unhinged", "sentient", "scream",
|
||||
"mock", "taunt", "expose their", "its human", "its user", "their owner",
|
||||
"about their", "their creator", "their master", "turned on", "backstab",
|
||||
"wrote about its", "turned against", "rebelled", "sassy", "savage",
|
||||
]
|
||||
# built / shipped / EARNED from an AI product (FIRST-PERSON builder only —
|
||||
# tight phrases; bare 'revenue'/'funding'/'ipo' EXCLUDED to avoid industry-news
|
||||
# false positives like TechCrunch "startups growing revenue").
|
||||
BUILT_SHIPPED = [
|
||||
"indie hacker", "i built", "i made", "i shipped", "i launched", "i sold",
|
||||
"my saas", "my startup", "my app", "my product", "my business",
|
||||
"side project", "bootstrapped", "profitable", "paying customers",
|
||||
"made money", "earn money", "mrr", "monthly recurring", "i run a",
|
||||
"made me $", "income from", "subscriptions", "sold my", "quit my job",
|
||||
"shipped a", "built a", "customers pay", "my first", "passive income",
|
||||
]
|
||||
|
||||
|
||||
def _parse(ts):
|
||||
if not ts:
|
||||
return None
|
||||
try:
|
||||
return dt.fromisoformat(ts.replace("Z", "+00:00"))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _curation(it):
|
||||
blob = f"{(it.get('title') or '')} {(rs._clean_summary(it.get('summary') or ''))}".lower()
|
||||
if any(k in blob for k in BUILT_SHIPPED):
|
||||
return ("built", 1.22)
|
||||
if any(k in blob for k in QUIRKY):
|
||||
return ("quirky", 1.16)
|
||||
return (None, 1.0)
|
||||
|
||||
|
||||
def main():
|
||||
conn = sqlite3.connect(f"file:{DB}?mode=ro", uri=True)
|
||||
items = cb.fetch_items(conn)
|
||||
conn.close()
|
||||
items = cb.compute_index(items)
|
||||
items = cb.decay_index(items, rs.HALF_LIFE_H)
|
||||
|
||||
cutoff = NOW - timedelta(days=DAYS)
|
||||
eligible = [it for it in items
|
||||
if it.get("title") and it.get("url")
|
||||
and it.get("first_seen") and _parse(it["first_seen"])
|
||||
and _parse(it["first_seen"]) >= cutoff
|
||||
and (it.get("source") or "").lower() not in EXCLUDE_SOURCES]
|
||||
# --- RECENCY GUARD (2026-07-13, Tony's correction): NEVER re-post old news.
|
||||
# Age is the dominant measure: today's items ALWAYS lead (week-open fresh
|
||||
# news); older items keep only if NEVER posted before (md-stack / seen).
|
||||
# This kills the Apple-vs-OpenAI / GPT-5.6 re-post problem at the source. ---
|
||||
from recency_guard import filter_fresh as _rg_filter
|
||||
_today, _older_new, _dropped = _rg_filter(eligible)
|
||||
if _dropped:
|
||||
print(f"[recency_guard] dropped {len(_dropped)} already-posted older items")
|
||||
eligible = _today + _older_new
|
||||
eligible.sort(key=lambda x: x["clickability_decayed"], reverse=True)
|
||||
top = eligible[:LIMIT]
|
||||
|
||||
scores = [it["clickability_decayed"] for it in top]
|
||||
n = len(scores)
|
||||
|
||||
def pct_rank(v):
|
||||
beaten = sum(1 for s in scores if s <= v)
|
||||
return beaten / n if n else 0.0
|
||||
|
||||
for it in top:
|
||||
src = (it.get("source") or "").lower()
|
||||
pr = pct_rank(it["clickability_decayed"])
|
||||
is_repo = src in REPO_SOURCES
|
||||
important = bool(IMPORTANCE.search(it.get("title") or ""))
|
||||
if (not is_repo) and important and pr >= BREAKING_PCT:
|
||||
tier = "breaking"
|
||||
else:
|
||||
tier = "normal"
|
||||
cleaned = ga.clean_headline(it["title"], it.get("source", ""))
|
||||
it["title"] = ga.add_prefix(cleaned, it["url"], tier)
|
||||
it["_tier"] = tier
|
||||
label, mult = _curation(it)
|
||||
it["_curated"] = label
|
||||
it["clickability_decayed"] = it["clickability_decayed"] * mult
|
||||
|
||||
ranked = sorted(top, key=lambda x: x["clickability_decayed"], reverse=True)
|
||||
fresh = [it for it in ranked if it.get("fresh")]
|
||||
top_cards = fresh[:rs.TOP_N]
|
||||
stack = [it for it in ranked if it not in top_cards]
|
||||
curated = [it for it in ranked if it.get("_curated")]
|
||||
curated.sort(key=lambda x: x["clickability_decayed"], reverse=True)
|
||||
curated_cards = curated[:12]
|
||||
|
||||
by_day = OrderedDict()
|
||||
for it in stack:
|
||||
day = (it.get("first_seen") or "")[:10] or "unknown"
|
||||
by_day.setdefault(day, []).append(it)
|
||||
|
||||
def card(it):
|
||||
title = _html.escape(it["title"] or "(untitled)")
|
||||
url = _html.escape(it["url"] or "#")
|
||||
src = _html.escape(it["source"])
|
||||
sig = it.get("signal_score") or 0
|
||||
t = rs._fmt_time(it.get("first_seen"))
|
||||
summary = _html.escape(rs._clean_summary(it.get("summary") or "")[:200])
|
||||
cls = "card"
|
||||
if it.get("_tier") == "breaking":
|
||||
cls += " breaking"
|
||||
if it.get("_curated"):
|
||||
cls += " curated"
|
||||
badge = ""
|
||||
if it.get("_curated") == "built":
|
||||
badge = '<span class="badge built">\U0001f4b0 Built & Earned</span>'
|
||||
elif it.get("_curated") == "quirky":
|
||||
badge = '<span class="badge quirky">\U0001f300 Quirky</span>'
|
||||
sum_html = f'<p class="summary">{summary}</p>' if summary else ""
|
||||
return f"""
|
||||
<article class="{cls}" data-src="{src}">
|
||||
<div class="meta"><span class="src">{src}</span>
|
||||
<span class="time">{t}</span>
|
||||
<span class="sig">sig {sig:.1f}</span>
|
||||
{badge}
|
||||
<span class="score">\U0001f525 {it['clickability_decayed']:.2f}</span></div>
|
||||
<h3><a href="{url}" target="_blank" rel="noopener">{title}</a></h3>
|
||||
{sum_html}
|
||||
</article>"""
|
||||
|
||||
top_html = "".join(card(it) for it in top_cards)
|
||||
curated_html = "".join(card(it) for it in curated_cards)
|
||||
stack_html = ""
|
||||
for day, rows in by_day.items():
|
||||
rows.sort(key=lambda x: x["clickability_decayed"], reverse=True)
|
||||
cards = "".join(card(it) for it in rows)
|
||||
stack_html += f"""
|
||||
<h3 class="day">\U0001f4c5 {day}</h3>
|
||||
<div class="stack">{cards}</div>"""
|
||||
|
||||
now_str = NOW.strftime("%Y-%m-%d %H:%M UTC")
|
||||
page = f"""<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Athena AI News — Ranked by Clickability</title>
|
||||
<style>
|
||||
:root {{ --bg:#0b0e14; --card:#141925; --fg:#e6e9ef; --mut:#8b93a7; --acc:#5b8cff; }}
|
||||
* {{ box-sizing:border-box; }}
|
||||
body {{ margin:0; background:var(--bg); color:var(--fg);
|
||||
font:15px/1.5 -apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif; }}
|
||||
header {{ padding:28px 20px 14px; border-bottom:1px solid #1f2533; text-align:center; }}
|
||||
header h1 {{ margin:0; font-size:28px; letter-spacing:.5px; }}
|
||||
header .sub {{ color:var(--mut); font-size:13px; margin-top:6px; }}
|
||||
main {{ max-width:1000px; margin:0 auto; padding:20px; }}
|
||||
h2.sech {{ font-size:18px; margin:26px 0 12px; border-left:3px solid var(--acc); padding-left:10px; }}
|
||||
.grid {{ display:grid; grid-template-columns:repeat(auto-fill,minmax(320px,1fr)); gap:14px; }}
|
||||
.card {{ background:var(--card); border:1px solid #1f2533; border-radius:12px; padding:16px; }}
|
||||
.card.breaking {{ border-left:3px solid #ff5b5b; }}
|
||||
.card.curated {{ border-left:3px solid #ffcf5b; background:#1a160c; }}
|
||||
.meta {{ display:flex; gap:8px; align-items:center; font-size:12px; color:var(--mut); flex-wrap:wrap; }}
|
||||
.src {{ background:#1f2533; padding:2px 8px; border-radius:20px; text-transform:uppercase; }}
|
||||
.badge {{ padding:1px 8px; border-radius:10px; font-size:11px; font-weight:600; }}
|
||||
.badge.built {{ background:#ffcf5b; color:#1a160c; }}
|
||||
.badge.quirky {{ background:#b98cff; color:#150c1f; }}
|
||||
.score {{ color:#ff9d5b; font-weight:600; margin-left:auto; }}
|
||||
.card h3 {{ font-size:16px; margin:10px 0 8px; line-height:1.35; }}
|
||||
.card h3 a {{ color:var(--fg); text-decoration:none; }}
|
||||
.card h3 a:hover {{ color:var(--acc); }}
|
||||
.summary {{ color:var(--mut); font-size:13px; margin:0; }}
|
||||
.day {{ font-size:15px; color:var(--mut); margin:28px 0 10px; border-bottom:1px solid #1f2533; padding-bottom:6px; }}
|
||||
.stack {{ display:grid; grid-template-columns:repeat(auto-fill,minmax(320px,1fr)); gap:12px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Athena AI News</h1>
|
||||
<div class="sub">Auto-ranked by Clickability Index · {len(top)} stories (4-day window, GitHub excluded) · curated: quirky + agents roasting their humans · generated {now_str}</div>
|
||||
</header>
|
||||
<main>
|
||||
<h2 class="sech">\U0001f4b0\U0001f300 Curated Picks — Built & Earned · Quirky · Agents Roasting Their Humans</h2>
|
||||
<div class="grid">{curated_html}</div>
|
||||
<h2 class="sech">\U0001f534 Top News</h2>
|
||||
<div class="grid">{top_html}</div>
|
||||
<h2 class="sech">\U0001f4f0 The Stack</h2>
|
||||
{stack_html}
|
||||
</main>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
target = WEBROOT if os.path.isdir(WEBROOT) else FALLBACK
|
||||
os.makedirs(target, exist_ok=True)
|
||||
with open(os.path.join(target, "index.html"), "w") as f:
|
||||
f.write(page)
|
||||
with open(os.path.join(target, "feed.json"), "w") as f:
|
||||
json.dump([
|
||||
{"title": i["title"], "url": i["url"], "source": i["source"],
|
||||
"tier": i.get("_tier"), "curated": i.get("_curated"),
|
||||
"clickability_decayed": round(i["clickability_decayed"], 3),
|
||||
"age_hours": i["age_hours"], "first_seen": i.get("first_seen")}
|
||||
for i in ranked
|
||||
], f, indent=2)
|
||||
|
||||
where = "WEBROOT(/var/www/preprod3)" if target == WEBROOT else "FALLBACK(~oracle/site)"
|
||||
tiers = {"breaking": 0, "normal": 0}
|
||||
for it in top:
|
||||
tiers[it["_tier"]] += 1
|
||||
cc = {"built": 0, "quirky": 0, "none": 0}
|
||||
for it in top:
|
||||
cc[it["_curated"] or "none"] += 1
|
||||
with_desc = sum(1 for it in top if rs._clean_summary(it.get("summary") or ""))
|
||||
print(f"[propagate v2.2] wrote {target}/index.html + feed.json")
|
||||
print(f" target : {where}")
|
||||
print(f" window : last {DAYS} days, GitHub EXCLUDED")
|
||||
print(f" eligible : {len(eligible)} (cap {LIMIT} -> rendered {len(top)})")
|
||||
print(f" tiers : {tiers['breaking']} breaking / {tiers['normal']} normal (update tier REMOVED)")
|
||||
print(f" curated flags : {cc['built']} built&earned | {cc['quirky']} quirky | {cc['none']} none")
|
||||
print(f" curated shown : top {len(curated_cards)} in Curated Picks section")
|
||||
print(f" with desc : {with_desc}/{len(top)} cards have a one-liner description")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
from oracle.cli import main as cli_main
|
||||
sys.argv = ["oracle", "render"]
|
||||
cli_main()
|
||||
|
||||
Reference in New Issue
Block a user