/** * News relevance audit for third-party publishers (sources.kind = news): which stored items would the tightened * `news_article_v1` gate still index today? An item is on-topic when its title + summary carry strong data-center * vocabulary in the lead (`relevanceOf` = strong and `leadRelevanceOf`), off-topic otherwise (industry chatter, * newsletters, job boards, chips, opinion). Also checks the ≤ 400-character summary rule. * * set -a; source .env; set +a * node node_modules/tsx/dist/cli.mjs scripts/quality-news.ts # report (share by connector + sample) * node node_modules/tsx/dist/cli.mjs scripts/quality-news.ts --apply # delete off-topic kind=news items (+ their events), trim long summaries * node node_modules/tsx/dist/cli.mjs scripts/quality-news.ts --sample 60 # random sample for hand classification */ import { closeDb, getDb, sql } from "@dci/db"; import { leadRelevanceOf, relevanceOf, isNonProjectTitle } from "../apps/worker/src/connectors/news/extract-project.js"; import { shortSummary, THIRD_PARTY_SUMMARY_MAX } from "../apps/worker/src/ingest/news.js"; const args = process.argv.slice(2); const APPLY = args.includes("--apply"); const SAMPLE = args.includes("--sample") ? Number(args[args.indexOf("--sample") + 1] ?? 60) : 0; async function main(): Promise { const db = getDb(); const rows = await db.execute(sql`select n.id, n.connector_id, n.title, n.summary, n.event_type, n.page_type, s.kind from news_items n join sources s on s.id = n.source_id order by n.created_at`); const items = rows.map((r) => ({ id: String(r.id), connector: String(r.connector_id), title: String(r.title), summary: r.summary == null ? null : String(r.summary), eventType: r.event_type == null ? null : String(r.event_type), kind: String(r.kind) })); const third = items.filter((i) => i.kind === "news"); if (SAMPLE) { for (const i of [...third].sort(() => Math.random() - 0.5).slice(0, SAMPLE)) console.log(`${i.connector} | ${i.eventType ?? "-"} | ${i.title.slice(0, 110)} | ${(i.summary ?? "").slice(0, 120)}`); return; } const judge = (i: { title: string; summary: string | null }) => { const text = i.summary ?? ""; const rel = relevanceOf(i.title, text); if (rel !== "strong") return `off-topic:${rel}`; if (!leadRelevanceOf(i.title, text)) return "off-topic:body-only"; return isNonProjectTitle(i.title) ? "on-topic:non-project" : "on-topic"; }; const perConnector = new Map(); const off: typeof third = []; const longSummaries = third.filter((i) => (i.summary?.length ?? 0) > THIRD_PARTY_SUMMARY_MAX); for (const i of third) { const v = judge(i); const s = perConnector.get(i.connector) ?? { total: 0, off: 0 }; s.total++; if (v.startsWith("off-topic")) { s.off++; off.push(i); } perConnector.set(i.connector, s); } console.log(`third-party (kind=news) items: ${third.length} — off-topic by the current gate: ${off.length} (${((off.length / Math.max(1, third.length)) * 100).toFixed(0)} %) — summaries > ${THIRD_PARTY_SUMMARY_MAX} chars: ${longSummaries.length}`); for (const [c, s] of [...perConnector].sort((a, b) => b[1].off - a[1].off)) console.log(` ${c.padEnd(24)} ${s.off}/${s.total}`); console.log("\noff-topic sample:"); for (const i of off.slice(0, 40)) console.log(` ${i.connector.padEnd(22)} ${i.title.slice(0, 100)}`); // operator newsrooms are kept whatever the topic (the source's own news), but report how many carry a non-project headline with a wrong event type const opWrong = items.filter((i) => i.kind !== "news" && i.eventType && !["news", "page_changed", "acquisition", "closure"].includes(i.eventType) && isNonProjectTitle(i.title)); console.log(`\noperator / government items with a people/deal/money headline but a specific event type: ${opWrong.length} (event type is reset to 'news' on reprocess)`); if (!APPLY) { console.log("\n(dry run — pass --apply to delete off-topic third-party items and trim long summaries)"); return; } let deleted = 0, trimmed = 0; for (const i of off) { await db.transaction(async (tx) => { await tx.execute(sql`delete from events where entity_type = 'news_event' and entity_id = ${i.id}`); await tx.execute(sql`delete from news_items where id = ${i.id}`); }); deleted++; } for (const i of longSummaries) { if (off.includes(i)) continue; await db.execute(sql`update news_items set summary = ${shortSummary(i.summary)} where id = ${i.id}`); trimmed++; } console.log(`deleted ${deleted} off-topic items, trimmed ${trimmed} summaries`); } main().catch((e) => { console.error(e); process.exitCode = 1; }).finally(() => closeDb());