← back to Crazy News Channel
scripts/lib/auto-tag-real-news.mjs
101 lines
// scripts/lib/auto-tag-real-news.mjs — shared real-news auto-tagging
// heuristic. Deterministic, $0, no API call, no LLM.
//
// HARD RULE (TK-12165, 2026-09-24): every entry in real-news-data.js MUST
// carry a non-empty `tags` array (3-6 short, lowercase-ish, human-readable
// tags). fetch-real-news.mjs imports autoTagRealNews() from here on every
// refresh so a future run can never write an untagged entry. If the
// keyword vocabulary below needs a new rule for a topic that keeps
// recurring untagged, add it here — this is the ONE place both the
// backfill and the live fetcher read from, so the vocabulary stays shared
// and tags actually recur across posts instead of drifting apart.
const KEYWORD_RULES = [
[/migrant|migration|border cross|channel cross/i, "immigration"],
[/senate race|house race|campaign trail|primary election|\bballot\b|\bcandidate\b/i, "elections"],
[/\bsenate\b|\bcongress\b|\bcapitol\b|lawmakers|legislation|\bbill\b/i, "congress"],
[/white house|the administration\b/i, "white house"],
[/\bfaa\b|\bairline\b|\bairport\b|aviation/i, "aviation"],
[/nor'?easter|hurricane|tropical storm|storm surge/i, "storms"],
[/flooding|\bflood\b/i, "flooding"],
[/el ni[nñ]o|la ni[nñ]a/i, "climate patterns"],
[/heat wave|heatwave|triple-digit/i, "heat wave"],
[/wildfire|\bdrought\b/i, "climate patterns"],
[/privacy|smartglasses|smart glasses/i, "consumer tech"],
[/\bapple\b|iphone|apple watch|mac mini|macbook/i, "apple"],
[/\bgoogle\b|pixel \d|\bgemini\b/i, "google"],
[/\bmeta\b|zuckerberg|\bfacebook\b/i, "meta"],
[/\bcamera\b|\bnikon\b|\bcanon\b|sony a\d/i, "cameras"],
[/headphones|earbuds|\bbeats\b|audio gear/i, "audio gear"],
[/\bnfl\b|quarterback|touchdown|packers|falcons|\bgiants\b|chiefs|cowboys/i, "nfl"],
[/college football|\bncaa\b/i, "college football"],
[/\bboxing\b|heavyweight|\bufc\b|\bmma\b/i, "combat sports"],
[/music video|\balbum\b|\btour\b|\bvmas\b|\bgrammy\b|billboard/i, "music"],
[/box office|\bfilm\b|\bmovie\b|\bstudio\b/i, "film & tv"],
[/streaming|netflix|\bhbo\b|paramount\+/i, "streaming tv"],
[/survivor|reality (tv|show)/i, "reality tv"],
[/\bmerger\b|antitrust/i, "corporate mergers"],
[/mortgage|housing market|home prices/i, "housing market"],
[/stock market|s&p 500|dow jones|nasdaq/i, "stock market"],
[/prediction market|polymarket|\bgambling\b/i, "prediction markets"],
[/data center|\borbital\b|space launch|\brocket\b/i, "space & data centers"],
[/\boil\b|\bopec\b|energy prices|strait of hormuz/i, "energy markets"],
[/missile|airstrike|drone attack|\bmilitary\b/i, "military conflict"],
[/\bnato\b|\brussia\b|\bukraine\b|geopolit/i, "geopolitics"],
[/\barrest\b|stabbing|\bpolice\b|\bcrime\b/i, "crime"],
[/hitler|\bnazi\b|world war/i, "history"],
];
const CATEGORY_FALLBACK = {
politics: "washington",
weather: "forecast",
scitech: "tech news",
sports: "game day",
entertainment: "pop culture",
business: "markets",
uncategorized: "world news",
};
function normalizeOutlet(name) {
if (!name) return null;
let n = String(name).trim();
n = n.replace(/\.com$/i, "");
const MAP = {
"9to5Google": "9to5google",
"bgr": "bgr",
"gearpatrol": "gear patrol",
"arstechnica": "ars technica",
"kare11": "kare 11",
"ABC7 Los Angeles": "abc7",
"E! News": "e! news",
};
if (MAP[n]) return MAP[n];
return n.toLowerCase();
}
// Returns a tags array, always length 3-6, for one real-news story object
// { category, categoryLabel, sourceName, stages: [{headline}] }.
export function autoTagRealNews(story) {
const headline = (story.stages && story.stages[0] && story.stages[0].headline) || "";
const tags = [];
const seen = new Set();
const push = (t) => {
if (!t) return;
const k = t.toLowerCase();
if (seen.has(k)) return;
seen.add(k);
tags.push(t);
};
push((story.categoryLabel || story.category || "").toLowerCase());
push(normalizeOutlet(story.sourceName));
for (const [re, tag] of KEYWORD_RULES) {
if (tags.length >= 6) break;
if (re.test(headline)) push(tag);
}
if (tags.length < 3) {
push(CATEGORY_FALLBACK[story.category] || "wire report");
}
if (tags.length < 3) push("wire report");
return tags.slice(0, 6);
}