Skip to content

fix(brief): add source-domain STRONG signal to classifyOpinion (May 19 regression)#3835

Merged
koala73 merged 1 commit into
mainfrom
fix/brief-opinion-source-domain
May 19, 2026
Merged

fix(brief): add source-domain STRONG signal to classifyOpinion (May 19 regression)#3835
koala73 merged 1 commit into
mainfrom
fix/brief-opinion-source-domain

Conversation

@koala73

@koala73 koala73 commented May 19, 2026

Copy link
Copy Markdown
Owner

Symptom

The 2026-05-19 0801 Pro brief shipped "How nuclear war would impact the global food system" from Bulletin of Atomic Scientists as CRITICAL story #6, sitting alongside breaking news about the Iran-Israel war and the WHO Ebola declaration. Pro subscriber filed the complaint that surfaced 5 distinct content-quality defects on the same day; this PR addresses defect #2.

Root cause

`classifyOpinion` has two existing STRONG signals (URL path segments + headline prefix) and three CORROBORATING signals (description framing, quote-wrap, `/analysis/` path). All five missed the Bulletin piece:

The signal IS the publisher. Bulletin of Atomic Scientists, Project Syndicate, Foreign Affairs, etc. are entirely commentary — no hard-news section to discriminate via URL path.

Fix

Add STRONG #3: a hand-curated hostname allowlist matched against the parsed `URL().hostname` (not raw `.includes()`), suffix-anchored so subdomains (`newsletter.`, `m.`, `www.`) match but typo-domains (`evilthebulletin.org`) don't.

Initial list (per docs/plans/2026-05-19-001 U1):

  • `thebulletin.org` — Bulletin of the Atomic Scientists
  • `project-syndicate.org` — op-eds from world leaders / academics
  • `foreignaffairs.com` — CFR's analysis quarterly
  • `foreignpolicy.com` — Foreign Policy magazine
  • `warontherocks.com` — defense analysis blog

Maintenance commitment documented in the source: quarterly review against `droppedOpinion` telemetry. Rollback is remove-from-Set, never ad-hoc URL exceptions.

