Files
athena-oracle/athena/scoring.py
T
Epictetus ed6b7475dc Sprint1: pure-rule bucket classifier + scorer (taste before intelligence)
- athena/scoring.py: deterministic bucket taxonomy (SHIPPING, LOCAL AI,
  PROBLEM SOLVED, MODEL RELEASE, RESEARCH, BUSINESS, INFRASTRUCTURE,
  CULTURE, UNCATEGORIZED) + component scores (shipping/utility/
  replication/enthusiast/novelty) with hype_penalty. No embeddings/LLM.
- Idempotent schema migration: 12 new columns incl actionability_score (reserved).
- attach_scoring() wired into pipeline.py AFTER store_entries, BEFORE render.
- Backfilled all existing rows; verified at box: null->scored via live run.
- Review report exposes every fired rule (editorial proof, not accuracy metric).
- Human-review tally (Published/Rejected/Borderline) is manual only.

Decision: founder directive 2026-07-15 — discover taxonomy before adding
intelligence. Sprint 2 (Lens) blocked until manual review completes.
2026-07-15 04:18:21 +00:00

546 lines
21 KiB
Python

#!/usr/bin/env python3
"""
athena/scoring.py — Sprint 1: Pure Rule-Based Bucket Classifier + Scorer.
DESIGN CONSTRAINT (founder directive, 2026-07-15):
Pure rules only. No embeddings, no semantic similarity, no LLM classification.
We are still discovering the editorial taxonomy. Deterministic systems are
easier to debug; misclassifications are signal; we must be able to explain
WHY every story landed where it did before we add intelligence.
Pipeline position:
ingestion/dedup (pipeline.py) -> [ATTACH HERE] -> rendering
Call attach_scoring(conn) immediately after store_entries and before render.
Schema migration (idempotent):
bucket, shipping_score, utility_score, replication_score, enthusiast_score,
novelty_score, hype_penalty, final_score, actionability_score,
narrative_id, topic_id, relation_json
Review report: human-readable, per-bucket, exposes every fired rule.
Usage:
python3 athena/scoring.py --migrate # add columns
python3 athena/scoring.py --backfill # score all unscored rows
python3 athena/scoring.py --review # write review report
python3 athena/scoring.py --all # migrate + backfill + review
"""
import argparse
import json
import os
import re
import sqlite3
from datetime import datetime, timezone
HERE = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(os.path.dirname(HERE), "oracle.db") # ~/oracle/oracle.db
# ---------------------------------------------------------------------------
# SCORE WEIGHTS (transparent, tunable in one place)
# ---------------------------------------------------------------------------
WEIGHTS = {
"shipping": 0.20, # a working artifact exists
"utility": 0.20, # clear practical use for an enthusiast
"replication": 0.25, # can a reader reproduce/run it locally
"enthusiast": 0.20, # signals a builder/DIY practitioner audience
"novelty": 0.15, # new, specific, non-generic
}
HYPE_CAP = 0.45 # final_score is floored if hype penalty is high
# ---------------------------------------------------------------------------
# BUCKETS + PURE RULES
# Each bucket: keyword hits and/or source constraints. First match wins,
# evaluated in BUCKET_ORDER (most specific taxon first).
# ---------------------------------------------------------------------------
# Keyword sets (lowercased; matched against title + summary + tags text).
KW_SHIPPING = [
"released", "launch", "v1.0", "v2.0", "v3.0", "shipping", "now available",
"open source", "open-source", "open weights", "weights released", "live now",
"beta", "public beta", "ga release", "general availability", "ships", "deployed",
"production", "now in", "available today", "download", "gradio", "demo", "playground",
]
KW_LOCAL_AI = [
"local llm", "local model", "local ai", "run locally", "run it locally", "on-device",
"on device", "ollama", "llama.cpp", "llamacpp", "gguf", "ggml", "lm studio",
"consumer hardware", "consumer gpu", "rtx", "your own gpu", "offline", "private ai",
"local-only", "self-host", "self-hosted", "home server", "edge device", "edge inference",
"quantized", "quantization", "q4", "q8", "int4", "fp16", "fine-tune at home",
"train at home", "local inference", "local deployment", "no api", "no cloud",
]
KW_PROBLEM_SOLVED = [
"how to", "how i", "solved", "fix", "fixed", "workaround", "benchmark", "improves",
"improved", "speedup", "speed-up", "reduces", "reduce", "cut", "cuts", "boost",
"optimize", "optimized", "optimisation", "faster", "3x", "10x", "2x", "latency",
"throughput", "roi", "cost", "cheaper", "save", "saves", "eliminate", "eliminated",
"from 117s to 30s", "p95", "memory usage", "vram", "token cost", "bottleneck",
"case study", "results", "we measured", "we tested", "showdown", "comparison",
]
KW_MODEL_RELEASE = [
"releases", "released", "unveils", "introduces", "new model", "new flagship",
"gpt-", "claude", "gemini", "llama", "mistral", "qwen", "deepseek", "grok",
"phi-", "command-r", "api access", "weights", "open model", "open-models",
"frontier", "checkpoint", "fine-tune", "finetune", "rl-trained", "rl train",
"trained", "post-training", "post training", "distilled", "distillation",
]
KW_RESEARCH = [
"paper", "arxiv", "preprint", "study", "research", "we propose", "we present",
"we introduce", "we show", "method", "framework", "theorem", "analysis of",
"survey", "benchmark", "dataset", "neural", "transformer", "diffusion",
"gradient", "ablation", "we find", "our approach", "novel", "state-of-the-art",
"sota", "cs.lg", "cs.cl", "cs.cv", "cs.ai",
]
KW_BUSINESS = [
"raises", "raised", "$", "valuation", "series a", "series b", "funding", "round",
"ipo", "acquisition", "acquires", "merger", "deal", "revenue", "layoff", "hiring",
"partnership", "invests", "investment", "market", "vc", "compute deal",
"billion", "million", "forecast", "miss", "earnings", "stock",
]
KW_INFRA = [
"gpu", "tpu", "data center", "datacenter", "data centre", "cluster", "cuda",
"rocm", "vllm", "tensorrt", "inference server", "serving", "kubernetes", "docker",
"pipeline", "mlops", "ci/cd", "rag", "vector db", "vector database", "agent",
"agents", "orchestration", "observability", "evaluation", "eval", "guardrail",
"safety", "red team", "jailbreak", "prompt injection", "fine-tuning stack",
]
KW_CULTURE = [
"says", "argues", "opinion", "essay", "think", "thinks", "the real", "why we",
"the future of", "dystopia", "utopia", "philosophy", "ethics", "regulation",
"policy", "ban", "lawsuit", "eu", "senate", "congress", "interview", "podcast",
"controversy", "controversial", "debate", "debate", "critic", "criticism",
"creepy", "creeping", "not sexy", "vibe", "hot take", "unpopular",
]
# Buckets evaluated in this order (specific -> generic). source_whitelist matches
# raw `source` value exactly; if present, story must come from one of those sources.
BUCKETS = {
"SHIPPING": {
"kw": KW_SHIPPING,
"source_whitelist": None,
"order": 0,
},
"LOCAL AI": {
"kw": KW_LOCAL_AI,
"source_whitelist": None,
"order": 1,
},
"PROBLEM SOLVED": {
"kw": KW_PROBLEM_SOLVED,
"source_whitelist": None,
"order": 2,
},
"MODEL RELEASE": {
"kw": KW_MODEL_RELEASE,
"source_whitelist": None,
"order": 3,
},
"RESEARCH": {
"kw": KW_RESEARCH,
"source_whitelist": ["arxiv"],
"order": 4,
},
"BUSINESS": {
"kw": KW_BUSINESS,
"source_whitelist": None,
"order": 5,
},
"INFRASTRUCTURE": {
"kw": KW_INFRA,
"source_whitelist": None,
"order": 6,
},
"CULTURE": {
"kw": KW_CULTURE,
"source_whitelist": None,
"order": 7,
},
}
BUCKET_ORDER = sorted(BUCKETS.keys(), key=lambda b: BUCKETS[b]["order"])
# Hype / low-signal penalty terms
HYPE_TERMS = [
"revolutionary", "game-changing", "game changer", "breakthrough", "mind-blowing",
"insane", "crazy", "unbelievable", "shocking", "the future is here", "omg",
"you won't believe", "secret", "they don't want you to know", "leaked", "viral",
"hype", "buzzword", "disrupt", "disrupting everything", "ai will replace",
"will change everything", "paradigm shift", "godlike", "magic", "miracle",
]
# Enthusiast-audience signals (builder / DIY / practitioner)
ENTHUSIAST_SIGNALS = [
"github", "repo", "repository", "self-host", "local", "ollama", "llamacpp",
"hugging face", "huggingface", "colab", "notebook", "pip install", "docker",
"cli", "open source", "open-source", "diy", "build your own", "tutorial",
"how to", "implementation", "agent", "agents", "fine-tune", "finetune",
"quantiz", "vllm", "rtx", "gpu", "consumer", "homelab", "self-hosted",
"machine-learning", "machine learning", "deep learning", "python", "rust",
"benchmark", "reproduc", "weights", "gguf",
]
# Source -> enthusiast affinity bonus
SOURCE_ENTHUSIAST_BONUS = {
"github": 0.20, "huggingface": 0.20, "arxiv": 0.10,
"hackernews": 0.10, "reddit": 0.05, "rss": 0.0,
}
def _norm(text):
if not text:
return ""
if isinstance(text, bytes):
text = text.decode("utf-8", "replace")
return " " + re.sub(r"\s+", " ", text.lower()) + " "
def classify(entry):
"""Pure rule classification.
Returns (bucket, matched_list) where matched_list is human-readable proof:
["kw:ollama", "kw:gguf", "src:huggingface", "tag:local"]
"""
title = _norm(entry.get("title") or "")
summary = _norm(_summary_text(entry.get("summary")))
tags_raw = entry.get("category_tags") or ""
try:
tags = " ".join(json.loads(tags_raw)) if tags_raw else ""
except Exception:
tags = tags_raw
tags = _norm(tags)
source = (entry.get("source") or "").lower()
haystack = title + " " + summary + " " + tags
matched = ["source=%s" % source]
best_bucket = "UNCATEGORIZED"
best_hits = 0
for bucket in BUCKET_ORDER:
spec = BUCKETS[bucket]
whitelist = spec["source_whitelist"]
if whitelist and source not in whitelist:
continue
hits = []
for kw in spec["kw"]:
kwn = " " + kw.lower() + " "
if kwn in haystack:
hits.append(kw)
if hits:
# record proof (cap displayed hits to keep report readable)
for h in hits[:8]:
matched.append("kw:%s" % h)
if len(hits) > best_hits:
best_hits = len(hits)
best_bucket = bucket
if best_bucket == "UNCATEGORIZED":
matched.append("(no rule fired)")
return best_bucket, matched
def _summary_text(raw):
if not raw:
return ""
try:
d = json.loads(raw)
if isinstance(d, dict):
return " ".join(str(v) for v in d.values() if isinstance(v, str))
except Exception:
pass
return raw
def score_entry(bucket, matched, entry):
"""Return dict of component scores (0..1) + final (0..1).
Components are deterministic functions of signals; see inline rationale.
"""
source = (entry.get("source") or "").lower()
title = _norm(entry.get("title") or "")
summary = _norm(_summary_text(entry.get("summary")))
tags_raw = entry.get("category_tags") or ""
try:
tags = " ".join(json.loads(tags_raw)) if tags_raw else ""
except Exception:
tags = tags_raw
tags = _norm(tags)
haystack = title + " " + summary + " " + tags
# --- enthusiast score ---
ent_hits = sum(1 for s in ENTHUSIAST_SIGNALS if (" " + s + " ") in haystack)
ent_base = min(ent_hits / 5.0, 1.0) # 5+ signals = full
ent_src = SOURCE_ENTHUSIAST_BONUS.get(source, 0.0)
enthusiast = min(ent_base + ent_src, 1.0)
# --- shipping score ---
ship_kw = [k for k in KW_SHIPPING if (" " + k + " ") in haystack]
shipping = 0.0
if bucket == "SHIPPING":
shipping = 0.9
elif ship_kw:
shipping = min(0.4 + 0.1 * len(ship_kw), 0.8)
# source artifacts (github/hf) imply something shippable exists
if source in ("github", "huggingface"):
shipping = max(shipping, 0.7)
# --- utility score ---
util_kw = [k for k in KW_PROBLEM_SOLVED if (" " + k + " ") in haystack]
utility = 0.0
if bucket == "PROBLEM SOLVED":
utility = 0.85
elif util_kw:
utility = min(0.4 + 0.1 * len(util_kw), 0.8)
if "github" in haystack or "huggingface" in haystack or "demo" in haystack:
utility = max(utility, 0.6)
# --- replication score (can a reader reproduce/run locally) ---
repl_kw = [k for k in KW_LOCAL_AI if (" " + k + " ") in haystack]
replication = 0.0
if bucket == "LOCAL AI":
replication = 1.0
elif repl_kw:
replication = min(0.5 + 0.1 * len(repl_kw), 0.9)
if source in ("github", "huggingface"):
replication = max(replication, 0.7)
if "open source" in haystack or "open-source" in haystack or "weights" in haystack:
replication = max(replication, 0.6)
# --- novelty score ---
nov_kw = ["new", "novel", "first", "breakthrough-method", "we propose",
"we introduce", "we present", "state-of-the-art", "sota", "unveils"]
novelty = 0.0
if bucket in ("RESEARCH", "MODEL RELEASE"):
novelty = 0.6
if any((" " + k + " ") in haystack for k in nov_kw):
novelty = min(novelty + 0.2, 0.9)
if bucket == "CULTURE":
novelty = min(novelty, 0.3) # opinion pieces are rarely novel technically
# --- hype penalty ---
hype_hits = [t for t in HYPE_TERMS if (" " + t + " ") in haystack]
hype_penalty = min(0.1 * len(hype_hits), 0.6)
# --- final ---
raw = (
WEIGHTS["shipping"] * shipping
+ WEIGHTS["utility"] * utility
+ WEIGHTS["replication"] * replication
+ WEIGHTS["enthusiast"] * enthusiast
+ WEIGHTS["novelty"] * novelty
)
final = max(raw - hype_penalty, 0.0)
final = min(final, 1.0)
return {
"shipping_score": round(shipping, 3),
"utility_score": round(utility, 3),
"replication_score": round(replication, 3),
"enthusiast_score": round(enthusiast, 3),
"novelty_score": round(novelty, 3),
"hype_penalty": round(hype_penalty, 3),
"final_score": round(final, 4),
}
# ---------------------------------------------------------------------------
# DB OPERATIONS
# ---------------------------------------------------------------------------
NEW_COLUMNS = [
"bucket TEXT",
"shipping_score REAL DEFAULT 0",
"utility_score REAL DEFAULT 0",
"replication_score REAL DEFAULT 0",
"enthusiast_score REAL DEFAULT 0",
"novelty_score REAL DEFAULT 0",
"hype_penalty REAL DEFAULT 0",
"final_score REAL DEFAULT 0",
"actionability_score REAL DEFAULT 0",
"narrative_id TEXT",
"topic_id TEXT",
"relation_json TEXT",
]
def migrate(db_path=DB_PATH):
"""Idempotent schema migration — only adds missing columns."""
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute("PRAGMA table_info(entries)")
existing = {row[1] for row in cur.fetchall()}
added = []
for col in NEW_COLUMNS:
name = col.split(" ")[0]
if name not in existing:
cur.execute("ALTER TABLE entries ADD COLUMN %s" % col)
added.append(name)
conn.commit()
conn.close()
print("[migrate] added columns: %s" % (", ".join(added) if added else "none (already present)"))
return added
def fetch_unscored(conn):
cur = conn.cursor()
cur.execute("""SELECT id, source, source_id, url, title, summary, category_tags,
raw_metadata FROM entries WHERE bucket IS NULL OR bucket = ''""")
cols = ["id", "source", "source_id", "url", "title", "summary",
"category_tags", "raw_metadata"]
return [dict(zip(cols, row)) for row in cur.fetchall()]
def attach_scoring(db_path=DB_PATH, dry_run=False):
"""Score every unscored entry. Call after ingestion/dedup, before render."""
conn = sqlite3.connect(db_path)
rows = fetch_unscored(conn)
print("[attach] scoring %d unscored entries" % len(rows))
for e in rows:
bucket, matched = classify(e)
scores = score_entry(bucket, matched, e)
if dry_run:
continue
conn.execute(
"""UPDATE entries SET bucket=?, shipping_score=?, utility_score=?,
replication_score=?, enthusiast_score=?, novelty_score=?,
hype_penalty=?, final_score=?, actionability_score=?,
narrative_id=?, topic_id=?, relation_json=? WHERE id=?""",
(bucket, scores["shipping_score"], scores["utility_score"],
scores["replication_score"], scores["enthusiast_score"],
scores["novelty_score"], scores["hype_penalty"], scores["final_score"],
0.0, # actionability_score: reserved, unused in Sprint 1
None, None, json.dumps({"matched_rules": matched}), e["id"]),
)
if not dry_run:
conn.commit()
conn.close()
print("[attach] done.")
# ---------------------------------------------------------------------------
# REVIEW REPORT
# ---------------------------------------------------------------------------
REVIEW_BUCKETS = ["SHIPPING", "LOCAL AI", "PROBLEM SOLVED", "MODEL RELEASE",
"RESEARCH", "BUSINESS", "INFRASTRUCTURE", "CULTURE",
"UNCATEGORIZED"]
def generate_review(db_path=DB_PATH, limit=200, out_path=None):
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute("""SELECT id, source, title, url, bucket, final_score,
relation_json, shipping_score, utility_score,
replication_score, enthusiast_score, novelty_score,
hype_penalty FROM entries
ORDER BY first_seen DESC LIMIT ?""", (limit,))
rows = cur.fetchall()
conn.close()
by_bucket = {b: [] for b in REVIEW_BUCKETS}
for r in rows:
(eid, src, title, url, bucket, final, rel_json, sh, ut, rp, en, no, hy) = r
try:
matched = json.loads(rel_json).get("matched_rules", []) if rel_json else []
except Exception:
matched = []
by_bucket.setdefault(bucket or "UNCATEGORIZED", []).append(
(eid, src, title, url, final, matched, (sh, ut, rp, en, no, hy))
)
lines = []
lines.append("=" * 70)
lines.append("ATHENA SPRINT 1 — MANUAL REVIEW REPORT")
lines.append("Generated: %s" % datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"))
lines.append("Stories reviewed: %d (most recent %d)" % (len(rows), limit))
lines.append("=" * 70)
lines.append("")
lines.append("HOW TO READ: For each story, the fired rules are listed as proof.")
lines.append("Bucket = first matching taxon (specific -> generic). Scores are")
lines.append("deterministic. 'Publish?' is for HUMAN review only — not algorithmic.")
lines.append("")
total_pub = total_rej = total_border = 0
for b in REVIEW_BUCKETS:
items = by_bucket.get(b, [])
if not items:
continue
lines.append(b)
lines.append("-" * len(b))
for (eid, src, title, url, final, matched, comps) in items:
sh, ut, rp, en, no, hy = comps
lines.append("")
lines.append("Story: %s" % (title or "(untitled)"))
lines.append(" id=%s source=%s final=%.3f" % (eid, src, final))
lines.append(" url: %s" % (url or ""))
lines.append(" Bucket: %s" % b)
lines.append(" Matched:")
for m in matched:
lines.append(" - %s" % m)
lines.append(" Score Components:")
lines.append(" Shipping: %.2f" % sh)
lines.append(" Utility: %.2f" % ut)
lines.append(" Replication: %.2f" % rp)
lines.append(" Enthusiast: %.2f" % en)
lines.append(" Novelty: %.2f" % no)
lines.append(" Hype: %.2f" % hy)
lines.append(" Final: %.3f" % final)
lines.append(" Publish? [Y/N] <- human review only")
lines.append("")
lines.append("")
# Summary block (the founder's key metric)
lines.append("=" * 70)
lines.append("BUCKET DISTRIBUTION")
lines.append("=" * 70)
for b in REVIEW_BUCKETS:
n = len(by_bucket.get(b, []))
if n:
lines.append(" %-16s %3d" % (b, n))
lines.append("")
lines.append("HUMAN REVIEW TALLY (fill in after manual pass):")
lines.append(" Published: %d" % total_pub)
lines.append(" Rejected: %d" % total_rej)
lines.append(" Borderline: %d" % total_border)
lines.append("")
lines.append("First question is not 'is the classifier accurate?'")
lines.append("First question: 'Would we proudly publish these stories?'")
lines.append("=" * 70)
report = "\n".join(lines) + "\n"
if out_path is None:
out_path = os.path.join(os.path.dirname(HERE), "athena_review_report.txt")
with open(out_path, "w") as f:
f.write(report)
print("[review] wrote %s (%d stories)" % (out_path, len(rows)))
# also print to stdout for immediate visibility
print(report)
return report
def main():
ap = argparse.ArgumentParser(description="Athena Sprint 1 scoring (pure rules)")
ap.add_argument("--migrate", action="store_true", help="add new columns")
ap.add_argument("--backfill", action="store_true", help="score all unscored rows")
ap.add_argument("--review", action="store_true", help="write manual review report")
ap.add_argument("--all", action="store_true", help="migrate + backfill + review")
ap.add_argument("--limit", type=int, default=200, help="review story count")
ap.add_argument("--dry-run", action="store_true", help="classify but don't write")
ap.add_argument("--db", default=DB_PATH, help="db path override")
args = ap.parse_args()
if args.all:
migrate(args.db)
attach_scoring(args.db, dry_run=args.dry_run)
generate_review(args.db, limit=args.limit)
else:
if args.migrate:
migrate(args.db)
if args.backfill:
attach_scoring(args.db, dry_run=args.dry_run)
if args.review:
generate_review(args.db, limit=args.limit)
if not (args.migrate or args.backfill or args.review):
ap.print_help()
if __name__ == "__main__":
main()