SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
2 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
4.6 KB · 72 lines typescript
Raw Blame History
1/**2 * News relevance audit for third-party publishers (sources.kind = news): which stored items would the tightened3 * `news_article_v1` gate still index today? An item is on-topic when its title + summary carry strong data-center4 * vocabulary in the lead (`relevanceOf` = strong and `leadRelevanceOf`), off-topic otherwise (industry chatter,5 * newsletters, job boards, chips, opinion). Also checks the ≤ 400-character summary rule.6 *7 *   set -a; source .env; set +a8 *   node node_modules/tsx/dist/cli.mjs scripts/quality-news.ts               # report (share by connector + sample)9 *   node node_modules/tsx/dist/cli.mjs scripts/quality-news.ts --apply       # delete off-topic kind=news items (+ their events), trim long summaries10 *   node node_modules/tsx/dist/cli.mjs scripts/quality-news.ts --sample 60   # random sample for hand classification11 */12import { closeDb, getDb, sql } from "@dci/db";13import { leadRelevanceOf, relevanceOf, isNonProjectTitle } from "../apps/worker/src/connectors/news/extract-project.js";14import { shortSummary, THIRD_PARTY_SUMMARY_MAX } from "../apps/worker/src/ingest/news.js";1516const args = process.argv.slice(2);17const APPLY = args.includes("--apply");18const SAMPLE = args.includes("--sample") ? Number(args[args.indexOf("--sample") + 1] ?? 60) : 0;1920async function main(): Promise<void> {21  const db = getDb();22  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`);23  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) }));24  const third = items.filter((i) => i.kind === "news");25  if (SAMPLE) {26    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)}`);27    return;28  }29  const judge = (i: { title: string; summary: string | null }) => {30    const text = i.summary ?? "";31    const rel = relevanceOf(i.title, text);32    if (rel !== "strong") return `off-topic:${rel}`;33    if (!leadRelevanceOf(i.title, text)) return "off-topic:body-only";34    return isNonProjectTitle(i.title) ? "on-topic:non-project" : "on-topic";35  };36  const perConnector = new Map<string, { total: number; off: number }>();37  const off: typeof third = [];38  const longSummaries = third.filter((i) => (i.summary?.length ?? 0) > THIRD_PARTY_SUMMARY_MAX);39  for (const i of third) {40    const v = judge(i);41    const s = perConnector.get(i.connector) ?? { total: 0, off: 0 };42    s.total++;43    if (v.startsWith("off-topic")) { s.off++; off.push(i); }44    perConnector.set(i.connector, s);45  }46  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}`);47  for (const [c, s] of [...perConnector].sort((a, b) => b[1].off - a[1].off)) console.log(`  ${c.padEnd(24)} ${s.off}/${s.total}`);48  console.log("\noff-topic sample:");49  for (const i of off.slice(0, 40)) console.log(`  ${i.connector.padEnd(22)} ${i.title.slice(0, 100)}`);50  // 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 type51  const opWrong = items.filter((i) => i.kind !== "news" && i.eventType && !["news", "page_changed", "acquisition", "closure"].includes(i.eventType) && isNonProjectTitle(i.title));52  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)`);5354  if (!APPLY) { console.log("\n(dry run — pass --apply to delete off-topic third-party items and trim long summaries)"); return; }55  let deleted = 0, trimmed = 0;56  for (const i of off) {57    await db.transaction(async (tx) => {58      await tx.execute(sql`delete from events where entity_type = 'news_event' and entity_id = ${i.id}`);59      await tx.execute(sql`delete from news_items where id = ${i.id}`);60    });61    deleted++;62  }63  for (const i of longSummaries) {64    if (off.includes(i)) continue;65    await db.execute(sql`update news_items set summary = ${shortSummary(i.summary)} where id = ${i.id}`);66    trimmed++;67  }68  console.log(`deleted ${deleted} off-topic items, trimmed ${trimmed} summaries`);69}7071main().catch((e) => { console.error(e); process.exitCode = 1; }).finally(() => closeDb());72