Test plan

  • 7 new test cases:
    • REGRESSION (May 19): Bulletin URL → opinion ✓
    • 4 other allowlisted publishers → opinion ✓
    • Subdomains (newsletter., m., www.) → match ✓
    • Typo-domains (`evilthebulletin.org`) → do NOT match ✓
    • Tracking params / fragments containing host name → do NOT match (closes the injection vector documented in PR fix(brief): exclude feel-good/lifestyle content from the digest pool #3748) ✓
    • Malformed URLs → no throw ✓
    • Allowlisted host with hard-news-shaped headline → still opinion (the publisher IS the signal) ✓
  • All 25 tests pass (`npx tsx --test tests/opinion-classifier.test.mjs`)
  • `npm run typecheck` clean
  • `biome lint` clean on changed files
  • After deploy: monitor `droppedOpinion` counter in `seed-digest-notifications.mjs` for 7 days; verify the new STRONG signal is hitting the expected publisher cohort without over-firing

Origin

PR-1 of the 4-PR Phase 1 wave from `docs/plans/2026-05-19-001-fix-brief-content-quality-regressions-plan.md`.

Sibling pattern: PR #3690 (opinion classifier original) and the May 17 feelgood-filter plan use the same ingest-stamp + read-time-re-classify wiring. This PR adds a new STRONG signal to the existing classifier; no new classifier, no new ingest-side stamp.

@vercel

vercel Bot commented May 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment May 19, 2026 10:12am

Request Review

@greptile-apps

greptile-apps Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a third STRONG signal to classifyOpinion — a hostname allowlist for commentary-only publishers — to fix the May 19 regression where a Bulletin of Atomic Scientists piece shipped as a breaking-news brief story. The implementation is careful: hostname is parsed (not substring-matched), the match is suffix-anchored to allow subdomains while rejecting typo-domains, and defensive URL handling is consistent with the existing safePathname pattern.

  • New COMMENTARY_HOSTNAMES Set + matchesCommentaryHost: five curated publishers are matched via exact-host or .endsWith('.' + entry) suffix anchor, closing the tracking-param/fragment injection vector that raw includes() would open.
  • Double URL parse: safePathname and safeHostname both call new URL(link) on the same string; the hostname could be extracted once alongside the pathname.
  • foreignpolicy.com scope: FP operates a continuous news desk in addition to its analysis columns; all FP URLs will now be classified as opinion, which may silently drop legitimate breaking-news events from briefs.

Confidence Score: 3/5

The core classifier logic and URL-parsing safety are sound, but including foreignpolicy.com in the blanket allowlist may silently suppress genuine breaking-news stories from that outlet.

The Bulletin/Project Syndicate/War on the Rocks inclusions are editorially clean — those outlets truly publish no hard-news wires. Foreign Policy is materially different: it runs a continuous news desk alongside its analysis pieces, so a blanket hostname match will drop real events without any fallback. That is a present, production-visible drop in brief quality on every FP story from the moment this deploys.

The COMMENTARY_HOSTNAMES Set in server/_shared/opinion-classifier.js — specifically the foreignpolicy.com entry — needs editorial verification before merge.

Important Files Changed

Filename Overview
server/_shared/opinion-classifier.js Adds STRONG #3: a hostname allowlist check using a new safeHostname() helper and matchesCommentaryHost(). Logic is sound with suffix-anchor and defensive URL parsing, but URL is now parsed twice per call; also, foreignpolicy.com publishes event-driven news briefs which may cause over-filtering.
tests/opinion-classifier.test.mjs Adds 7 well-targeted test cases covering the regression, all 4 new publishers, subdomain matching, typo-domain rejection, tracking-param/fragment injection guards, malformed URL safety, and the publisher-IS-the-signal design invariant.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[classifyOpinion story] --> B[Extract title / link / description]
    B --> C[safePathname link → pathname]
    C --> D{STRONG 1\nURL path segment\n/opinion/, /views/, …}
    D -->|match| Z[return true\nopinion]
    D -->|no match| E{STRONG 2\nHeadline prefix\nOpinion: Analysis: …}
    E -->|match| Z
    E -->|no match| F[safeHostname link → hostname\nnew in this PR]
    F --> G{STRONG 3 NEW\nmatchesCommentaryHost\nhostname === entry OR\nhostname.endsWith '.' + entry}
    G -->|match| Z
    G -->|no match| H[Accumulate CORROBORATING signals]
    H --> I{corroborating ≥ 2?}
    I -->|yes| Z
    I -->|no| J[return false\nhard news]
Loading

Comments Outside Diff (1)

  1. server/_shared/opinion-classifier.js, line 172-173 (link)

    P2 Double URL parse per call — safePathname already invokes new URL(link) (stored in pathname at line 173), and safeHostname invokes it a second time at line 191. Since link is the same string both times the result is always consistent, but there's no reason to pay the parse cost twice. Extracting the hostname once alongside the pathname keeps the two helpers aligned.

Reviews (1): Last reviewed commit: "fix(brief): add source-domain STRONG sig..." | Re-trigger Greptile

// section to distinguish from. Hostname match on the parsed URL
// only, suffix-anchored to permit `newsletter.<host>` and `m.<host>`
// while rejecting typo-domains.
if (matchesCommentaryHost(safeHostname(link))) return true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 If the hostname is hoisted to a variable (see the double-parse comment above), the call site should use that cached value instead of reparsing.

Suggested change
if (matchesCommentaryHost(safeHostname(link))) return true;
if (matchesCommentaryHost(hostname)) return true;

Comment thread server/_shared/opinion-classifier.js Outdated
Comment on lines +86 to +87
'foreignpolicy.com', // Foreign Policy magazine
'warontherocks.com', // War on the Rocks (defense analysis blog)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 foreignpolicy.com may over-filter breaking-news events

Foreign Policy runs a continuous news desk alongside its analysis columns — "The Cable" and their daily news briefings cover diplomatic events, troop movements, and sanctions announcements that are structurally identical to hard-news stories you'd want in the brief. Classifying every foreignpolicy.com URL as opinion would silently drop those events. The selection criterion in the comment above ("They do NOT publish breaking-news wires or event coverage") is not met by FP the way it is for, say, thebulletin.org or project-syndicate.org. Worth confirming this was intentional before it ships, or narrowing the match to FP's known opinion subdirectories instead.

…9 regression)

