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.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# athena package — Sprint 1 pure-rule scoring lives in scoring.py
|
||||
@@ -0,0 +1,545 @@
|
||||
#!/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()
|
||||
File diff suppressed because it is too large
Load Diff
+10
@@ -27,6 +27,10 @@ sys.path.insert(0, os.path.dirname(__file__))
|
||||
from adapters import SourceAdapter
|
||||
from adapters._store import upsert_entries
|
||||
|
||||
# Sprint 1 (2026-07-15): pure-rule bucket classifier + scorer.
|
||||
# Attaches immediately after ingest/dedup and before any rendering step.
|
||||
from athena import scoring as _scoring
|
||||
|
||||
# Adapter registry — add new adapters here (one line each)
|
||||
ADAPTERS = {
|
||||
"github": lambda: __import__("adapters.github", fromlist=["GitHubAdapter"]).GitHubAdapter(),
|
||||
@@ -273,6 +277,12 @@ def run_pipeline(sources: list[str] | None = None, limit: int = 20, dry_run: boo
|
||||
if src in source_stats:
|
||||
source_stats[src]["stored"] += 1
|
||||
|
||||
# --- Sprint 1 attach point: score after ingest/dedup, before render ---
|
||||
try:
|
||||
_scoring.attach_scoring(db_path)
|
||||
except Exception as e:
|
||||
print(f" ⚠ scoring attach failed: {e}")
|
||||
|
||||
# Verification
|
||||
if verify:
|
||||
print(f"\n [Verification]")
|
||||
|
||||
Reference in New Issue
Block a user