The 2026-05-19 Pro brief shipped "How nuclear war would impact the
global food system" from Bulletin of Atomic Scientists as CRITICAL
story #6, sitting alongside breaking news about the Iran-Israel war
and the WHO Ebola declaration. The brief promises event-driven
intelligence — a Bulletin analysis essay is not an event.

The existing classifier missed it on all three current signals:
- STRONG #1 (URL section /opinion/): Bulletin URLs have no opinion-
  style path because the WHOLE SITE is commentary, no hard-news
  section to distinguish from.
- STRONG #2 (headline prefix "Opinion:"): hard-news-shaped title.
- CORROBORATING (quote-wrap + columnist framing): description reads
  like a news lede.

The signal IS the publisher. Add STRONG #3: a hand-curated allowlist
of commentary-only publishers, matched by hostname (suffix-anchored to
permit `newsletter.<host>` / `m.<host>` while rejecting typo-domains
like `evilthebulletin.org`).

Initial list (per docs/plans/2026-05-19-001 U1):
- thebulletin.org      — Bulletin of Atomic Scientists
- project-syndicate.org — op-eds from world leaders / academics
- foreignaffairs.com   — CFR's analysis quarterly
- foreignpolicy.com    — Foreign Policy magazine
- warontherocks.com    — defense analysis blog

Maintenance commitment: quarterly review against `droppedOpinion`
telemetry to catch (a) new commentary publishers to add, (b) listed
publishers that launched a hard-news section. Rollback path is
remove-from-Set, never per-URL exceptions (cruft).

Pattern mirrors the existing safePathname/STRONG_URL_SEGMENTS shape;
new safeHostname helper parses URL().hostname so the match is on
the parsed host (not raw .includes() on the link string — closes the
tracking-param injection vector documented in PR #3748).

7 new test cases cover the May 19 regression, the 4 other publishers,
subdomain matching, typo-domain rejection, tracking-param / fragment
spoofing, malformed URLs, and the "allowlisted host PLUS hard-news
content → still opinion" rule.

This is PR-1 of the 4-PR Phase 1 wave from
docs/plans/2026-05-19-001-fix-brief-content-quality-regressions-plan.md.
@koala73

koala73 commented May 19, 2026

Copy link
Copy Markdown
Owner Author

P1 confirmed and fixed in c32fbcfd3.

Foreign Policy runs hard-news surfaces (World Brief, Situation Report, Morning Brief) alongside their op-ed section — blanket hostname allowlist violates the file's own selection criterion #2 ("they do NOT publish breaking-news / event coverage"). Your verbatim G-7 finance-ministers story would have been silently dropped.

Changes:

  • Removed foreignpolicy.com from COMMENTARY_HOSTNAMES.
  • Added a multi-line // NOTE: block at the deletion site explaining WHY it's excluded so a future contributor doesn't re-add it. Documents that FP's actual opinion section is caught via the existing /opinion/ path segment (STRONG Batch market and RSS fetching with progressive updates #1) or the "Opinion:" headline prefix (STRONG Add smart zoom visibility for dense map layers #2) — that's the right granularity for mixed-content publishers.
  • Two new regression tests:
    • The G-7 Finance Ministers story you cited → false (must NOT be classified as opinion).
    • An FP /opinion/ path → true (FP commentary is still caught via the existing path-segment signal, demonstrating the right granularity).
  • Updated the existing "Project Syndicate, Foreign Affairs, Foreign Policy, War on the Rocks → opinion" test to drop FP from the asserted set.

Verified the other 4 hostnames still meet criterion #2:

  • thebulletin.org — entirely commentary/analysis, no hard-news surface
  • project-syndicate.org — entirely op-eds from world leaders / academics
  • foreignaffairs.com — Foreign Affairs is long-form essays; doesn't run breaking-news wires
  • warontherocks.com — defense-analysis blog

27 tests pass (was 25); 2 new regression cases pin the FP exclusion against future re-addition.

@koala73
koala73 merged commit 76cc4a1 into main May 19, 2026
11 checks passed
@koala73
koala73 deleted the fix/brief-opinion-source-domain branch May 19, 2026 13:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant